pull/715/head
Travis Fischer 2025-06-10 05:35:05 +07:00
rodzic 077a88bb90
commit b97a210602
6 zmienionych plików z 2 dodań i 271 usunięć

Wyświetl plik

@ -49,7 +49,7 @@ export const fixtureSuites: MCPE2ETestFixtureSuite[] = [
{ {
title: 'Basic MCP => OpenAPI get_post success', title: 'Basic MCP => OpenAPI get_post success',
path: '@dev/test-basic-openapi/mcp', path: '@dev/test-basic-openapi/mcp',
debug: true, // debug: true,
fixtures: [ fixtures: [
{ {
request: { request: {

Wyświetl plik

@ -56,6 +56,7 @@ app.all(async (ctx) => {
const { toolName } = parseToolIdentifier(requestedToolIdentifier) const { toolName } = parseToolIdentifier(requestedToolIdentifier)
if (toolName === 'mcp') { if (toolName === 'mcp') {
ctx.set('isJsonRpcRequest', true)
const executionCtx = ctx.executionCtx as any const executionCtx = ctx.executionCtx as any
const mcpInfo = await resolveMcpEdgeRequest(ctx) const mcpInfo = await resolveMcpEdgeRequest(ctx)
executionCtx.props = mcpInfo executionCtx.props = mcpInfo

Wyświetl plik

@ -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<Record<string, any>>({
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<string, unknown>,
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<DurableMcpClient> =
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
}

Wyświetl plik

@ -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'
)
}

Wyświetl plik

@ -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` // Handle the request with `hono`
return app.fetch(request, parsedEnv, ctx) return app.fetch(request, parsedEnv, ctx)
} }