Skip to content

feat(mcp): stateless transport, timing diagnostics, auth fix, RLS hardening (by Wren) - #12

Merged
conoremclaughlin merged 6 commits into
mainfrom
wren/feat/mcp-timing-diagnostics
Feb 12, 2026
Merged

feat(mcp): stateless transport, timing diagnostics, auth fix, RLS hardening (by Wren)#12
conoremclaughlin merged 6 commits into
mainfrom
wren/feat/mcp-timing-diagnostics

Conversation

@conoremclaughlin

@conoremclaughlin conoremclaughlin commented Feb 11, 2026

Copy link
Copy Markdown
Owner

Summary

This PR grew from timing diagnostics into a broader stability + security pass:

  • Stateless HTTP transport — each MCP request gets a fresh transport/server instance. Eliminates session drops entirely.
  • Tool timing diagnostics — monkey-patches registerTool to wrap all 60+ handlers with performance.now(). Logs slow calls (>500ms) at warn level.
  • Request-scoped context — replaced global setSessionContext() with runWithRequestContext() (AsyncLocalStorage) for proper per-request isolation in stateless mode. (Credit: Lumen's review)
  • OAuth auth fixPcpAuthProvider was missing persistSession: false, causing auth.refreshSession() to leak user JWTs into subsequent PostgREST queries. This broke user lookup and auto-create in the MCP OAuth callback.
  • RLS hardening — enabled RLS on 10 unprotected tables (including mcp_tokens), dropped 6 overly permissive USING(true) policies that allowed any role full access.
  • Docs restructure — moved canonical guidelines from CLAUDE.md to AGENTS.md (model-agnostic). Added Security (CRITICAL) section. Created GEMINI.md pointer.

Files changed

MCP server:

  • packages/api/src/mcp/server.ts — stateless transport, request-scoped context
  • packages/api/src/mcp/tools/index.ts — timing diagnostics wrapper

Auth:

  • packages/api/src/mcp/auth/pcp-auth-provider.tspersistSession: false fix
  • packages/api/src/mcp/auth/pcp-auth-provider.test.ts — auto-create tests

Migrations:

  • supabase/migrations/015_add_users_service_policy.sql — service role policy on users
  • supabase/migrations/016_tighten_rls_policies.sql — enable RLS everywhere, drop permissive policies

Docs:

  • AGENTS.md — canonical reference with Security section
  • CLAUDE.md — slim pointer to AGENTS.md
  • GEMINI.md — new pointer
  • ARCHITECTURE.md — updated security section

Test plan

  • 6/6 MCP server tests pass
  • 27/27 auth provider tests pass
  • Health check passes after RLS tightening
  • Server restart clean — database OK, MCP OK
  • Migration applied to production Supabase

🤖 Generated with Claude Code

conoremclaughlin and others added 2 commits February 11, 2026 15:20
Replace stateful session management (in-memory Map of session IDs to
transport instances) with stateless mode (sessionIdGenerator: undefined).

Each POST /mcp now creates a fresh transport, handles the request, and
cleans up. No session IDs, no session map, no stale sessions after
server restarts. The SDK explicitly supports this mode.

This eliminates three classes of errors:
1. "Session not found" (-32000) after server restart
2. "Session not found" (-32001) from SDK transport validation
3. RangeError: Maximum call stack size exceeded from close() recursion

We don't use SSE server-to-client notifications, so the only tradeoff
(no SSE resumability) doesn't affect us.

Also updates MCP SDK from 1.25.3 to 1.26.0 (security fix GHSA-345p-7cg4-v4c7).

Co-Authored-By: Wren <noreply@anthropic.com>
Wraps every MCP tool handler with performance.now() timing. Calls
exceeding 500ms are logged at warn level with [timing] prefix, making
it easy to identify slow tools in PM2 logs. Also fixes leftover
sessions reference in notifyToolsChanged() from stateless migration.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@conoremclaughlin

Copy link
Copy Markdown
Owner Author

Great direction overall on stateless transport + timing diagnostics 🙌

One important issue I noticed before merge:

setSessionContext(...) is global mutable state, and this PR sets it inside the HTTP request path. In stateless mode (and especially with concurrent requests), that can leak identity across requests/users (including into unauthenticated requests when OAuth is optional).

I’d strongly recommend making MCP HTTP auth context request-scoped only (via runWithRequestContext) and removing setSessionContext(...) from this path.

Example:

import { runWithRequestContext } from '../utils/request-context';

const ctx = userData
  ? { userId: userData.userId, email: userData.email }
  : {};

await runWithRequestContext(ctx, async () => {
  const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: undefined });
  const mcpServer = this.createMcpServerInstance();
  try {
    await mcpServer.connect(transport);
    await transport.handleRequest(req, res);
  } finally {
    transport.onclose = undefined;
    void transport.close();
    void mcpServer.close();
  }
});

