From b97a210602dab33d0ef5c5a73b778c5c998eac4c Mon Sep 17 00:00:00 2001 From: Travis Fischer Date: Tue, 10 Jun 2025 05:35:05 +0700 Subject: [PATCH] =?UTF-8?q?=F0=9F=90=B2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/e2e/src/mcp-fixtures.ts | 2 +- apps/gateway/src/app.ts | 1 + apps/gateway/src/lib/consumer-mcp-server.ts | 244 -------------------- apps/gateway/src/{mcp.ts => lib/temp-mcp} | 0 apps/gateway/src/mcp/utils.ts | 12 - apps/gateway/src/worker.ts | 14 -- 6 files changed, 2 insertions(+), 271 deletions(-) delete mode 100644 apps/gateway/src/lib/consumer-mcp-server.ts rename apps/gateway/src/{mcp.ts => lib/temp-mcp} (100%) delete mode 100644 apps/gateway/src/mcp/utils.ts diff --git a/apps/e2e/src/mcp-fixtures.ts b/apps/e2e/src/mcp-fixtures.ts index 84b0d907..a1b1a299 100644 --- a/apps/e2e/src/mcp-fixtures.ts +++ b/apps/e2e/src/mcp-fixtures.ts @@ -49,7 +49,7 @@ export const fixtureSuites: MCPE2ETestFixtureSuite[] = [ { title: 'Basic MCP => OpenAPI get_post success', path: '@dev/test-basic-openapi/mcp', - debug: true, + // debug: true, fixtures: [ { request: { diff --git a/apps/gateway/src/app.ts b/apps/gateway/src/app.ts index e1b90a9c..f33ce716 100644 --- a/apps/gateway/src/app.ts +++ b/apps/gateway/src/app.ts @@ -56,6 +56,7 @@ app.all(async (ctx) => { const { toolName } = parseToolIdentifier(requestedToolIdentifier) if (toolName === 'mcp') { + ctx.set('isJsonRpcRequest', true) const executionCtx = ctx.executionCtx as any const mcpInfo = await resolveMcpEdgeRequest(ctx) executionCtx.props = mcpInfo diff --git a/apps/gateway/src/lib/consumer-mcp-server.ts b/apps/gateway/src/lib/consumer-mcp-server.ts deleted file mode 100644 index 1e3cf19b..00000000 --- a/apps/gateway/src/lib/consumer-mcp-server.ts +++ /dev/null @@ -1,244 +0,0 @@ -import type { AdminDeployment, PricingPlan } from '@agentic/platform-types' -import { assert } from '@agentic/platform-core' -import { parseDeploymentIdentifier } from '@agentic/platform-validators' -import { Server } from '@modelcontextprotocol/sdk/server/index.js' -import { - CallToolRequestSchema, - ListToolsRequestSchema -} from '@modelcontextprotocol/sdk/types.js' -import contentType from 'fast-content-type-parse' - -import type { DurableMcpClient } from './durable-mcp-client' -import type { - AdminConsumer, - AgenticMcpRequestMetadata, - GatewayHonoContext, - McpToolCallResponse -} from './types' -import { cfValidateJsonSchema } from './cf-validate-json-schema' -import { createRequestForOpenAPIOperation } from './create-request-for-openapi-operation' -import { fetchCache } from './fetch-cache' -import { getRequestCacheKey } from './get-request-cache-key' -import { updateOriginRequest } from './update-origin-request' - -export type ConsumerMcpServerOptions = { - sessionId: string - deployment: AdminDeployment - consumer?: AdminConsumer - pricingPlan?: PricingPlan -} - -export function createConsumerMcpServer( - ctx: GatewayHonoContext, - { sessionId, deployment, consumer, pricingPlan }: ConsumerMcpServerOptions -) { - const { originAdapter } = deployment - const { projectIdentifier } = parseDeploymentIdentifier(deployment.identifier) - - const server = new Server( - { name: projectIdentifier, version: deployment.version ?? '0.0.0' }, - { - capabilities: { - // TODO: add support for more capabilities - tools: {} - } - } - ) - - const tools = deployment.tools - .map((tool) => { - const toolConfig = deployment.toolConfigs.find( - (toolConfig) => toolConfig.name === tool.name - ) - - if (toolConfig) { - const pricingPlanToolConfig = pricingPlan - ? toolConfig.pricingPlanConfig?.[pricingPlan.slug] - : undefined - - if (pricingPlanToolConfig?.enabled === false) { - // Tool is disabled / hidden for the customer's current pricing plan - return undefined - } - - if (!pricingPlanToolConfig?.enabled && !toolConfig.enabled) { - // Tool is disabled / hidden for all pricing plans - return undefined - } - } - - return tool - }) - .filter(Boolean) - - server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools })) - - server.setRequestHandler(CallToolRequestSchema, async (request) => { - const { name, arguments: args, _meta } = request.params - - const tool = tools.find((tool) => tool.name === name) - assert(tool, 404, `Unknown tool: ${name}`) - - // TODO: Implement tool config logic - // const toolConfig = deployment.toolConfigs.find( - // (toolConfig) => toolConfig.name === tool.name - // ) - - if (originAdapter.type === 'raw') { - // TODO - assert(false, 500, 'Raw origin adapter not implemented') - } else { - // Validate incoming request params against the tool's input schema. - const toolCallArgs = cfValidateJsonSchema>({ - schema: tool.inputSchema, - data: args, - errorMessage: `Invalid request parameters for tool "${tool.name}"`, - strictAdditionalProperties: true - }) - - if (originAdapter.type === 'openapi') { - const operation = originAdapter.toolToOperationMap[tool.name] - assert(operation, 404, `Tool "${tool.name}" not found in OpenAPI spec`) - assert(toolCallArgs, 500) - - const originRequest = await createRequestForOpenAPIOperation({ - toolCallArgs, - operation, - deployment - }) - - updateOriginRequest(originRequest, { consumer, deployment }) - - const cacheKey = await getRequestCacheKey(ctx, originRequest) - - // TODO: transform origin 5XX errors to 502 errors... - // TODO: fetch origin request and transform response - const originResponse = await fetchCache(ctx, { - cacheKey, - fetchResponse: () => fetch(originRequest) - }) - - const { type: mimeType } = contentType.safeParse( - originResponse.headers.get('content-type') || - 'application/octet-stream' - ) - - if (tool.outputSchema) { - assert( - mimeType.includes('json'), - 502, - `Tool "${tool.name}" requires a JSON response, but the origin returned content type "${mimeType}"` - ) - const res: any = await originResponse.json() - - const toolCallResponseContent = cfValidateJsonSchema({ - schema: tool.outputSchema, - data: res as Record, - coerce: false, - // TODO: double-check MCP schema on whether additional properties are allowed - strictAdditionalProperties: true, - errorMessage: `Invalid tool response for tool "${tool.name}"`, - errorStatusCode: 502 - }) - - return { - structuredContent: toolCallResponseContent, - isError: originResponse.status >= 400 - } - } else { - const result: McpToolCallResponse = { - isError: originResponse.status >= 400 - } - - if (mimeType.includes('json')) { - result.structuredContent = await originResponse.json() - } else if (mimeType.includes('text')) { - result.content = [ - { - type: 'text', - text: await originResponse.text() - } - ] - } else { - const resBody = await originResponse.arrayBuffer() - const resBodyBase64 = Buffer.from(resBody).toString('base64') - const type = mimeType.includes('image') - ? 'image' - : mimeType.includes('audio') - ? 'audio' - : 'resource' - - // TODO: this needs work - result.content = [ - { - type, - mimeType, - ...(type === 'resource' - ? { - blob: resBodyBase64 - } - : { - data: resBodyBase64 - }) - } - ] - } - - return result - } - } else if (originAdapter.type === 'mcp') { - const id: DurableObjectId = ctx.env.DO_MCP_CLIENT.idFromName(sessionId) - const originMcpClient: DurableObjectStub = - ctx.env.DO_MCP_CLIENT.get(id) - - await originMcpClient.init({ - url: deployment.originUrl, - name: originAdapter.serverInfo.name, - version: originAdapter.serverInfo.version - }) - - const { projectIdentifier } = parseDeploymentIdentifier( - deployment.identifier, - { errorStatusCode: 500 } - ) - - const originMcpRequestMetadata = { - agenticProxySecret: deployment._secret, - sessionId, - // ip, - isCustomerSubscriptionActive: !!consumer?.isStripeSubscriptionActive, - customerId: consumer?.id, - customerSubscriptionPlan: consumer?.plan, - customerSubscriptionStatus: consumer?.stripeStatus, - userId: consumer?.user.id, - userEmail: consumer?.user.email, - userUsername: consumer?.user.username, - userName: consumer?.user.name, - userCreatedAt: consumer?.user.createdAt, - userUpdatedAt: consumer?.user.updatedAt, - deploymentId: deployment.id, - deploymentIdentifier: deployment.identifier, - projectId: deployment.projectId, - projectIdentifier - } as AgenticMcpRequestMetadata - - // TODO: add timeout support to the origin tool call? - // TODO: add response caching for MCP tool calls - const toolCallResponseString = await originMcpClient.callTool({ - name: tool.name, - args: toolCallArgs, - metadata: originMcpRequestMetadata! - }) - const toolCallResponse = JSON.parse( - toolCallResponseString - ) as McpToolCallResponse - - return toolCallResponse - } else { - assert(false, 500) - } - } - }) - - return server -} diff --git a/apps/gateway/src/mcp.ts b/apps/gateway/src/lib/temp-mcp similarity index 100% rename from apps/gateway/src/mcp.ts rename to apps/gateway/src/lib/temp-mcp diff --git a/apps/gateway/src/mcp/utils.ts b/apps/gateway/src/mcp/utils.ts deleted file mode 100644 index 2a91b2fb..00000000 --- a/apps/gateway/src/mcp/utils.ts +++ /dev/null @@ -1,12 +0,0 @@ -export function isDurableObjectNamespace( - namespace: unknown -): namespace is DurableObjectNamespace { - return ( - typeof namespace === 'object' && - namespace !== null && - 'newUniqueId' in namespace && - typeof namespace.newUniqueId === 'function' && - 'idFromName' in namespace && - typeof namespace.idFromName === 'function' - ) -} diff --git a/apps/gateway/src/worker.ts b/apps/gateway/src/worker.ts index 44d916bb..e8987dd9 100644 --- a/apps/gateway/src/worker.ts +++ b/apps/gateway/src/worker.ts @@ -30,20 +30,6 @@ export default { }) } - // const requestUrl = new URL(request.url) - // const { pathname } = requestUrl - // const requestedToolIdentifier = pathname - // .replace(/^\//, '') - // .replace(/\/$/, '') - // const { toolName } = parseToolIdentifier(requestedToolIdentifier) - - // if (toolName === 'mcp') { - // // Handle MCP requests - // return DurableMcpServer.serve('/*', { - // binding: 'DO_MCP_SERVER' - // }).fetch(request, parsedEnv, ctx) - // } - // Handle the request with `hono` return app.fetch(request, parsedEnv, ctx) }