/** * MCP Serve Endpoint - Implements MCP protocol for workflow servers using SDK types. */ import { type CallToolResult, ErrorCode, type InitializeResult, isJSONRPCNotification, isJSONRPCRequest, type JSONRPCError, type JSONRPCMessage, type JSONRPCResultResponse, LATEST_PROTOCOL_VERSION, type ListToolsResult, type RequestId, SUPPORTED_PROTOCOL_VERSIONS, type Tool, } from '@modelcontextprotocol/sdk/types.js' import { db } from '@sim/db' import { workflow, workflowMcpServer, workflowMcpTool, workspace } from '@sim/db/schema' import { createLogger } from '@sim/logger' import { and, asc, eq, gt, isNull, sql } from 'drizzle-orm' import { type NextRequest, NextResponse } from 'next/server' import { mcpJsonRpcNotificationSchema, mcpJsonRpcRequestSchema, mcpServeRouteParamsSchema, mcpToolCallParamsSchema, } from '@/lib/api/contracts/mcp' import { AuthType, checkHybridAuth } from '@/lib/auth/hybrid' import { generateInternalToken } from '@/lib/auth/internal' import { API_EXECUTION_REQUIRES_PAID_PLAN_MESSAGE, isWorkspaceApiExecutionEntitled, } from '@/lib/billing/core/api-access' import { getMaxExecutionTimeout } from '@/lib/core/execution-limits' import { assertContentLengthWithinLimit, assertKnownSizeWithinLimit, isPayloadSizeLimitError, readResponseTextWithLimit, readStreamToBufferWithLimit, } from '@/lib/core/utils/stream-limits' import { getInternalApiBaseUrl } from '@/lib/core/utils/urls' import { withRouteHandler } from '@/lib/core/utils/with-route-handler' import { SIM_VIA_HEADER } from '@/lib/execution/call-chain' import { MAX_MCP_PARAMETER_SCHEMA_BYTES, MAX_MCP_TOOLS_LIST_RESPONSE_BYTES, MAX_MCP_TOOLS_PER_SERVER, MAX_MCP_WORKFLOW_RESPONSE_BYTES, MCP_TOOL_BRIDGE_ACTOR_HEADER, MCP_TOOL_BRIDGE_HEADER, } from '@/lib/mcp/constants' import { getMeaningfulWorkflowDescription } from '@/lib/mcp/workflow-tool-schema' import { getUserEntityPermissions } from '@/lib/workspaces/permissions/utils' const logger = createLogger('WorkflowMcpServeAPI') const MAX_MCP_SERVE_BODY_BYTES = 10 * 1024 * 1024 const MAX_MCP_WORKFLOW_REQUEST_BYTES = 10 * 1024 * 1024 const MAX_MCP_TOOL_RESULT_TEXT_BYTES = 10 * 1024 * 1024 const MAX_MCP_TOOLS_LIST_COUNT = MAX_MCP_TOOLS_PER_SERVER const MAX_MCP_TOOLS_LIST_SCHEMA_BYTES = MAX_MCP_PARAMETER_SCHEMA_BYTES const MB = 1024 * 1024 function negotiateProtocolVersion(rpcParams: unknown): string { const requested = rpcParams && typeof rpcParams === 'object' && 'protocolVersion' in rpcParams ? (rpcParams as { protocolVersion?: unknown }).protocolVersion : undefined if (typeof requested === 'string' && SUPPORTED_PROTOCOL_VERSIONS.includes(requested)) { return requested } return LATEST_PROTOCOL_VERSION } export const dynamic = 'force-dynamic' interface RouteParams { serverId: string } interface ExecuteAuthContext { userId: string useAuthenticatedUserAsActor: boolean } function createResponse(id: RequestId, result: unknown): JSONRPCResultResponse { return { jsonrpc: '2.0', id, result: result as JSONRPCResultResponse['result'], } } function createError( id: RequestId, code: ErrorCode | number, message: string, data?: unknown ): JSONRPCError { return { jsonrpc: '2.0', id, error: { code, message, ...(data !== undefined && { data }) }, } } function clientCancelledJsonRpcResponse(id: RequestId): NextResponse { return NextResponse.json( createError(id, ErrorCode.ConnectionClosed, 'Client cancelled request'), { status: 499, } ) } function callerAbortedJsonRpcResponse( id: RequestId, abortSignal?: ManagedAbortSignal | null ): NextResponse | null { return abortSignal?.isCallerAborted() ? clientCancelledJsonRpcResponse(id) : null } function limitMessage(label: string, maxBytes: number): string { return `${label} exceeds maximum size of ${Math.round(maxBytes / MB)}MB` } async function readJsonRpcBody(request: NextRequest): Promise { assertContentLengthWithinLimit(request.headers, MAX_MCP_SERVE_BODY_BYTES, 'MCP request body') const buffer = await readStreamToBufferWithLimit(request.body, { maxBytes: MAX_MCP_SERVE_BODY_BYTES, label: 'MCP request body', signal: request.signal, }) return JSON.parse(buffer.toString('utf-8')) } interface ManagedAbortSignal { signal: AbortSignal cleanup: () => void isCallerAborted: () => boolean isTimedOut: () => boolean } function createManagedAbortSignal( parentSignal: AbortSignal, timeoutMs: number ): ManagedAbortSignal { const controller = new AbortController() let callerAborted = false let timedOut = false const timeoutId = setTimeout(() => { timedOut = true controller.abort(new Error(`MCP workflow execution timed out after ${timeoutMs}ms`)) }, timeoutMs) const abortFromParent = () => { callerAborted = true controller.abort(parentSignal.reason ?? new Error('MCP client disconnected')) } if (parentSignal.aborted) { abortFromParent() } else { parentSignal.addEventListener('abort', abortFromParent, { once: true }) } return { signal: controller.signal, cleanup: () => { clearTimeout(timeoutId) parentSignal.removeEventListener('abort', abortFromParent) }, isCallerAborted: () => callerAborted || parentSignal.aborted, isTimedOut: () => timedOut, } } function serializeToolText(value: unknown): string { const text = JSON.stringify(value, null, 2) ?? 'null' assertKnownSizeWithinLimit( Buffer.byteLength(text, 'utf-8'), MAX_MCP_TOOL_RESULT_TEXT_BYTES, 'MCP tool result text' ) return text } function createJsonRpcResponseWithLimit( id: RequestId, result: unknown, maxBytes: number, label: string ): NextResponse { const responseBody = createResponse(id, result) const responseText = JSON.stringify(responseBody) assertKnownSizeWithinLimit(Buffer.byteLength(responseText, 'utf-8'), maxBytes, label) return new NextResponse(responseText, { status: 200, headers: { 'Content-Type': 'application/json' }, }) } function toToolInputSchema(schema: unknown): Partial { if (!schema || typeof schema !== 'object' || Array.isArray(schema)) return {} const candidate = schema as Record const properties = candidate.properties && typeof candidate.properties === 'object' && !Array.isArray(candidate.properties) ? (candidate.properties as Tool['inputSchema']['properties']) : {} const required = Array.isArray(candidate.required) ? candidate.required.filter((entry): entry is string => typeof entry === 'string') : undefined return { properties, ...(required && required.length > 0 && { required }), } } function isJsonObject(value: unknown): value is Record { return value !== null && typeof value === 'object' && !Array.isArray(value) } function parseJsonValue(text: string): { success: true; value: unknown } | { success: false } { if (!text) return { success: true, value: {} } try { return { success: true, value: JSON.parse(text) } } catch { return { success: false } } } function hasResponseField(value: Record, property: string): boolean { return Object.hasOwn(value, property) } function getWorkflowErrorStatus(status: number): number { return [400, 401, 403, 404, 408, 409, 413, 429, 499, 503].includes(status) ? status : 500 } function getWorkflowErrorCode(status: number, executeResult: Record): ErrorCode { if (status === 499) return ErrorCode.ConnectionClosed if (status === 400) return ErrorCode.InvalidParams if (status === 413 && executeResult.code !== 'workflow_response_too_large') { return ErrorCode.InvalidRequest } return ErrorCode.InternalError } function getToolsListCursor(rpcParams: unknown): string | undefined { if (!rpcParams || typeof rpcParams !== 'object' || !('cursor' in rpcParams)) return undefined const cursor = (rpcParams as { cursor?: unknown }).cursor return typeof cursor === 'string' && cursor.length > 0 ? cursor : undefined } async function getDuplicateToolName(serverId: string): Promise { const [duplicate] = await db .select({ toolName: workflowMcpTool.toolName }) .from(workflowMcpTool) .where(and(eq(workflowMcpTool.serverId, serverId), isNull(workflowMcpTool.archivedAt))) .groupBy(workflowMcpTool.toolName) .having(sql`count(*) > 1`) .limit(1) return duplicate?.toolName ?? null } async function readWorkflowExecutionResult( response: Response, signal: AbortSignal ): Promise { const text = await readResponseTextWithLimit(response, { maxBytes: MAX_MCP_WORKFLOW_RESPONSE_BYTES, label: 'MCP workflow execution response', signal, }) const parsed = parseJsonValue(text) if (parsed.success) return parsed.value if (!response.ok) return { error: response.statusText || 'Workflow execution failed' } throw new Error('Invalid workflow execution response') } async function getServer(serverId: string) { const [server] = await db .select({ id: workflowMcpServer.id, name: workflowMcpServer.name, workspaceId: workflowMcpServer.workspaceId, isPublic: workflowMcpServer.isPublic, createdBy: workflowMcpServer.createdBy, }) .from(workflowMcpServer) .innerJoin(workspace, eq(workflowMcpServer.workspaceId, workspace.id)) .where( and( eq(workflowMcpServer.id, serverId), isNull(workflowMcpServer.deletedAt), isNull(workspace.archivedAt) ) ) .limit(1) return server } type WorkflowMcpServeServer = NonNullable>> async function authorizeMcpServeRequest( request: NextRequest, server: WorkflowMcpServeServer, options: { requireAuthForPublic?: boolean } = {} ): Promise<{ response?: NextResponse; executeAuthContext?: ExecuteAuthContext }> { if (!(await isWorkspaceApiExecutionEntitled(server.workspaceId))) { return { response: NextResponse.json( { error: API_EXECUTION_REQUIRES_PAID_PLAN_MESSAGE }, { status: 402 } ), } } if (server.isPublic && !options.requireAuthForPublic) return {} const auth = await checkHybridAuth(request, { requireWorkflowId: false }) if (!auth.success || !auth.userId) { return { response: NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) } } if (server.isPublic) return {} if (auth.apiKeyType === 'workspace' && auth.workspaceId !== server.workspaceId) { return { response: NextResponse.json({ error: 'Forbidden' }, { status: 403 }) } } const workspacePermission = await getUserEntityPermissions( auth.userId, 'workspace', server.workspaceId ) if (workspacePermission === null) { return { response: NextResponse.json({ error: 'Forbidden' }, { status: 403 }) } } return { executeAuthContext: { userId: auth.userId, useAuthenticatedUserAsActor: auth.authType === AuthType.API_KEY && auth.apiKeyType === 'personal', }, } } function unsupportedSseTransportResponse(): NextResponse { return NextResponse.json( { error: { code: 'unsupported_transport', message: 'SSE transport is not supported for workflow MCP servers', supportedTransports: ['streamable-http'], allowedMethods: ['GET', 'POST', 'DELETE'], }, }, { status: 405, headers: { Allow: 'GET, POST, DELETE', 'X-MCP-Supported-Transport': 'streamable-http', }, } ) } export const GET = withRouteHandler( async (request: NextRequest, { params }: { params: Promise }) => { try { const { serverId } = mcpServeRouteParamsSchema.parse(await params) const server = await getServer(serverId) if (!server) { return NextResponse.json({ error: 'Server not found' }, { status: 404 }) } const authResult = await authorizeMcpServeRequest(request, server) if (authResult.response) return authResult.response if (request.headers.get('accept')?.includes('text/event-stream')) { return unsupportedSseTransportResponse() } return NextResponse.json({ name: server.name, version: '1.0.0', protocolVersion: '2024-11-05', capabilities: { tools: {} }, }) } catch (error) { logger.error('Error getting MCP server info:', error) return NextResponse.json({ error: 'Internal server error' }, { status: 500 }) } } ) export const POST = withRouteHandler( async (request: NextRequest, { params }: { params: Promise }) => { try { const { serverId } = mcpServeRouteParamsSchema.parse(await params) const server = await getServer(serverId) if (!server) { return NextResponse.json({ error: 'Server not found' }, { status: 404 }) } let executeAuthContext: ExecuteAuthContext | null = null const authResult = await authorizeMcpServeRequest(request, server) if (authResult.response) return authResult.response executeAuthContext = authResult.executeAuthContext ?? null let body: unknown try { body = await readJsonRpcBody(request) } catch (error) { if (isPayloadSizeLimitError(error)) { logger.warn('MCP request body exceeded size limit', { maxBytes: error.maxBytes, observedBytes: error.observedBytes, }) return NextResponse.json( createError( 0, ErrorCode.InvalidRequest, limitMessage('MCP request body', MAX_MCP_SERVE_BODY_BYTES) ), { status: 413 } ) } if (request.signal.aborted) return clientCancelledJsonRpcResponse(0) return NextResponse.json(createError(0, ErrorCode.ParseError, 'Invalid JSON body'), { status: 400, }) } const message = body as JSONRPCMessage if (isJSONRPCNotification(message)) { const notificationValidation = mcpJsonRpcNotificationSchema.safeParse(message) if (!notificationValidation.success) { return NextResponse.json( createError(0, ErrorCode.InvalidRequest, 'Invalid JSON-RPC message'), { status: 400, } ) } logger.info(`Received notification: ${message.method}`) return new NextResponse(null, { status: 202 }) } if (!isJSONRPCRequest(message)) { return NextResponse.json( createError(0, ErrorCode.InvalidRequest, 'Invalid JSON-RPC message'), { status: 400, } ) } const requestValidation = mcpJsonRpcRequestSchema.safeParse(message) if (!requestValidation.success) { return NextResponse.json( createError(0, ErrorCode.InvalidRequest, 'Invalid JSON-RPC message'), { status: 400, } ) } const { id, method, params: rpcParams } = requestValidation.data switch (method) { case 'initialize': { const result: InitializeResult = { protocolVersion: negotiateProtocolVersion(rpcParams), capabilities: { tools: {} }, serverInfo: { name: server.name, version: '1.0.0' }, } return NextResponse.json(createResponse(id, result)) } case 'ping': return NextResponse.json(createResponse(id, {})) case 'tools/list': return handleToolsList(id, serverId, rpcParams) case 'tools/call': { const paramsValidation = mcpToolCallParamsSchema.safeParse(rpcParams) if (!paramsValidation.success) { return NextResponse.json( createError(id, ErrorCode.InvalidParams, 'Invalid tool call parameters'), { status: 400, } ) } return handleToolsCall( id, serverId, paramsValidation.data, executeAuthContext, server.isPublic ? server.createdBy : undefined, request.headers.get(SIM_VIA_HEADER), request.signal ) } default: return NextResponse.json( createError(id, ErrorCode.MethodNotFound, `Method not found: ${method}`), { status: 404, } ) } } catch (error) { logger.error('Error handling MCP request:', error) return NextResponse.json(createError(0, ErrorCode.InternalError, 'Internal error'), { status: 500, }) } } ) async function handleToolsList( id: RequestId, serverId: string, rpcParams: unknown ): Promise { try { const duplicateToolName = await getDuplicateToolName(serverId) if (duplicateToolName) { return NextResponse.json( createError(id, ErrorCode.InvalidRequest, 'MCP server has duplicate tool names', { code: 'duplicate_tool_name', toolName: duplicateToolName, recovery: 'Rename or remove duplicate workflow MCP tools before listing this server', }), { status: 409 } ) } const cursor = getToolsListCursor(rpcParams) const pageCondition = cursor ? gt(workflowMcpTool.id, cursor) : undefined const toolSizes = await db .select({ id: workflowMcpTool.id, toolNameBytes: sql`octet_length(${workflowMcpTool.toolName})`, toolDescriptionBytes: sql`coalesce(octet_length(${workflowMcpTool.toolDescription}), 0)`, parameterSchemaBytes: sql`octet_length(${workflowMcpTool.parameterSchema}::text)`, }) .from(workflowMcpTool) .where( and( eq(workflowMcpTool.serverId, serverId), isNull(workflowMcpTool.archivedAt), pageCondition ) ) .orderBy(asc(workflowMcpTool.id)) .limit(MAX_MCP_TOOLS_LIST_COUNT + 1) const pageSizes = toolSizes.slice(0, MAX_MCP_TOOLS_LIST_COUNT) let estimatedSchemaBytes = 0 let estimatedMetadataBytes = 0 for (const toolSize of pageSizes) { estimatedSchemaBytes += Number(toolSize.parameterSchemaBytes) || 0 estimatedMetadataBytes += (Number(toolSize.toolNameBytes) || 0) + (Number(toolSize.toolDescriptionBytes) || 0) + (Number(toolSize.parameterSchemaBytes) || 0) assertKnownSizeWithinLimit( estimatedSchemaBytes, MAX_MCP_TOOLS_LIST_SCHEMA_BYTES, 'MCP tools/list schemas' ) assertKnownSizeWithinLimit( estimatedMetadataBytes, MAX_MCP_TOOLS_LIST_RESPONSE_BYTES, 'MCP tools/list stored metadata' ) } const tools = await db .select({ id: workflowMcpTool.id, toolName: workflowMcpTool.toolName, toolDescription: workflowMcpTool.toolDescription, parameterSchema: workflowMcpTool.parameterSchema, workflowName: workflow.name, workflowDescription: workflow.description, }) .from(workflowMcpTool) .innerJoin(workflow, eq(workflowMcpTool.workflowId, workflow.id)) .where( and( eq(workflowMcpTool.serverId, serverId), isNull(workflowMcpTool.archivedAt), pageCondition ) ) .orderBy(asc(workflowMcpTool.id)) .limit(MAX_MCP_TOOLS_LIST_COUNT + 1) const hasNextPage = tools.length > MAX_MCP_TOOLS_LIST_COUNT const pageTools = tools.slice(0, MAX_MCP_TOOLS_LIST_COUNT) const nextCursor = hasNextPage ? pageTools.at(-1)?.id : undefined let schemaBytes = 0 const result: ListToolsResult = { tools: pageTools.map((tool) => { const schema = toToolInputSchema(tool.parameterSchema) const schemaByteLength = Buffer.byteLength(JSON.stringify(schema ?? {}), 'utf-8') schemaBytes += schemaByteLength assertKnownSizeWithinLimit( schemaBytes, MAX_MCP_TOOLS_LIST_SCHEMA_BYTES, 'MCP tools/list schemas' ) return { name: tool.toolName, description: tool.toolDescription?.trim() || getMeaningfulWorkflowDescription(tool.workflowDescription, tool.workflowName) || `Execute workflow: ${tool.toolName}`, inputSchema: { type: 'object' as const, properties: schema?.properties || {}, ...(schema?.required && schema.required.length > 0 && { required: schema.required }), }, } }), ...(nextCursor && { nextCursor }), } return createJsonRpcResponseWithLimit( id, result, MAX_MCP_TOOLS_LIST_RESPONSE_BYTES, 'MCP tools/list response' ) } catch (error) { if (isPayloadSizeLimitError(error)) { logger.warn('MCP tools/list exceeded size limit', { serverId, maxBytes: error.maxBytes, observedBytes: error.observedBytes, }) return NextResponse.json( createError(id, ErrorCode.InternalError, 'MCP tools/list response is too large', { code: 'payload_too_large', maxBytes: error.maxBytes, observedBytes: error.observedBytes, recovery: 'Reduce tool names, descriptions, schemas, or tool count before retrying', }), { status: 413 } ) } logger.error('Error listing tools:', error) return NextResponse.json(createError(id, ErrorCode.InternalError, 'Failed to list tools'), { status: 500, }) } } async function handleToolsCall( id: RequestId, serverId: string, params: { name: string; arguments?: Record } | undefined, executeAuthContext?: ExecuteAuthContext | null, publicServerOwnerId?: string, simViaHeader?: string | null, requestSignal?: AbortSignal ): Promise { let abortSignal: ManagedAbortSignal | null = null try { if (!params?.name) { return NextResponse.json(createError(id, ErrorCode.InvalidParams, 'Tool name required'), { status: 400, }) } abortSignal = createManagedAbortSignal( requestSignal ?? new AbortController().signal, getMaxExecutionTimeout() ) const abortedBeforeToolLookup = callerAbortedJsonRpcResponse(id, abortSignal) if (abortedBeforeToolLookup) return abortedBeforeToolLookup const matchingTools = await db .select({ toolName: workflowMcpTool.toolName, workflowId: workflowMcpTool.workflowId, }) .from(workflowMcpTool) .where( and( eq(workflowMcpTool.serverId, serverId), eq(workflowMcpTool.toolName, params.name), isNull(workflowMcpTool.archivedAt) ) ) .orderBy(asc(workflowMcpTool.id)) .limit(2) const abortedAfterToolLookup = callerAbortedJsonRpcResponse(id, abortSignal) if (abortedAfterToolLookup) return abortedAfterToolLookup if (matchingTools.length > 1) { return NextResponse.json( createError(id, ErrorCode.InvalidRequest, `Duplicate tool name: ${params.name}`, { code: 'duplicate_tool_name', toolName: params.name, recovery: 'Rename or remove duplicate workflow MCP tools before calling this tool', }), { status: 409 } ) } const [tool] = matchingTools if (!tool) { return NextResponse.json( createError(id, ErrorCode.InvalidParams, `Tool not found: ${params.name}`), { status: 404, } ) } const [wf] = await db .select({ isDeployed: workflow.isDeployed }) .from(workflow) .where(and(eq(workflow.id, tool.workflowId), isNull(workflow.archivedAt))) .limit(1) const abortedAfterWorkflowLookup = callerAbortedJsonRpcResponse(id, abortSignal) if (abortedAfterWorkflowLookup) return abortedAfterWorkflowLookup if (!wf?.isDeployed) { return NextResponse.json( createError(id, ErrorCode.InternalError, 'Workflow is not deployed'), { status: 400, } ) } const executeUrl = `${getInternalApiBaseUrl()}/api/workflows/${tool.workflowId}/execute` const headers: Record = { 'Content-Type': 'application/json', [MCP_TOOL_BRIDGE_HEADER]: 'true', } const abortedBeforeExecute = callerAbortedJsonRpcResponse(id, abortSignal) if (abortedBeforeExecute) return abortedBeforeExecute if (publicServerOwnerId) { const internalToken = await generateInternalToken(publicServerOwnerId) headers.Authorization = `Bearer ${internalToken}` } else if (executeAuthContext) { const internalToken = await generateInternalToken(executeAuthContext.userId) headers.Authorization = `Bearer ${internalToken}` if (executeAuthContext.useAuthenticatedUserAsActor) { headers[MCP_TOOL_BRIDGE_ACTOR_HEADER] = 'authenticated-user' } } if (simViaHeader) { headers[SIM_VIA_HEADER] = simViaHeader } logger.info(`Executing workflow ${tool.workflowId} via MCP tool ${params.name}`) const workflowRequestBody = JSON.stringify({ input: params.arguments || {}, triggerType: 'mcp', includeFileBase64: false, }) assertKnownSizeWithinLimit( Buffer.byteLength(workflowRequestBody, 'utf-8'), MAX_MCP_WORKFLOW_REQUEST_BYTES, 'MCP workflow execution request body' ) const response = await fetch(executeUrl, { method: 'POST', headers, body: workflowRequestBody, signal: abortSignal.signal, }) const executeResult = await readWorkflowExecutionResult(response, abortSignal.signal) const executeResultObject = isJsonObject(executeResult) ? executeResult : null if (!response.ok) { const errorMessage = typeof executeResultObject?.error === 'string' ? executeResultObject.error : 'Workflow execution failed' const status = getWorkflowErrorStatus(response.status) const responseHeaders: Record = {} const retryAfter = response.headers.get('retry-after') if (retryAfter) responseHeaders['Retry-After'] = retryAfter return NextResponse.json( createError( id, getWorkflowErrorCode(response.status, executeResultObject ?? {}), errorMessage, { httpStatus: response.status, retryable: [408, 429, 503].includes(response.status), code: typeof executeResultObject?.code === 'string' ? executeResultObject.code : undefined, } ), { status, headers: responseHeaders } ) } const toolOutput = executeResultObject?.success === false ? executeResult : executeResultObject && hasResponseField(executeResultObject, 'output') ? executeResultObject.output : executeResult const result: CallToolResult = { content: [{ type: 'text', text: serializeToolText(toolOutput) }], isError: executeResultObject?.success === false, } return createJsonRpcResponseWithLimit( id, result, MAX_MCP_WORKFLOW_RESPONSE_BYTES, 'MCP tool call response' ) } catch (error) { if (abortSignal?.isTimedOut()) { return NextResponse.json( createError(id, ErrorCode.InternalError, 'Tool execution timed out', { code: 'timeout', retryable: true, }), { status: 408, } ) } const abortedAfterExecute = callerAbortedJsonRpcResponse(id, abortSignal) if (abortedAfterExecute) return abortedAfterExecute if (isPayloadSizeLimitError(error)) { logger.warn('MCP tool call exceeded size limit', { maxBytes: error.maxBytes, observedBytes: error.observedBytes, label: error.label, }) return NextResponse.json( createError( id, error.label === 'MCP workflow execution request body' ? ErrorCode.InvalidParams : ErrorCode.InternalError, limitMessage(error.label, error.maxBytes), { code: 'payload_too_large', maxBytes: error.maxBytes, observedBytes: error.observedBytes, retryable: false, } ), { status: 413 } ) } logger.error('Error calling tool:', error) return NextResponse.json(createError(id, ErrorCode.InternalError, 'Tool execution failed'), { status: 500, }) } finally { abortSignal?.cleanup() } } export const DELETE = withRouteHandler( async (request: NextRequest, { params }: { params: Promise }) => { try { const { serverId } = mcpServeRouteParamsSchema.parse(await params) const server = await getServer(serverId) if (!server) { return NextResponse.json({ error: 'Server not found' }, { status: 404 }) } const authResult = await authorizeMcpServeRequest(request, server, { requireAuthForPublic: true, }) if (authResult.response) return authResult.response logger.info(`MCP session terminated for server ${serverId}`) return new NextResponse(null, { status: 204 }) } catch (error) { logger.error('Error handling MCP DELETE request:', error) return NextResponse.json({ error: 'Internal server error' }, { status: 500 }) } } )