Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 14 additions & 5 deletions apps/cloud/src/mcp/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,12 @@ const jwks = createCachedRemoteJWKSet(new URL(`${AUTHKIT_DOMAIN}/oauth2/jwks`));

const MCP_PATH = "/mcp";
export const PROTECTED_RESOURCE_METADATA_PATH = "/.well-known/oauth-protected-resource/mcp";
export const PROTECTED_RESOURCE_METADATA_URL = `${RESOURCE_ORIGIN}${PROTECTED_RESOURCE_METADATA_PATH}`;
// The plain `/mcp` challenge names this alias of the bare document instead of
// the RFC 9728 path-derived one. Executor v2 will own `executor.sh/mcp`'s root
// discovery documents; a v1 client whose token expired follows this pointer
// back to v1's authorization server instead of discovering v2's. The leading
// underscore keeps it out of the org slug grammar and the `org_` id namespace.
export const V1_PROTECTED_RESOURCE_METADATA_PATH = "/.well-known/oauth-protected-resource/_v1/mcp";
export const RESOURCE_URL = `${RESOURCE_ORIGIN}${MCP_PATH}`;
const TOOLKIT_SEGMENT = "/toolkits/";

Expand Down Expand Up @@ -94,15 +99,19 @@ export const resourceUrlFor = (
? `${RESOURCE_ORIGIN}/${organizationSelector}${toolkitMcpPath(toolkitSlug)}`
: `${RESOURCE_ORIGIN}${toolkitMcpPath(toolkitSlug)}`;

/** The protected-resource-metadata URL for an org selector, or the bare one. */
/** The protected-resource-metadata URL a challenge names: the org-scoped or
* toolkit document, or the v1 alias of the bare one for plain `/mcp`. */
export const protectedResourceMetadataUrlFor = (
organizationSelector: string | null,
toolkitSlug: string | null = null,
): string => {
const toolkitSuffix = toolkitSlug ? `/toolkits/${toolkitSlug}` : "";
return organizationSelector
? `${RESOURCE_ORIGIN}/.well-known/oauth-protected-resource/${organizationSelector}/mcp${toolkitSuffix}`
: `${PROTECTED_RESOURCE_METADATA_URL}${toolkitSuffix}`;
if (organizationSelector) {
return `${RESOURCE_ORIGIN}/.well-known/oauth-protected-resource/${organizationSelector}/mcp${toolkitSuffix}`;
}
return toolkitSlug
? `${RESOURCE_ORIGIN}${PROTECTED_RESOURCE_METADATA_PATH}${toolkitSuffix}`
: `${RESOURCE_ORIGIN}${V1_PROTECTED_RESOURCE_METADATA_PATH}`;
};

type McpUnauthorizedReason = "missing_bearer" | "invalid_token";
Expand Down
16 changes: 13 additions & 3 deletions apps/cloud/src/mcp/mount.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,11 @@

import { isValidOrgSlug } from "@executor-js/api";

import { MCP_ORGANIZATION_HEADER, PROTECTED_RESOURCE_METADATA_PATH } from "./auth";
import {
MCP_ORGANIZATION_HEADER,
PROTECTED_RESOURCE_METADATA_PATH,
V1_PROTECTED_RESOURCE_METADATA_PATH,
} from "./auth";

const MCP_PATH = "/mcp";
const AUTHORIZATION_SERVER_METADATA_PATH = "/.well-known/oauth-authorization-server";
Expand Down Expand Up @@ -71,15 +75,21 @@ const matchMcpSuffix = (segments: readonly string[]): MatchedMcpSuffix | undefin
* "is this an MCP path?" predicate — under the envelope `HttpRouter.toWebHandler`
* 404s unknown paths rather than returning `null`, so this gate decides whether
* to even invoke the envelope handler (null -> fall through to Start routing).
* Recognizes the bare `/mcp` + the two discovery docs AND their org-scoped
* variants (`/org_xxx/mcp`, `/.well-known/oauth-protected-resource/org_xxx/mcp`);
* Recognizes the bare `/mcp` + the two discovery docs (and the v1 alias of the
* protected-resource doc) AND their org-scoped variants (`/org_xxx/mcp`,
* `/.well-known/oauth-protected-resource/org_xxx/mcp`);
* only `org_…`-shaped segments are claimed. `prepareMcpOrgScope` then rewrites an
* org-scoped path to the bare path the shared envelope actually routes.
*/
export const classifyMcpPath = (pathname: string): McpRoute => {
if (pathname === AUTHORIZATION_SERVER_METADATA_PATH) {
return { kind: "oauth-authorization-server", organizationId: null };
}
// The v1 alias of the bare document, which plain `/mcp` challenges name. Its
// `_v1` segment is never an org selector, so it is matched before them.
if (pathname === V1_PROTECTED_RESOURCE_METADATA_PATH) {
return { kind: "oauth-protected-resource", organizationId: null };
}
const segments = pathname.split("/").filter((segment) => segment.length > 0);

// Protected-resource metadata: `${prefix}/mcp` or `${prefix}/<org>/mcp`. The
Expand Down
Loading