Skip to content

feat(mcp): funnel telemetry, health, rate-limit headers, playground, idempotency - #19

Merged
criptogus merged 2 commits into
mainfrom
claude/mcp-roadmap-slice-1
May 23, 2026
Merged

criptogus merged 2 commits into
mainfrom
claude/mcp-roadmap-slice-1

Conversation

@criptogus

Copy link
Copy Markdown
Owner

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/funnel

  • mcp_funnel_events table with a closed event vocabulary (CHECK constraint) so we don't end up with 127 spellings of the same event.
  • record_mcp_funnel_event RPC: anonymous-callable, rate-limited 60/min per anon_hash, swallows errors so telemetry never breaks a user flow.
  • Events fire from /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/funnel with conversion rates against connect_viewed.

2. Health endpoint + status page

  • GET /api/mcp/health (CORS) → { ok, version, uptime_seconds, db: { ok, ping_ms }, … }.
  • /status polls it every 15s and shows live operational/degraded state + DB latency.

3. Rate-limit headers on every quota response

  • /api/mcp now sets X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset, X-RateLimit-Window on 200s as well as 429s. Agents can self-regulate before hitting the ceiling.

4. Playground at /play

  • Wraps the existing McpTester in a marketing landing — pick a tool, fill args, hit run. Zero-install evaluation path.

5. Idempotency keys

  • upload_packages and request_primitive accept an optional idempotency_key. Backed by mcp_idempotency (24h TTL, lazy GC) via mcp_idempotency_get/_put SECURITY DEFINER RPCs.
  • Retries within 24h return { ...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 event
  • src/lib/mcp/tools/skills.ts — idempotency in upload_packages + request_primitive
  • src/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 emission

Test plan

  • Visit /connect then /oauth/authorize?... → click Authorize → land on /oauth/success. Confirm /admin/funnel shows connect_viewed, oauth_authorize_viewed, oauth_authorize_approved, oauth_success_shown with counts.
  • Hit /api/mcp with a tools/call → response has X-RateLimit-Limit, X-RateLimit-Remaining headers; second response decrements Remaining.
  • Hit /api/mcp/health → 200 with db.ping_ms < 200 typically; visit /status and confirm the green banner.
  • On /play, click a tool, run it, see the JSON response inline.
  • Call upload_packages twice with the same idempotency_key → second response has replayed: true and no new draft is created (check /account/packages).
  • As a non-admin, /admin/funnel returns Forbidden.

https://claude.ai/code/session_019gMoupKKTVydpNwiiACQRd


Generated by Claude Code

…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
@criptogus
criptogus marked this pull request as ready for review May 23, 2026 01:47
@criptogus

Copy link
Copy Markdown
Owner Author

@copilot resolve the merge conflicts in this pull request

@criptogus

Copy link
Copy Markdown
Owner Author

@copilot resolve the merge conflicts in this pull request

…ice-1

# Conflicts:
#	src/routes/oauth.success.tsx
@criptogus
criptogus merged commit 25072e1 into main May 23, 2026
1 check passed
Copilot stopped work on behalf of criptogus due to an error May 23, 2026 01:50

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment on lines +152 to +155
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 $$

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge 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 👍 / 👎.

Comment on lines +312 to 318
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);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge 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 👍 / 👎.

Comment on lines +6 to +8
export const getMcpFunnelSummary = createServerFn({ method: "GET" })
.middleware([requireSupabaseAuth])
.inputValidator((d: unknown) =>

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge 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 👍 / 👎.

Comment thread src/routes/api/mcp.ts
Comment on lines +273 to +277
// 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);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants