Skip to content

Commit cdd51a5

Browse files
authored
Revert "refactor(cloudflare): improve McpAgent constraint abstraction (#517)" (#519)
This reverts commit 84d4fe8.
1 parent d5d99a2 commit cdd51a5

7 files changed

Lines changed: 346 additions & 589 deletions

File tree

‎packages/mcp-cloudflare/package.json‎

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,6 @@
3737
"@types/react-scroll-to-bottom": "^4.2.5",
3838
"@vitejs/plugin-react": "catalog:",
3939
"tailwindcss": "catalog:",
40-
"urlpattern-polyfill": "^10.1.0",
4140
"vite": "catalog:",
4241
"vitest": "catalog:",
4342
"wrangler": "catalog:"

‎packages/mcp-cloudflare/src/server/index.ts‎

Lines changed: 98 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,116 @@
11
import * as Sentry from "@sentry/cloudflare";
22
import OAuthProvider from "@cloudflare/workers-oauth-provider";
3-
import SentryMCP from "./lib/mcp-agent";
3+
import SentryMCP from "./lib/mcp-transport";
44
import app from "./app";
55
import { SCOPES } from "../constants";
66
import type { Env } from "./types";
77
import getSentryConfig from "./sentry.config";
8+
import { isValidSlug } from "./lib/slug-validation";
89

910
// required for Durable Objects
1011
export { SentryMCP };
1112

12-
// SentryMCP handles URLPattern-based constraint extraction from request URLs
13-
// and passes context to Durable Objects via headers for org/project scoping.
13+
// Custom wrapper to preserve path parameters via headers.
14+
//
15+
// ARCHITECTURAL LIMITATION:
16+
// We must pass constraints via headers because the agents library rewrites URLs
17+
// from /mcp/org/project to /streamable-http, losing path information.
18+
//
19+
// IDEAL SOLUTION (not possible with current libraries):
20+
// Each URL path (/mcp/org1/project1) would map to a separate DO instance,
21+
// providing immutable configuration per context.
22+
//
23+
// CURRENT SOLUTION:
24+
// 1. Extract org/project from URL path here
25+
// 2. Pass as headers (X-Sentry-Org-Slug, X-Sentry-Project-Slug)
26+
// 3. DO extracts headers and reconfigures when constraints change
27+
//
28+
// This wrapper ensures:
29+
// - Security: External clients cannot bypass URL-based constraints
30+
// - Validation: Slugs are validated before being passed to DO
31+
// - Compatibility: Works with both SSE and streamable-http transports
32+
const createMcpHandler = (basePath: string, isSSE = false) => {
33+
const handler = isSSE ? SentryMCP.serveSSE("/*") : SentryMCP.serve("/*");
34+
35+
return {
36+
fetch: (request: Request, env: unknown, ctx: ExecutionContext) => {
37+
const url = new URL(request.url);
38+
39+
// Always create new headers to prevent external manipulation
40+
const headers = new Headers(request.headers);
41+
42+
// Remove any externally-set constraint headers for security
43+
// This prevents clients from bypassing URL-based constraints
44+
headers.delete("X-Sentry-Org-Slug");
45+
headers.delete("X-Sentry-Project-Slug");
46+
47+
// Extract org/project from URL path
48+
// NOTE: Extra path segments after project (e.g., /mcp/org/project/extra) are
49+
// intentionally ignored - the MCP handler manages any additional routing
50+
// IMPORTANT: /sse/message and /mcp/message are reserved SSE protocol endpoints
51+
// and must not be interpreted as organization slugs
52+
const pathMatch = url.pathname.match(
53+
/^\/(mcp|sse)(?:\/([a-zA-Z0-9._-]{1,100}))?(?:\/([a-zA-Z0-9._-]{1,100}))?/,
54+
);
55+
56+
// Check if this is a reserved protocol endpoint
57+
const isReservedEndpoint =
58+
url.pathname === "/sse/message" ||
59+
url.pathname.startsWith("/sse/message?") ||
60+
url.pathname === "/mcp/message" ||
61+
url.pathname.startsWith("/mcp/message?");
62+
63+
// Validate and set headers based on URL path (unless it's a reserved endpoint)
64+
if (!isReservedEndpoint && pathMatch?.[2]) {
65+
// Organization slug is present - validate it
66+
if (!isValidSlug(pathMatch[2])) {
67+
return new Response(
68+
JSON.stringify({
69+
error: "invalid_request",
70+
error_description: "Invalid organization slug format",
71+
}),
72+
{
73+
status: 400,
74+
headers: { "Content-Type": "application/json" },
75+
},
76+
);
77+
}
78+
headers.set("X-Sentry-Org-Slug", pathMatch[2]);
79+
80+
// Project slug is optional but must be valid if present
81+
if (pathMatch[3]) {
82+
if (!isValidSlug(pathMatch[3])) {
83+
return new Response(
84+
JSON.stringify({
85+
error: "invalid_request",
86+
error_description: "Invalid project slug format",
87+
}),
88+
{
89+
status: 400,
90+
headers: { "Content-Type": "application/json" },
91+
},
92+
);
93+
}
94+
headers.set("X-Sentry-Project-Slug", pathMatch[3]);
95+
}
96+
}
97+
// If no path params, headers remain deleted (cleared above)
98+
99+
// Create a new request with the sanitized headers
100+
const modifiedRequest = new Request(request, { headers });
101+
return handler.fetch(modifiedRequest, env, ctx);
102+
},
103+
};
104+
};
14105

15106
const oAuthProvider = new OAuthProvider({
16107
apiHandlers: {
17-
// OAuthProvider uses prefix matching, so "/mcp" matches all MCP routes.
18-
// SentryMCP.serve() uses URLPattern to extract org/project constraints from URLs.
108+
// NOTE: OAuthProvider only does prefix matching, not parameterized routes.
109+
// So "/mcp" will match "/mcp", "/mcp/org", "/mcp/org/project" etc.
110+
// We use a custom wrapper to extract path params for /mcp endpoints only.
19111
// SSE endpoints don't support subpath constraints due to protocol limitations.
20112
"/sse": SentryMCP.serveSSE("/sse"),
21-
"/mcp": SentryMCP.serve("/mcp/:org?/:project?"),
113+
"/mcp": createMcpHandler("/mcp", false),
22114
},
23115
// @ts-ignore
24116
defaultHandler: app,

‎packages/mcp-cloudflare/src/server/lib/constraint-utils.test.ts‎

Lines changed: 0 additions & 185 deletions
This file was deleted.

‎packages/mcp-cloudflare/src/server/lib/constraint-utils.ts‎

Lines changed: 0 additions & 76 deletions
This file was deleted.

0 commit comments

Comments
 (0)