Skip to content
Merged
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
1 change: 1 addition & 0 deletions packages/mcp-cloudflare/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@
"@ai-sdk/react": "catalog:",
"@cloudflare/workers-oauth-provider": "catalog:",
"@modelcontextprotocol/sdk": "catalog:",
"@modelcontextprotocol/server": "catalog:",
"@radix-ui/react-accordion": "catalog:",
"@radix-ui/react-slot": "catalog:",
"@sentry/cloudflare": "catalog:",
Expand Down
34 changes: 18 additions & 16 deletions packages/mcp-cloudflare/src/server/lib/mcp-handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,12 @@
*/

import type { ExportedHandler } from "@cloudflare/workers-types";
import { CfWorkerJsonSchemaValidator } from "@modelcontextprotocol/sdk/validation/cfworker";
import * as Sentry from "@sentry/cloudflare";
import { buildServer } from "@sentry/mcp-core/server";
import {
ACTIVE_SKILLS,
parseSkills,
type Skill,
parseSkills,
} from "@sentry/mcp-core/skills";
import { logWarn } from "@sentry/mcp-core/telem/logging";
import type { ServerContext } from "@sentry/mcp-core/types";
Expand Down Expand Up @@ -450,20 +449,23 @@ async function handleAuthenticatedMcpRequest(
auth.kind === "oauth" ? auth.onUpstreamUnauthorized : undefined,
};

// Create and configure MCP server with tools filtered by context
// Context is captured in tool handler closures during buildServer()
// Use CfWorkerJsonSchemaValidator for Cloudflare Workers (ajv is not compatible with workerd)
const server = buildServer({
context: serverContext,
agentMode: isAgentMode,
experimentalMode: isExperimentalMode,
jsonSchemaValidator: new CfWorkerJsonSchemaValidator(),
});

// Run MCP handler - context already captured in closures
return createMcpHandler(server, {
route: url.pathname,
})(request, env, ctx);
// The modern handler requires a factory and creates a fresh SDK v2 server
// for every request. Context remains request-scoped through tool closures.
return createMcpHandler(
() =>
buildServer({
context: serverContext,
agentMode: isAgentMode,
experimentalMode: isExperimentalMode,
sdkVersion: "v2",
}),
{
route: url.pathname,
// The Agents handler rejects browser Origins by default except localhost.
// Permit only the hostname serving this authenticated endpoint.
allowedOriginHostnames: [url.hostname],
},
)(request, env, ctx);
}

/**
Expand Down
9 changes: 5 additions & 4 deletions packages/mcp-cloudflare/src/test-utils/ajv-stub.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
/**
* Stub for ajv module to bypass CJS require() issues in workerd runtime.
*
* The MCP SDK imports ajv at module level (even when using CfWorkerJsonSchemaValidator).
* ajv uses CJS require() for JSON files which fails in workerd.
* The legacy MCP SDK imports ajv at module level even though the hosted v2 path
* uses the SDK's workerd-compatible default validator. ajv uses CJS require()
* for JSON files which fails in workerd.
* See: https://github.com/cloudflare/workers-sdk/issues/9822
*
* This stub provides the minimal API surface that the SDK imports,
* but is never actually used since we use CfWorkerJsonSchemaValidator.
* This stub provides the minimal API surface required by that legacy import;
* hosted MCP requests do not invoke it.
*/

export class Ajv {
Expand Down
9 changes: 5 additions & 4 deletions packages/mcp-cloudflare/vitest.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,12 +33,13 @@ export default defineConfig({
/**
* Workaround for ajv CJS compatibility in workerd runtime.
*
* The MCP SDK imports ajv at module level (even when using CfWorkerJsonSchemaValidator).
* ajv uses CJS require() for JSON files which fails in workerd.
* The legacy MCP SDK imports ajv at module level even though hosted requests use
* the v2 SDK's workerd-compatible default validator. ajv uses CJS require() for
* JSON files which fails in workerd.
* See: https://github.com/cloudflare/workers-sdk/issues/9822
*
* This is TEST-ONLY - production uses CfWorkerJsonSchemaValidator which
* doesn't actually invoke ajv, but the import still triggers the CJS issue.
* This is TEST-ONLY: hosted MCP requests do not invoke ajv, but importing the
* legacy SDK still triggers the CJS issue.
*/
resolve: {
alias: {
Expand Down
1 change: 1 addition & 0 deletions packages/mcp-core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,7 @@
"@logtape/logtape": "^1.1.1",
"@logtape/sentry": "^1.1.1",
"@modelcontextprotocol/sdk": "catalog:",
"@modelcontextprotocol/server": "catalog:",
"@sentry/core": "catalog:",
"ai": "catalog:",
"dotenv": "catalog:",
Expand Down
37 changes: 35 additions & 2 deletions packages/mcp-core/src/server.test.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { InMemoryTransport } from "@modelcontextprotocol/sdk/inMemory.js";
import { McpServer as ModernMcpServer } from "@modelcontextprotocol/server";
import { type Span, setUser, startSpan } from "@sentry/core";
import { mswServer } from "@sentry/mcp-server-mocks";
import { http, HttpResponse } from "msw";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { mswServer } from "@sentry/mcp-server-mocks";
import { z } from "zod";
import { buildServer } from "./server";
import { structuredResult } from "./internal/tool-helpers/results";
import { buildServer } from "./server";
import type { Skill } from "./skills";
import {
getGeneratedTextFromStructuredContent,
Expand Down Expand Up @@ -152,6 +153,38 @@ describe("buildServer", () => {
...options,
});

it("registers and executes tools with the SDK v2 server", async () => {
const server = buildServer({
context: baseContext,
tools: {
v2_tool: createMockTool("v2_tool", {
inputSchema: { value: z.string() },
handler: async ({ value }) => `v2:${value}`,
}),
},
sdkVersion: "v2",
});

expect(server).toBeInstanceOf(ModernMcpServer);
const registeredTools = (
server as unknown as {
_registeredTools: Record<
string,
{
handler: (params: Record<string, unknown>) => Promise<unknown>;
}
>;
}
)._registeredTools;

expect(registeredTools.v2_tool).toBeDefined();
await expect(
registeredTools.v2_tool?.handler({ value: "ok" }),
).resolves.toMatchObject({
content: [{ type: "text", text: "v2:ok" }],
});
});

describe("telemetry context", () => {
it("generates compatibility text for structured-only tool output", async () => {
const server = buildServer({
Expand Down
Loading
Loading