Skip to content

Commit 74d900c

Browse files
dcramerclaude
andauthored
refactor(cloudflare): remove Durable Object migration artifacts (#592)
Completes the Durable Objects to stateless MCP handler migration by removing temporary migration code and unnecessary URL rewriting logic. The MCP handler now uses the original URL path directly instead of rewriting to /mcp, simplifying the request flow. Changes: - Remove temporary SentryMCP stub class used for DO migration - Remove URL rewriting logic in mcp-handler (passes original path to createMcpHandler) - Update documentation to reflect stateless architecture - Simplify tests to remove URL rewriting assertions 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude <noreply@anthropic.com>
1 parent ec3fc8b commit 74d900c

11 files changed

Lines changed: 1298 additions & 299 deletions

File tree

‎docs/cloudflare/architecture.md‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -137,8 +137,8 @@ const result = streamText({
137137

138138
- **Workers**: Serverless compute for API routes
139139
- **Pages**: Static asset hosting for React app
140-
- **KV Namespace**: Token storage
141-
- **Durable Objects**: State management (future)
140+
- **KV Namespace**: OAuth token storage
141+
- **AI Binding**: Access to Cloudflare AI models (AutoRAG for docs search)
142142
- **R2**: File storage (future)
143143

144144
### Environment Variables

‎docs/cloudflare/deployment.md‎

Lines changed: 57 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,7 @@ Cloudflare Workers deployment configuration and patterns.
55
## Architecture Overview
66

77
The deployment consists of:
8-
- **Worker**: Main HTTP server with OAuth flow
9-
- **Durable Object**: MCP transport handling WebSocket connections
8+
- **Worker**: Stateless HTTP server with OAuth flow and MCP handler
109
- **KV Storage**: OAuth token storage
1110
- **Static Assets**: React UI for setup instructions
1211

@@ -16,30 +15,30 @@ The deployment consists of:
1615

1716
```jsonc
1817
{
19-
"name": "sentry-mcp-oauth",
18+
"name": "sentry-mcp",
2019
"main": "./src/server/index.ts",
2120
"compatibility_date": "2025-03-21",
2221
"compatibility_flags": [
2322
"nodejs_compat",
24-
"nodejs_compat_populate_process_env"
23+
"nodejs_compat_populate_process_env",
24+
"global_fetch_strictly_public"
2525
],
2626
"keep_vars": true,
27-
27+
2828
// Bindings
29-
"durable_objects": {
30-
"bindings": [{
31-
"name": "SENTRY_MCP",
32-
"class_name": "SentryMCP"
33-
}]
34-
},
3529
"kv_namespaces": [{
36-
"binding": "KV",
37-
"id": "your-kv-namespace-id"
30+
"binding": "OAUTH_KV",
31+
"id": "8dd5e9bafe1945298e2d5ca3b408a553"
3832
}],
39-
40-
// SPA configuration
41-
"site": {
42-
"bucket": "./dist/client"
33+
"ai": {
34+
"binding": "AI"
35+
},
36+
37+
// Static assets configuration
38+
"assets": {
39+
"directory": "./public",
40+
"binding": "ASSETS",
41+
"not_found_handling": "single-page-application"
4342
}
4443
}
4544
```
@@ -51,7 +50,12 @@ Required in production:
5150
SENTRY_CLIENT_ID=your_oauth_app_id
5251
SENTRY_CLIENT_SECRET=your_oauth_app_secret
5352
COOKIE_SECRET=32_char_random_string
54-
SENTRY_HOST=sentry.io # Optional for self-hosted
53+
```
54+
55+
Optional overrides for self-hosted deployments:
56+
```bash
57+
# Leave unset to target the SaaS host
58+
SENTRY_HOST=sentry.example.com # Hostname only (self-hosted only)
5559
```
5660

5761
Development (.dev.vars):
@@ -61,27 +65,36 @@ SENTRY_CLIENT_SECRET=dev_secret
6165
COOKIE_SECRET=dev-cookie-secret
6266
```
6367

64-
## Durable Object Setup
68+
## MCP Handler Setup
6569

66-
The MCP transport runs as a Durable Object:
70+
The MCP handler uses a stateless architecture with AsyncLocalStorage:
6771

6872
```typescript
69-
export class SentryMCP extends DurableObject {
70-
async fetch(request: Request): Promise<Response> {
71-
// Handle WebSocket upgrade
72-
if (request.headers.get("Upgrade") === "websocket") {
73-
const [client, server] = Object.values(new WebSocketPair());
74-
75-
await this.handleWebSocket(server);
76-
return new Response(null, {
77-
status: 101,
78-
webSocket: client
79-
});
80-
}
81-
82-
return new Response("Not found", { status: 404 });
83-
}
84-
}
73+
import { experimental_createMcpHandler as createMcpHandler } from "agents/mcp";
74+
import { serverContextStorage } from "@sentry/mcp-server/internal/context-storage";
75+
76+
const mcpHandler: ExportedHandler<Env> = {
77+
async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise<Response> {
78+
// Extract auth props from ExecutionContext (set by OAuth provider)
79+
const oauthCtx = ctx as OAuthExecutionContext;
80+
81+
// Build complete ServerContext from OAuth props + constraints
82+
const serverContext: ServerContext = {
83+
userId: oauthCtx.props.userId,
84+
clientId: oauthCtx.props.clientId,
85+
accessToken: oauthCtx.props.accessToken,
86+
grantedScopes: expandedScopes,
87+
constraints: verification.constraints,
88+
sentryHost,
89+
mcpUrl: oauthCtx.props.mcpUrl,
90+
};
91+
92+
// Run MCP handler within ServerContext (AsyncLocalStorage)
93+
return serverContextStorage.run(serverContext, () => {
94+
return createMcpHandler(server, { route: "/mcp" })(request, env, ctx);
95+
});
96+
},
97+
};
8598
```
8699

87100
## OAuth Provider Setup
@@ -212,19 +225,16 @@ Tests validate (using Vitest):
212225

213226
First-time setup:
214227
```bash
215-
# Create KV namespace
216-
npx wrangler kv:namespace create KV
217-
218-
# Create Durable Object namespace
219-
npx wrangler durable-objects namespace create SENTRY_MCP
228+
# Create KV namespace for OAuth token storage
229+
npx wrangler kv:namespace create OAUTH_KV
220230

221-
# Update wrangler.jsonc with IDs
231+
# Update wrangler.jsonc with the namespace ID
222232
```
223233

224234
## Multi-Region Considerations
225235

226236
Cloudflare Workers run globally, but consider:
227-
- Durable Objects have a home region
237+
- Workers are stateless and edge-deployed
228238
- KV is eventually consistent globally
229239
- Use regional hints for performance
230240

@@ -275,7 +285,7 @@ export default {
275285
Monitor via Cloudflare dashboard:
276286
- Request rates
277287
- Error rates
278-
- Durable Object usage
288+
- CPU time and memory usage
279289
- KV operations
280290

281291
## Troubleshooting
@@ -286,9 +296,9 @@ Monitor via Cloudflare dashboard:
286296
- Ensure callback URL matches Sentry app config
287297
- Check protocol (http vs https)
288298

289-
2. **Durable Object not found**
290-
- Verify namespace binding in wrangler.jsonc
291-
- Check class export in main file
299+
2. **AsyncLocalStorage context missing**
300+
- Verify serverContextStorage.run() wraps MCP handler
301+
- Check ExecutionContext.props contains OAuth data
292302

293303
3. **Environment variables missing**
294304
- Use `wrangler secret put` for production

‎docs/cloudflare/oauth-architecture.md‎

Lines changed: 44 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ The system uses a dual-token approach:
3232
sequenceDiagram
3333
participant Client as MCP Client (Cursor)
3434
participant MCPOAuth as MCP OAuth Provider<br/>(Our Server)
35-
participant MCP as MCP Server<br/>(Durable Object)
35+
participant MCP as MCP Server<br/>(Stateless Handler)
3636
participant SentryOAuth as Sentry OAuth Provider<br/>(sentry.io)
3737
participant SentryAPI as Sentry API
3838
participant User as User
@@ -167,14 +167,13 @@ const oAuthProvider = new OAuthProvider({
167167
});
168168
```
169169

170-
### 2. API Handlers
170+
### 2. API Handler
171171

172-
The `apiHandlers` are protected endpoints that require valid OAuth tokens:
172+
The `apiHandler` is a protected endpoint that requires valid OAuth tokens:
173173

174-
- `/mcp/*` - MCP protocol endpoints
175-
- `/sse/*` - Server-sent events for MCP
174+
- `/mcp` - MCP protocol endpoint (HTTP transport)
176175

177-
These handlers receive:
176+
The handler receives:
178177
- `request`: The incoming request
179178
- `env`: Cloudflare environment bindings
180179
- `ctx`: Execution context with `ctx.props` containing decrypted user data
@@ -202,39 +201,44 @@ interface WorkerProps {
202201
The MCP server needs to support URL-based constraints like `/mcp/sentry/javascript` to limit agent access to specific organizations/projects. However:
203202

204203
1. OAuth Provider only does prefix matching (`/mcp` matches `/mcp/*`)
205-
2. The agents library rewrites URLs to `/streamable-http` before reaching the Durable Object
206-
3. URL path parameters are lost in this rewrite
204+
2. The MCP handler needs to extract constraints from URL paths
205+
3. URL path parameters must be preserved through the OAuth middleware
207206

208207
#### The Solution
209208

210209
We use HTTP headers to preserve constraints through the URL rewriting:
211210

212211
```typescript
213-
const createMcpHandler = (basePath: string, isSSE = false) => {
214-
const handler = isSSE ? SentryMCP.serveSSE("/*") : SentryMCP.serve("/*");
215-
216-
return {
217-
fetch: (request: Request, env: unknown, ctx: ExecutionContext) => {
218-
const url = new URL(request.url);
219-
220-
// Extract constraints from URL
221-
const pathMatch = url.pathname.match(
222-
/^\/(mcp|sse)(?:\/([a-z0-9._-]+))?(?:\/([a-z0-9._-]+))?/i
223-
);
224-
225-
// Pass constraints via headers (preserved through URL rewriting)
226-
const headers = new Headers(request.headers);
227-
if (pathMatch?.[2]) {
228-
headers.set("X-Sentry-Org-Slug", pathMatch[2]);
229-
}
230-
if (pathMatch?.[3]) {
231-
headers.set("X-Sentry-Project-Slug", pathMatch[3]);
232-
}
233-
234-
const modifiedRequest = new Request(request, { headers });
235-
return handler.fetch(modifiedRequest, env, ctx);
236-
},
237-
};
212+
// The MCP handler extracts constraints from URL path segments
213+
// Example URLs:
214+
// /mcp - No constraints
215+
// /mcp/sentry - Organization constraint
216+
// /mcp/sentry/javascript - Organization + project constraints
217+
218+
const mcpHandler: ExportedHandler<Env> = {
219+
async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise<Response> {
220+
// Extract auth props from ExecutionContext (set by OAuth provider)
221+
const oauthCtx = ctx as OAuthExecutionContext;
222+
223+
// Parse constraints from URL path
224+
const url = new URL(request.url);
225+
const pathSegments = url.pathname.split('/').filter(Boolean);
226+
const constraints = {
227+
organizationSlug: pathSegments[1] || null,
228+
projectSlug: pathSegments[2] || null,
229+
};
230+
231+
// Build complete ServerContext
232+
const serverContext: ServerContext = {
233+
...oauthCtx.props,
234+
constraints,
235+
};
236+
237+
// Run MCP handler within ServerContext (AsyncLocalStorage)
238+
return serverContextStorage.run(serverContext, () => {
239+
return createMcpHandler(server, { route: "/mcp" })(request, env, ctx);
240+
});
241+
},
238242
};
239243
```
240244

@@ -375,19 +379,19 @@ Note: These describe the MCP OAuth server, not Sentry's OAuth endpoints.
375379

376380
## Integration Between MCP OAuth and MCP Server
377381

378-
The MCP Server (Durable Object `SentryMCP`) receives:
382+
The MCP Server (stateless handler) receives context via AsyncLocalStorage:
379383

380-
1. **Props via constructor**: Decrypted data from MCP token (includes Sentry tokens)
381-
2. **Constraints via headers**: Organization/project limits from URL path
382-
3. **Both stored**: In Durable Object storage for session persistence
384+
1. **Props via ExecutionContext**: Decrypted data from MCP token (includes Sentry tokens)
385+
2. **Constraints from URL**: Organization/project limits parsed from URL path
386+
3. **Context storage**: AsyncLocalStorage provides per-request isolation
383387

384-
The MCP Server then uses the Sentry access token from props to make Sentry API calls.
388+
The MCP Server then uses the Sentry access token from context to make Sentry API calls.
385389

386390
## Limitations
387391

388392
1. **No direct Hono integration**: OAuth Provider expects specific handler signatures
389-
2. **URL rewriting**: Requires header-based constraint passing
390-
3. **Props architecture mismatch**: OAuth passes props per-request, agents library expects them in constructor
393+
2. **Constraint extraction**: Must parse URL segments to extract organization/project constraints
394+
3. **AsyncLocalStorage dependency**: Requires Node.js compatibility mode in Cloudflare Workers
391395

392396
## Why Use Two OAuth Systems?
393397

‎docs/llms/document-scopes.mdc‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,7 @@ Defines the specific purpose and content for each documentation file.
110110
**Must Include**:
111111
- Wrangler configuration
112112
- Environment variables
113-
- Durable Objects setup
113+
- MCP handler setup
114114
- OAuth flow
115115
- Deployment commands
116116

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

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,6 @@ import type { Env } from "./types";
66
import getSentryConfig from "./sentry.config";
77
import { tokenExchangeCallback } from "./oauth";
88
import sentryMcpHandler from "./lib/mcp-handler";
9-
import { SentryMCP as SentryMCPStub } from "./lib/mcp-agent-stub";
10-
11-
// SentryMCP stub exported ONLY for Durable Object migration purposes.
12-
// This will be removed after the deleted_classes migration completes.
139

1410
// Public metadata endpoints that should be accessible from any origin
1511
const PUBLIC_METADATA_PATHS = [
@@ -75,8 +71,3 @@ export default Sentry.withSentry(
7571
getSentryConfig,
7672
corsWrappedOAuthProvider,
7773
) satisfies ExportedHandler<Env>;
78-
79-
// Export SentryMCP Durable Object class for migration
80-
// TEMPORARY: This export is required for Cloudflare to apply the deleted_classes migration.
81-
// Once all Durable Object instances are deleted, this export should be removed.
82-
export { SentryMCPStub as SentryMCP };

‎packages/mcp-cloudflare/src/server/lib/mcp-agent-stub.ts‎

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

0 commit comments

Comments
 (0)