Repository navigation
fix(router): route /mcp and root OAuth discovery to the Cloud app #250
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -130,9 +130,18 @@ function isPrimaryFileObserverPath(hostname: string, pathname: string): boolean | |
| // 9728 does the same for protected-resource metadata. These canonical URLs | ||
| // sit outside /cloud, so route them back to the Cloud app that owns the | ||
| // metadata rather than letting the marketing app return a 404. | ||
| // | ||
| // https://agentrelay.com/mcp is the short MCP URL for third-party clients. Its | ||
| // RFC 9728 document is /.well-known/oauth-protected-resource/mcp. The bare | ||
| // root documents are fallbacks for clients that skip path insertion: the | ||
| // protected-resource one describes /mcp, and pre-2025-06 MCP clients look | ||
| // for authorization-server metadata at the MCP server's origin root. | ||
| const CLOUD_OAUTH_WELL_KNOWN_REWRITES: ReadonlyArray<readonly [string, string]> = [ | ||
| ["/.well-known/oauth-authorization-server/cloud", "/cloud/.well-known/oauth-authorization-server"], | ||
| ["/.well-known/openid-configuration/cloud", "/cloud/.well-known/openid-configuration"], | ||
| ["/.well-known/oauth-authorization-server", "/cloud/.well-known/oauth-authorization-server"], | ||
| ["/.well-known/oauth-protected-resource", "/cloud/.well-known/oauth-protected-resource/mcp"], | ||
| ["/.well-known/oauth-protected-resource/mcp", "/cloud/.well-known/oauth-protected-resource/mcp"], | ||
| ]; | ||
| const CLOUD_PROTECTED_RESOURCE_PREFIX = "/.well-known/oauth-protected-resource/cloud/"; | ||
|
|
||
|
|
@@ -146,6 +155,13 @@ export function rewriteCloudOAuthWellKnownPath(pathname: string): string | null | |
| return null; | ||
| } | ||
|
|
||
| // The root MCP endpoint is served by the Cloud app at /cloud/mcp. Unlike the | ||
| // discovery documents it takes every method (Streamable HTTP POSTs JSON-RPC, | ||
| // GETs an SSE stream, DELETEs a session), so it is not limited to GET/HEAD. | ||
| export function rewriteCloudMcpPath(pathname: string): string | null { | ||
| return pathname === "/mcp" || pathname === "/mcp/" ? "/cloud/mcp" : null; | ||
| } | ||
|
|
||
| function isCloudPath(pathname: string): boolean { | ||
| return isPathWithinPrefix(pathname, CLOUD_PATH_PREFIX); | ||
| } | ||
|
|
@@ -748,12 +764,14 @@ const router = { | |
| return openAiAppsChallenge; | ||
| } | ||
|
|
||
| const oauthWellKnownPath = url.hostname === PRIMARY_HOST | ||
| && (request.method === "GET" || request.method === "HEAD") | ||
| ? rewriteCloudOAuthWellKnownPath(url.pathname) | ||
| : null; | ||
| if (oauthWellKnownPath) { | ||
| url.pathname = oauthWellKnownPath; | ||
| const cloudRewritePath = url.hostname !== PRIMARY_HOST | ||
| ? null | ||
| : rewriteCloudMcpPath(url.pathname) | ||
| ?? (request.method === "GET" || request.method === "HEAD" | ||
| ? rewriteCloudOAuthWellKnownPath(url.pathname) | ||
| : null); | ||
|
Comment on lines
+769
to
+772
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟡 MCP streams leave recorder tasks open When an MCP GET returns a live SSE stream, Learn moreThe new public Example: A client subscribes with Recommended fix: Exclude MCP streaming requests from Was this helpful? React with 👍 or 👎 to provide feedback.
Comment on lines
+769
to
+772
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟥 Private MCP conversations enter replay corpus An authenticated Was this helpful? React with 👍 or 👎 to provide feedback. |
||
| if (cloudRewritePath) { | ||
| url.pathname = cloudRewritePath; | ||
| request = new Request(url.toString(), request); | ||
| } | ||
|
|
||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Script executed:
Repository: AgentWorkforce/agentrelay.com
Length of output: 8160
Script executed:
Repository: AgentWorkforce/agentrelay.com
Length of output: 27964
Script executed:
Repository: AgentWorkforce/agentrelay.com
Length of output: 19844
Exclude
/cloud/mcpfrom the recorder.When the recorder is configured and sampling selects the request, the
/mcprewrite makes the request path/cloud/mcpbeforerecorderRequestCloneis created. The path gate excludes only favicon, health, and observer paths, so the recorder can persist JSON-RPC arguments and results. JSON redaction removes selected secret keys, not arbitrary tool data.The recorder also clones and reads every response body with
text(). It has no SSE or content-type bypass. An open SSE response can therefore keep the recorder'swaitUntiltask pending until the stream ends. The 256 KiB limit applies after the body is read, so it does not limit the read duration. The same behavior applies to existing/cloud/*routes; this PR newly places MCP traffic in that existing recording policy.Exclude the rewritten MCP path before cloning:
Suggested fix
&& !relayAgentRoute && !isAgentPageRequestPath(url.pathname) + && url.pathname !== "/cloud/mcp" && !carriesDashboardGrant(url)