feat: integrate /x/mcp with user_session_issuer_id#2926
Conversation
There was a problem hiding this comment.
Claude Code Review
This repository is configured for manual code reviews. Comment @claude review to trigger a review and subscribe this PR to future pushes, or @claude review once for a one-time review.
Tip: disable this comment in your organization's Code Review settings.
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
🦋 Changeset detectedLatest commit: 5ffee94 The changes in this PR will be included in the next version bump. This PR includes changesets to release 1 package
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
|
|
||||||||||||||||
|
|
||||||||||||||||
5a5ad4b to
b69c3db
Compare
qstearns
left a comment
There was a problem hiding this comment.
Missed this come through the PR queue earlier, but looks great. Will try to get started with the client tomorrow
|
@qstearns working on rebasing and addressing your comments. 👍 |
b69c3db to
73745da
Compare
73745da to
047e388
Compare
https://linear.app/speakeasy/issue/AGE-2395/integrate-xmcp-with-user-session-issuer-id The mcpServers.create and mcpServers.update management endpoints now accept an optional user_session_issuer_id, and the McpServer result type carries it on read. When set on an mcp_server, /x/mcp requests are issuer-gated: callers without a valid Authorization receive 401 + WWW-Authenticate pointing at /.well-known/oauth-protected-resource/x/mcp/{slug}, and the full Gram-hosted OAuth surface — RFC 7591 dynamic client registration, authorize, IDP callback, consent, token, revoke — is mounted under /x/mcp/{slug}/... against the same JWT-validation machinery /mcp uses. Issued tokens are bound to urn.NewUserSessionIssuer(...) as their audience so they stay portable between toolset-backed and remote-backed mcp_servers under the same issuer. Both well-known metadata routes under /x/mcp now return the issuer-gated metadata shape for any addressed mcp_server with an issuer set, including remote-backed servers (previously 404). The /mcp OAuth surface is reorganised but unchanged in behaviour. A new ResolvedMcpEndpoint type captures the backend-neutral resolved-endpoint shape (project, org, issuer, audience, route base, slug, custom domain, plus the backend-specific ToolsetID or McpServerID) and owns URL construction via RootURL, ProtectedResourceURL, ConsentURL, IDPCallbackURL, and the consolidated AuthorizationServerURLs helper. A new ChallengeEndpointRef replaces LegacyMcpEndpointRef plus the top-level RouteBase and McpServerID fields on AuthnChallengeState, capturing only what's needed to re-resolve a cached in-flight challenge through the right addressing path (toolset-keyed for /mcp, mcp_endpoint-keyed for /x/mcp). The seven issuer-gated /mcp OAuth handlers (HandleRegister, HandleAuthorize, HandleConsent, HandleToken, HandleRevoke, HandleGetProtectedResource, HandleGetAuthorizationServer) each split into a chi handler that resolves a slug through the toolsets path plus a public Serve* post-resolution variant taking *ResolvedMcpEndpoint; the Serve* methods are what xmcp's per-route adapters dispatch to. ApplyIssuerGate is exposed as a public wrapper so xmcp can pre-gate before backend dispatch, and ServeToolsetResolved gains skipIssuerGate plus extraUpstreamTokens parameters so xmcp's toolset-backed branch doesn't double-gate. HandleIDPCallback dispatches resolution on the cached ChallengeEndpointRef.McpServerID so the global callback resumes a challenge on the route surface it was minted under, even when both /mcp and /x/mcp address the same underlying mcp_server. The remote-session leg gets the same surface-awareness: remotesessions.ParentChallenge and the cached RemoteLoginState now thread a RouteBase ("mcp" or "x/mcp"), and HandleRemoteLoginCallback bounces to /<RouteBase>/{slug}/connect instead of a hard-coded /mcp/ path, with empty values falling back to "mcp" so in-flight states minted before the field landed still resume cleanly.
https://linear.app/speakeasy/issue/AGE-2395/integrate-xmcp-with-user-session-issuer-id Follow-up to 047e388. Closes coverage gaps in the /x/mcp surface, renames the runtime tests onto a consistent Test<Surface>_<Variant>_<Scenario> taxonomy, and extracts the issuer-gated seed plumbing into a reusable helper. New coverage: - GET method through ServeMCP + proxy (the SSE-listen leg). - tools/call end-to-end through the full interceptor pipeline. - authorize / consent / token / revoke OAuth route adapters mounted by xmcp.Attach (previously only register was smoke-tested). - Full register -> authorize -> consent -> token -> ServeMCP dance through the chi mux on a public-visibility endpoint. - handleRemoteLoginCallback (zero coverage before) against a live devidptest upstream, asserting the /x/mcp RouteBase threads through to the post-callback redirect. - Custom-domain mismatch resolution: an endpoint scoped to a custom domain must not resolve on the platform domain. - Dangling-issuer-FK race on HandleWellKnownOAuthServerMetadata, symmetric to the existing ServeMCP coverage. - Issuer-gated toolset backend on private visibility. - Issuer-gated well-known metadata on a custom domain and on the toolset backend. Renames TestServeRuntime_* -> TestServeMCP_PublicRemoteBackend_* / TestServeMCP_PrivateRemoteBackend_*, TestServeMCP_IssuerGated_* -> TestServeMCP_IssuerGatedRemoteBackend_*, the register smoke test to TestAttach_OAuthRegisterRoute_MintsClientID, and fixes the truncated _Returns suffix on the IDP-callback mismatch test. Relocates the two issuer-gated well-known tests out of serveruntime_test.go into wellknown_test.go alongside the existing well-known coverage. Adds a test-internal createIssuerGatedMcpServer helper that wires up the full /x/mcp resolution chain plus the upstream-IDP plumbing (user_session_issuer + remote_session_issuer + DCR-registered remote_session_client) in one call, mirroring oauthtest.CreateIssuerGatedToolset. oauthtest.IssuerGatedToolsetOpts grows a RouteBase field so the DCR-registered callback URL matches the surface under test - /mcp callers keep their existing behaviour via the default. seedIssuerGatedRemoteMCPEndpointOnDomain mirrors the existing seedToolsetMCPEndpointOnDomain pattern for the remote-backed case.
047e388 to
5ffee94
Compare
🚀 Preview Environment (PR #2926)Preview URL: https://pr-2926.dev.getgram.ai
Gram Preview Bot |
https://linear.app/speakeasy/issue/AGE-2395/integrate-xmcp-with-user-session-issuer-id
The
mcpServers.createandmcpServers.updatemanagement endpoints now accept an optionaluser_session_issuer_id, and theMcpServerresult type carries it on read. When set on anmcp_server,/x/mcprequests are issuer-gated: callers without a validAuthorizationreceive401+WWW-Authenticatepointing at/.well-known/oauth-protected-resource/x/mcp/{slug}, and the full Gram-hosted OAuth surface — RFC 7591 dynamic client registration, authorize, IDP callback, consent, token, revoke — is mounted under/x/mcp/{slug}/...against the same JWT-validation machinery/mcpuses. Issued tokens are bound tourn.NewUserSessionIssuer(...)as their audience so they stay portable between toolset-backed and remote-backedmcp_serversunder the same issuer. Both well-known metadata routes under/x/mcpnow return the issuer-gated metadata shape for any addressedmcp_serverwith an issuer set, including remote-backed servers (previously 404).The
/mcpOAuth surface is reorganised but unchanged in behaviour. A newResolvedMcpEndpointtype captures the backend-neutral resolved-endpoint shape (project, org, issuer, audience, route base, slug, custom domain, plus the backend-specificToolsetIDorMcpServerID) and owns URL construction viaRootURL,ProtectedResourceURL,ConsentURL,IDPCallbackURL, and the consolidatedAuthorizationServerURLshelper. A newChallengeEndpointRefreplacesLegacyMcpEndpointRefplus the top-levelRouteBaseandMcpServerIDfields onAuthnChallengeState, capturing only what's needed to re-resolve a cached in-flight challenge through the right addressing path (toolset-keyed for/mcp,mcp_endpoint-keyed for/x/mcp). The seven issuer-gated/mcpOAuth handlers (HandleRegister,HandleAuthorize,HandleConsent,HandleToken,HandleRevoke,HandleGetProtectedResource,HandleGetAuthorizationServer) each split into a chi handler that resolves a slug through the toolsets path plus a publicServe*post-resolution variant taking*ResolvedMcpEndpoint; theServe*methods are whatxmcp's per-route adapters dispatch to.ApplyIssuerGateis exposed as a public wrapper soxmcpcan pre-gate before backend dispatch, andServeToolsetResolvedgainsskipIssuerGateplusextraUpstreamTokensparameters soxmcp's toolset-backed branch doesn't double-gate.HandleIDPCallbackdispatches resolution on the cachedChallengeEndpointRef.McpServerIDso the global callback resumes a challenge on the route surface it was minted under, even when both/mcpand/x/mcpaddress the same underlyingmcp_server. The remote-session leg gets the same surface-awareness:remotesessions.ParentChallengeand the cachedRemoteLoginStatenow thread aRouteBase("mcp"or"x/mcp"), andHandleRemoteLoginCallbackbounces to/<RouteBase>/{slug}/connectinstead of a hard-coded/mcp/path, with empty values falling back to"mcp"so in-flight states minted before the field landed still resume cleanly.