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
3 changes: 0 additions & 3 deletions packages/mcp-cloudflare/src/server/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,9 +85,6 @@ const wrappedOAuthProvider = {

const response = await oAuthProvider.fetch(request, env, ctx);

// Flush buffered logs before Worker terminates
ctx.waitUntil(Sentry.flush(2000));

// Add CORS headers to public metadata endpoints
if (isPublicMetadataEndpoint(url.pathname)) {
return addCorsHeaders(response);
Expand Down
5 changes: 0 additions & 5 deletions packages/mcp-cloudflare/src/server/lib/mcp-handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@ import type { ServerContext } from "@sentry/mcp-core/types";
import type { Env } from "../types";
import { verifyConstraintsAccess } from "./constraint-utils";
import type { ExportedHandler } from "@cloudflare/workers-types";
import * as Sentry from "@sentry/cloudflare";

/**
* ExecutionContext with OAuth props injected by the OAuth provider.
Expand Down Expand Up @@ -169,10 +168,6 @@ const mcpHandler: ExportedHandler<Env> = {
const server = buildServer({
context: serverContext,
agentMode: isAgentMode,
onToolComplete: () => {
// Flush Sentry events after tool execution
ctx.waitUntil(Sentry.flush(2000));
},
});

// Run MCP handler - context already captured in closures
Expand Down
163 changes: 77 additions & 86 deletions packages/mcp-core/src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,12 +82,10 @@ import {
*/
export function buildServer({
context,
onToolComplete,
agentMode = false,
tools: customTools,
}: {
context: ServerContext;
onToolComplete?: () => void;
agentMode?: boolean;
tools?: Record<string, ToolConfig<any>>;
}): McpServer {
Expand All @@ -99,7 +97,6 @@ export function buildServer({
configureServer({
server,
context,
onToolComplete,
agentMode,
tools: customTools,
});
Expand All @@ -119,13 +116,11 @@ export function buildServer({
function configureServer({
server,
context,
onToolComplete,
agentMode = false,
tools: customTools,
}: {
server: McpServer;
context: ServerContext;
onToolComplete?: () => void;
agentMode?: boolean;
tools?: Record<string, ToolConfig<any>>;
}) {
Expand Down Expand Up @@ -254,104 +249,100 @@ function configureServer({
params: any,
extra: RequestHandlerExtra<ServerRequest, ServerNotification>,
) => {
try {
// Get active span (mcp.server span) and attach more attributes to it
const activeSpan = getActiveSpan();

if (activeSpan) {
if (context.constraints.organizationSlug) {
activeSpan.setAttribute(
"sentry-mcp.constraint-organization",
context.constraints.organizationSlug,
);
}
if (context.constraints.projectSlug) {
activeSpan.setAttribute(
"sentry-mcp.constraint-project",
context.constraints.projectSlug,
);
}
}
// Get active span (mcp.server span) and attach more attributes to it
const activeSpan = getActiveSpan();

if (context.userId) {
setUser({
id: context.userId,
});
if (activeSpan) {
if (context.constraints.organizationSlug) {
activeSpan.setAttribute(
"sentry-mcp.constraint-organization",
context.constraints.organizationSlug,
);
}
if (context.clientId) {
setTag("client.id", context.clientId);
if (context.constraints.projectSlug) {
activeSpan.setAttribute(
"sentry-mcp.constraint-project",
context.constraints.projectSlug,
);
}
}

try {
// Apply constraints as parameters, handling aliases (e.g., projectSlug → projectSlugOrId)
const applicableConstraints = getConstraintParametersToInject(
context.constraints,
tool.inputSchema,
);
if (context.userId) {
setUser({
id: context.userId,
});
}
if (context.clientId) {
setTag("client.id", context.clientId);
}

const paramsWithConstraints = {
...params,
...applicableConstraints,
};
try {
// Apply constraints as parameters, handling aliases (e.g., projectSlug → projectSlugOrId)
const applicableConstraints = getConstraintParametersToInject(
context.constraints,
tool.inputSchema,
);

const output = await tool.handler(paramsWithConstraints, context);
const paramsWithConstraints = {
...params,
...applicableConstraints,
};

if (activeSpan) {
activeSpan.setStatus({
code: 1, // ok
});
}
const output = await tool.handler(paramsWithConstraints, context);

// if the tool returns a string, assume it's a message
if (typeof output === "string") {
return {
content: [
{
type: "text" as const,
text: output,
},
],
};
}
// if the tool returns a list, assume it's a content list
if (Array.isArray(output)) {
return {
content: output,
};
}
throw new Error(`Invalid tool output: ${output}`);
} catch (error) {
if (activeSpan) {
activeSpan.setStatus({
code: 2, // error
});
activeSpan.recordException(error);
}
if (activeSpan) {
activeSpan.setStatus({
code: 1, // ok
});
}

// CRITICAL: Tool errors MUST be returned as formatted text responses,
// NOT thrown as exceptions. This ensures consistent error handling
// and prevents the MCP client from receiving raw error objects.
//
// The logAndFormatError function provides user-friendly error messages
// with appropriate formatting for different error types:
// - UserInputError: Clear guidance for fixing input problems
// - ConfigurationError: Clear guidance for fixing configuration issues
// - ApiError: HTTP status context with helpful messaging
// - System errors: Sentry event IDs for debugging
//
// DO NOT change this to throw error - it breaks error handling!
// if the tool returns a string, assume it's a message
if (typeof output === "string") {
return {
content: [
{
type: "text" as const,
text: await formatErrorForUser(error),
text: output,
},
],
isError: true,
};
}
} finally {
onToolComplete?.();
// if the tool returns a list, assume it's a content list
if (Array.isArray(output)) {
return {
content: output,
};
}
throw new Error(`Invalid tool output: ${output}`);
} catch (error) {
if (activeSpan) {
activeSpan.setStatus({
code: 2, // error
});
activeSpan.recordException(error);
}

// CRITICAL: Tool errors MUST be returned as formatted text responses,
// NOT thrown as exceptions. This ensures consistent error handling
// and prevents the MCP client from receiving raw error objects.
//
// The logAndFormatError function provides user-friendly error messages
// with appropriate formatting for different error types:
// - UserInputError: Clear guidance for fixing input problems
// - ConfigurationError: Clear guidance for fixing configuration issues
// - ApiError: HTTP status context with helpful messaging
// - System errors: Sentry event IDs for debugging
//
// DO NOT change this to throw error - it breaks error handling!
return {
content: [
{
type: "text" as const,
text: await formatErrorForUser(error),
},
],
isError: true,
};
}
},
);
Expand Down
Loading