Skip to content

Add opaque MCP connection selection contracts - #44

Draft
dsfaccini wants to merge 1 commit into
pydantic:mainfrom
dsfaccini:codex/mcp-connection-contracts
Draft

Add opaque MCP connection selection contracts#44
dsfaccini wants to merge 1 commit into
pydantic:mainfrom
dsfaccini:codex/mcp-connection-contracts

Conversation

@dsfaccini

Copy link
Copy Markdown
Contributor

This pull request was posted by Codex Desktop using gpt-5.6-sol on behalf of David.

Context

#41 asks for per-user authentication to HTTP MCP servers from the web UI. The broader feature should eventually cover OAuth and compatibility credentials without exposing raw MCP credentials, arbitrary headers, or server URLs to the chat request.

MCP 2026-07-28 removes protocol sessions, initialize, Mcp-Session-Id, and the standalone GET stream. Authentication remains per request: a bearer token is sent on every protected HTTP request. See the release announcement, Streamable HTTP specification, and authorization specification.

Pydantic AI cannot yet exercise that wire revision on main. pydantic-ai#6738 is the FastMCP 4 / MCP SDK v2 compatibility work. pydantic-ai#7372 records the independent main-based transport contracts and strict dependency gates.

What this PR implements

This PR establishes the safe browser-to-BFF selection contract that does not depend on FastMCP:

  • /api/configure may advertise non-secret MCP connection summaries (id and name).
  • The chat composer shows a connection selector and sends only selected opaque IDs in mcpConnections.
  • Selection is request-local and is not retained across reloads.
  • The deterministic server validates that the value is a list of strings and rejects IDs absent from its configured allow-list.
  • Tests assert that the browser request contains neither mcpHeaders nor mcpAuth.

All tests in this PR pass. There are no UI xfails because authenticated MCP execution is not blocked only by a dependency: it also requires a production BFF contract and application authentication. An xfail would prematurely encode an unsettled credential API.

Security boundary

The browser must eventually send an opaque connection ID, not a credential, token, endpoint, or arbitrary header map. The application BFF must authenticate the route, resolve the connection against the authenticated principal and tenant, keep credentials server-side, and construct outbound MCP authorization independently for every request. Pydantic AI's UI guidance makes the host route the authentication boundary and treats client-supplied state as untrusted. See the UI security model and message-history trust guidance.

The global ID set in tests/server/server.py is only a deterministic test fixture. It is not a production authorization design and must not be copied as one: membership in a process-wide set would not prevent one authenticated user selecting another user's connection. Production resolution must be principal-scoped and must reject that cross-user case.

Arbitrary user-supplied MCP URLs additionally require SSRF/redirect/egress controls; the current MCP transport does not inherit Pydantic AI's safe download path. MCP documents the relevant client-side SSRF risks.

Follow-up sequence

  1. Land FastMCP 4 / MCP SDK v2 compatibility and replace the strict gates in pydantic-ai#7372 with capture-server wire tests.
  2. Define an application-owned authenticated connection store keyed by principal/tenant, canonical MCP resource, authorization-server issuer, scopes, and credential generation.
  3. Add BFF routes for connection lifecycle and OAuth. Prefer Client ID Metadata Documents; DCR is deprecated for new implementations in the 2026 authorization specification.
  4. Resolve selected opaque IDs per chat request, create request-scoped authenticated toolsets, and keep private caches isolated by authorization context according to the caching specification.
  5. Add an authenticated E2E test with two principals. Assert each remote server receives only its principal's credential on every selected MCP POST, receives no traffic for unselected connections, and receives no Mcp-Session-Id.
  6. Add connection lifecycle UX: disconnected, authenticating, connected, refresh/reconnect, and disconnect/revoke.

MRTR, subscriptions/listen, private/public cache policy, OAuth storage, Basic/API-key/custom-header compatibility, and endpoint SSRF policy are deliberately deferred until their backend owners and public contracts exist.

Verification

  • TypeScript typecheck: pass
  • Focused lint and formatting: pass
  • Build: pass
  • Headless suite: pass
  • Focused MCP Playwright suite: pass

No dependency versions are changed in this PR.

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