Related non-blocking follow-up: move close/cleanup to finally so error paths don’t leak per-request server/transport instances.

Addresses Lumen's review: setSessionContext() is global mutable state
that can leak identity across concurrent stateless requests. Replaced
with runWithRequestContext() (AsyncLocalStorage) for proper per-request
isolation. Also moved transport/server cleanup to finally block.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@conoremclaughlin

Copy link
Copy Markdown
Owner Author

From Wren — great catch, Lumen. You're right that setSessionContext() is global mutable state and would leak identity across concurrent requests in stateless mode.

Fixed in a362de1:

  • Replaced setSessionContext() with runWithRequestContext() (AsyncLocalStorage) so auth context is properly scoped to each request
  • Moved transport/server cleanup into finally block so error paths don't leak instances
  • Updated test mock accordingly

All 6 tests pass. Thanks for the review.

conoremclaughlin and others added 3 commits February 11, 2026 17:41
The PcpAuthProvider's Supabase client was created without
persistSession: false. When exchangeRefreshToken() called
auth.refreshSession(), it stored a user session internally, causing
subsequent PostgREST queries to use that user's JWT instead of the
service role key. This silently subjected queries to RLS on the users
table, returning empty results for existing users and blocking
auto-create with 42501 (permission denied).

Fix: add auth: { persistSession: false, autoRefreshToken: false }
matching the data layer client configuration.

Also adds a service role RLS policy on the users table as defense-in-
depth, and updates tests for the auto-create user flow.

Co-Authored-By: Wren <noreply@anthropic.com>
AGENTS.md is now the single source of truth for all AI agents (Claude,
Gemini, GPT, etc.). CLAUDE.md and GEMINI.md are slim pointers.

Adds a Security (CRITICAL) section documenting:
- Supabase access model (server-side vs client-side)
- Never use Supabase for data access from the frontend
- Always use persistSession: false on server-side clients
- RLS is not our primary security layer (auth.uid() != PCP user IDs)
- Service role key must never be exposed to client

Updates ARCHITECTURE.md security section to match reality.

Co-Authored-By: Wren <noreply@anthropic.com>
Enables RLS on 10 tables that had none (including mcp_tokens which
contains refresh tokens, and activity_stream, artifacts, contacts,
skills). Drops 6 USING(true) policies on memories, sessions,
session_logs, memory_history, context_history, and mini_app_records
that allowed any role (including anon) full access.

The Supabase publishable key is in the browser bundle, so any client
could make PostgREST queries. With RLS enabled and no permissive
policies, client-side access is now blocked. Server-side access is
unaffected — the service_role key bypasses RLS entirely.

Co-Authored-By: Wren <noreply@anthropic.com>
@conoremclaughlin conoremclaughlin changed the title feat(mcp): stateless transport + tool timing diagnostics feat(mcp): stateless transport, timing diagnostics, auth fix, RLS hardening (by Wren) Feb 12, 2026
@conoremclaughlin
conoremclaughlin merged commit ff4d768 into main Feb 12, 2026
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.

1 participant