feat(mcp): funnel telemetry, health, rate-limit headers, playground, idempotency - #19
Conversation
…idempotency
Slice 1 of the MCP UX roadmap. Five orthogonal improvements landed
together because they all hit /api/mcp or the connect flow.
1. Funnel telemetry — new mcp_funnel_events table with a closed
event vocabulary and a SECURITY DEFINER RPC that anonymous
browsers and the server can both call. Events fire from
/oauth/authorize (viewed/approved/denied), /oauth/success
(shown/loopback/scheme/manual_copy), /connect (viewed), the
install buttons, the PAT mint server fn, and /api/mcp (first
tools/call + first write). Aggregates exposed via the
mcp_funnel_summary RPC and rendered at /admin/funnel with
conversion rates against connect_viewed.
2. Health endpoint — GET /api/mcp/health returns { ok, db.ping_ms,
version, uptime } with CORS. Public on purpose — status pages
shouldn't require auth.
3. /status page — polls /api/mcp/health every 15s and shows a
live operational/degraded banner plus DB latency + uptime so
users (and us) can sanity-check the service.
4. Rate-limit headers — /api/mcp now emits X-RateLimit-Limit,
-Remaining, -Reset and -Window on EVERY quota-bearing response,
not just 429s. Lets agents self-regulate before they hit the
ceiling.
5. Playground at /play — chat-style UI wrapping the existing
McpTester with one-click run for every tool, plus a token
paste box for write tools. Zero install path for evaluators.
6. Idempotency keys — upload_packages and request_primitive now
accept an optional idempotency_key. New mcp_idempotency table
caches the response for 24h per (user_id, key_hash, tool); retry
returns { ...response, replayed: true }. Kills the duplicate
uploads we see whenever an agent retries a flaky network call.
Telemetry is fire-and-forget end-to-end: the RPC swallows errors,
the server fn never throws, and a failure to record an event will
never break a user flow.
https://claude.ai/code/session_019gMoupKKTVydpNwiiACQRd
|
@copilot resolve the merge conflicts in this pull request |
|
@copilot resolve the merge conflicts in this pull request |
…ice-1 # Conflicts: # src/routes/oauth.success.tsx
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 1479161b09
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| CREATE OR REPLACE FUNCTION public.mcp_idempotency_put( | ||
| _user_id UUID, _key_hash TEXT, _tool TEXT, _response JSONB | ||
| ) RETURNS VOID | ||
| LANGUAGE plpgsql SECURITY DEFINER SET search_path = public AS $$ |
There was a problem hiding this comment.
Revoke public execute on idempotency SECURITY DEFINER RPCs
These two functions are created as SECURITY DEFINER but never have EXECUTE revoked/granted, so they inherit PostgreSQL’s default function privilege and remain callable by non-service roles. That lets external callers invoke mcp_idempotency_get/_put directly (with arbitrary _user_id/_tool/_key_hash), which can leak cached responses or poison idempotency state. Add explicit REVOKE ... FROM PUBLIC, anon, authenticated and grant only the intended server role.
Useful? React with 👍 / 👎.
| if (idempotency_key) { | ||
| const cached = await getIdempotent(userId, "upload_packages", idempotency_key); | ||
| if (cached) return json({ ...(cached as object), replayed: true }); | ||
| } | ||
| try { | ||
| // Always private. Marketplace listing requires an explicit user action in the UI. | ||
| const results = await processBulkUpload(supabaseAdmin as any, userId, files); |
There was a problem hiding this comment.
Make idempotency check-and-write atomic
This flow checks cache first, performs the write, and only then stores the idempotent response. If two retries with the same idempotency_key arrive concurrently, both requests can miss the cache and both execute processBulkUpload, creating duplicates before either putIdempotent runs. To provide real idempotency under retry races, reserve the key atomically before the side effect (or move the whole operation into one transactional RPC).
Useful? React with 👍 / 👎.
| export const getMcpFunnelSummary = createServerFn({ method: "GET" }) | ||
| .middleware([requireSupabaseAuth]) | ||
| .inputValidator((d: unknown) => |
There was a problem hiding this comment.
Enforce admin middleware on funnel summary endpoint
This server function is labeled “Admin only” but only requires authentication, not admin role. As written, any signed-in user can call it; mcp_funnel_summary just filters rows by auth.uid() and returns an empty result instead of a 403, so /admin/funnel is not actually access-controlled as intended. Use the existing requireAdmin middleware (or an explicit role check) here.
Useful? React with 👍 / 👎.
| // Funnel telemetry: fire-and-forget record of the first successful | ||
| // tools/call this caller has made. The RPC itself dedupes on event + | ||
| // (user_id | anon_hash) so we don't need to check here. | ||
| if (isToolsCall) { | ||
| const identity = quotaIdentity(userId, request); |
There was a problem hiding this comment.
Emit first-call funnel events only once per identity
mcp_first_call/mcp_first_write are recorded on every tools/call request, but this path assumes deduplication that does not exist in record_mcp_funnel_event (the SQL function always inserts). As a result, “first call” metrics are inflated by repeat usage and no longer represent conversion milestones. Add a real uniqueness guard (e.g., unique index/upsert keyed by event + identity) or pre-check before logging.
Useful? React with 👍 / 👎.
Slice 1 of the MCP UX roadmap (the second half — SDK, push notifications and i18n — will land in a follow-up PR).
What's in
1. Funnel telemetry →
/admin/funnelmcp_funnel_eventstable with a closed event vocabulary (CHECK constraint) so we don't end up with 127 spellings of the same event.record_mcp_funnel_eventRPC: anonymous-callable, rate-limited 60/min per anon_hash, swallows errors so telemetry never breaks a user flow./oauth/authorize(viewed/approved/denied),/oauth/success(shown/loopback/scheme/manual_copy),/connect(viewed), the install buttons (install_button_clicked), the PAT mint server fn (pat_minted), and/api/mcp(mcp_first_call/mcp_first_write).mcp_funnel_summary(days)aggregate, rendered at/admin/funnelwith conversion rates againstconnect_viewed.2. Health endpoint + status page
GET /api/mcp/health(CORS) →{ ok, version, uptime_seconds, db: { ok, ping_ms }, … }./statuspolls it every 15s and shows live operational/degraded state + DB latency.3. Rate-limit headers on every quota response
/api/mcpnow setsX-RateLimit-Limit,X-RateLimit-Remaining,X-RateLimit-Reset,X-RateLimit-Windowon 200s as well as 429s. Agents can self-regulate before hitting the ceiling.4. Playground at
/playMcpTesterin a marketing landing — pick a tool, fill args, hit run. Zero-install evaluation path.5. Idempotency keys
upload_packagesandrequest_primitiveaccept an optionalidempotency_key. Backed bymcp_idempotency(24h TTL, lazy GC) viamcp_idempotency_get/_putSECURITY DEFINER RPCs.{ ...originalResponse, replayed: true }— kills the duplicate uploads we see whenever an agent retries a flaky call.Files
supabase/migrations/20260524000000_mcp_funnel_and_idempotency.sql(new)src/lib/telemetry/funnel.functions.ts(new)src/lib/admin/funnel.functions.ts(new)src/lib/mcp/idempotency.ts(new)src/routes/admin.funnel.tsx(new)src/routes/api/mcp/health.ts(new)src/routes/status.tsx(new)src/routes/play.tsx(new)src/routes/api/mcp.ts— rate-limit headers, first-call eventsrc/lib/mcp/tools/skills.ts— idempotency inupload_packages+request_primitivesrc/routes/oauth.authorize.tsx,src/routes/oauth.success.tsx,src/components/site/InstallButtons.tsx,src/routes/connect.tsx,src/lib/account/tokens.functions.ts— event emissionTest plan
/connectthen/oauth/authorize?...→ click Authorize → land on/oauth/success. Confirm/admin/funnelshowsconnect_viewed,oauth_authorize_viewed,oauth_authorize_approved,oauth_success_shownwith counts./api/mcpwith atools/call→ response hasX-RateLimit-Limit,X-RateLimit-Remainingheaders; second response decrementsRemaining./api/mcp/health→ 200 withdb.ping_ms < 200typically; visit/statusand confirm the green banner./play, click a tool, run it, see the JSON response inline.upload_packagestwice with the sameidempotency_key→ second response hasreplayed: trueand no new draft is created (check/account/packages)./admin/funnelreturns Forbidden.https://claude.ai/code/session_019gMoupKKTVydpNwiiACQRd
Generated by Claude Code