feat: api gateway mcp edge working now

pull/715/head
Travis Fischer 2025-06-10 05:10:09 +07:00
rodzic 52101d15a4
commit 0b94b02c19
15 zmienionych plików z 812 dodań i 166 usunięć

Wyświetl plik

@ -0,0 +1,17 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`Basic MCP => OpenAPI get_post success > 0.0: @dev/test-basic-openapi/mcp get_post 1`] = `
{
"content": [],
"isError": false,
"structuredContent": {
"body": "quia et suscipit
suscipit recusandae consequuntur expedita et cum
reprehenderit molestiae ut ut quas totam
nostrum rerum est autem sunt rem eveniet architecto",
"id": 1,
"title": "sunt aut facere repellat provident occaecati excepturi optio reprehenderit",
"userId": 1,
},
}
`;

Wyświetl plik

@ -24,6 +24,7 @@ for (const [i, fixtureSuite] of fixtureSuites.entries()) {
for (const [j, fixture] of fixtures.entries()) {
const method = fixture.request?.method ?? 'GET'
const timeout = fixture.timeout ?? 30_000
const {
status = 200,
contentType: expectedContentType = 'application/json',
@ -45,11 +46,14 @@ for (const [i, fixtureSuite] of fixtureSuites.entries()) {
testFn(
`${i}.${j}: ${method} ${fixture.path}`,
{
timeout: fixture.timeout ?? 60_000
timeout
},
// eslint-disable-next-line no-loop-func
async () => {
const res = await ky(fixture.path, fixture.request)
const res = await ky(fixture.path, {
timeout,
...fixture.request
})
expect(res.status).toBe(status)
const { type } = contentType.safeParse(

Wyświetl plik

@ -30,7 +30,11 @@ for (const [i, fixtureSuite] of fixtureSuites.entries()) {
})
for (const [j, fixture] of fixtures.entries()) {
const { isError, body: expectedBody, validate } = fixture.response ?? {}
const {
isError,
result: expectedResult,
validate
} = fixture.response ?? {}
const snapshot =
fixture.response?.snapshot ?? fixtureSuite.snapshot ?? !isError
const debugFixture = !!(fixture.debug ?? fixtureSuite.debug)
@ -52,15 +56,18 @@ for (const [i, fixtureSuite] of fixtureSuites.entries()) {
console.log('tools', tools)
expect(tools.map((t) => t.name)).toContain(fixture.request.name)
const result = await client.callTool(fixture.request)
const result = await client.callTool({
name: fixture.request.name,
arguments: fixture.request.args
})
if (isError) {
expect(result.isError).toBeTruthy()
} else {
expect(result.isError).toBeFalsy()
}
if (expectedBody) {
expect(result).toEqual(expectedBody)
if (expectedResult) {
expect(result).toEqual(expectedResult)
}
if (snapshot) {

Wyświetl plik

@ -15,7 +15,7 @@ export type MCPE2ETestFixture = {
response?: {
isError?: boolean
body?: any
result?: any
validate?: (result: any) => void | Promise<void>
/** @default true */
snapshot?: boolean
@ -49,6 +49,7 @@ export const fixtureSuites: MCPE2ETestFixtureSuite[] = [
{
title: 'Basic MCP => OpenAPI get_post success',
path: '@dev/test-basic-openapi/mcp',
debug: true,
fixtures: [
{
request: {
@ -59,52 +60,56 @@ export const fixtureSuites: MCPE2ETestFixtureSuite[] = [
}
}
]
},
{
title: 'Basic MCP => MCP "echo" tool call success',
path: '@dev/test-basic-mcp/mcp',
snapshot: false,
fixtures: [
{
request: {
name: 'echo',
args: {
nala: 'kitten',
num: 123,
now
}
},
response: {
result: {
content: [
{
type: 'text',
text: JSON.stringify({ nala: 'kitten', num: 123, now })
}
]
}
}
},
{
request: {
name: 'echo',
args: {
nala: 'kitten',
num: 123,
now: `${now}`
}
},
response: {
result: {
content: [
{
type: 'text',
text: JSON.stringify({
nala: 'kitten',
num: 123,
now: `${now}`
})
}
]
}
}
}
]
}
// {
// title: 'Basic MCP => MCP "echo" tool call success',
// path: '@dev/test-basic-mcp/mcp',
// snapshot: false,
// fixtures: [
// {
// request: {
// name: 'echo',
// args: {
// nala: 'kitten',
// num: 123,
// now
// }
// },
// response: {
// body: [
// {
// type: 'text',
// text: JSON.stringify({ nala: 'kitten', num: 123, now })
// }
// ]
// }
// },
// {
// request: {
// name: 'echo',
// args: {
// nala: 'kitten',
// num: 123,
// now: `${now}`
// }
// },
// response: {
// body: [
// {
// type: 'text',
// text: JSON.stringify({
// nala: 'kitten',
// num: '123',
// now: `${now}`
// })
// }
// ]
// }
// }
// ]
// }
]

Wyświetl plik

@ -37,6 +37,7 @@
"@agentic/platform-validators": "workspace:*",
"@hono/zod-validator": "catalog:",
"@modelcontextprotocol/sdk": "catalog:",
"agents": "^0.0.95",
"fast-content-type-parse": "catalog:",
"hono": "catalog:",
"ky": "catalog:",

Wyświetl plik

@ -14,8 +14,9 @@ import { createAgenticClient } from './lib/agentic-client'
import { createHttpResponseFromMcpToolCallResponse } from './lib/create-http-response-from-mcp-tool-call-response'
import { fetchCache } from './lib/fetch-cache'
import { getRequestCacheKey } from './lib/get-request-cache-key'
import { resolveMcpEdgeRequest } from './lib/resolve-mcp-edge-request'
import { resolveOriginRequest } from './lib/resolve-origin-request'
import { handleMcpRequest } from './mcp'
import { DurableMcpServer } from './worker'
export const app = new Hono<GatewayHonoEnv>()
@ -55,7 +56,14 @@ app.all(async (ctx) => {
const { toolName } = parseToolIdentifier(requestedToolIdentifier)
if (toolName === 'mcp') {
return handleMcpRequest(ctx)
const executionCtx = ctx.executionCtx as any
const mcpInfo = await resolveMcpEdgeRequest(ctx)
executionCtx.props = mcpInfo
// Handle MCP requests
return DurableMcpServer.serve(pathname, {
binding: 'DO_MCP_SERVER'
}).fetch(ctx.req.raw, ctx.env, executionCtx)
}
const resolvedOriginRequest = await resolveOriginRequest(ctx)

Wyświetl plik

@ -1,129 +1,311 @@
import type { AdminDeployment, PricingPlan } from '@agentic/platform-types'
import type { JSONRPCRequest } from '@modelcontextprotocol/sdk/types.js'
// import type { JSONRPCRequest } from '@modelcontextprotocol/sdk/types.js'
import { assert } from '@agentic/platform-core'
import { assert, HttpError } from '@agentic/platform-core'
import { parseDeploymentIdentifier } from '@agentic/platform-validators'
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js'
import { DurableObject } from 'cloudflare:workers'
// import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'
import { Server } from '@modelcontextprotocol/sdk/server/index.js'
import {
CallToolRequestSchema,
ListToolsRequestSchema
} from '@modelcontextprotocol/sdk/types.js'
import { McpAgent } from 'agents/mcp'
import contentType from 'fast-content-type-parse'
import type { AdminConsumer } from './types'
import type { RawEnv } from './env'
import type {
AdminConsumer,
AgenticMcpRequestMetadata,
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 DurableMcpServerInfo = {
deployment: AdminDeployment
consumer?: AdminConsumer
pricingPlan?: PricingPlan
}
type State = { counter: number }
export class DurableMcpServer extends DurableObject {
protected server?: McpServer
protected serverTransport?: StreamableHTTPServerTransport
protected serverConnectionP?: Promise<void>
export class DurableMcpServer extends McpAgent<
RawEnv,
State,
{
deployment: AdminDeployment
consumer?: AdminConsumer
pricingPlan?: PricingPlan
}
> {
protected _serverP = Promise.withResolvers<Server>()
override server = this._serverP.promise
async init(mcpServerInfo: DurableMcpServerInfo) {
const existingMcpServerInfo =
await this.ctx.storage.get<DurableMcpServerInfo>('mcp-server-info')
if (!existingMcpServerInfo) {
await this.ctx.storage.put('mcp-server-info', mcpServerInfo)
} else {
assert(
mcpServerInfo.deployment.id === existingMcpServerInfo.deployment.id,
500,
`DurableMcpServerInfo deployment id mismatch: "${mcpServerInfo.deployment.id}" vs "${existingMcpServerInfo.deployment.id}"`
)
}
return this.ensureServerConnection(mcpServerInfo)
override initialState: State = {
counter: 1
}
async isInitialized(): Promise<boolean> {
return !!(await this.ctx.storage.get('mcp-server-info'))
}
async ensureServerConnection(mcpServerInfo?: DurableMcpServerInfo) {
if (this.serverConnectionP) return this.serverConnectionP
mcpServerInfo ??=
await this.ctx.storage.get<DurableMcpServerInfo>('mcp-server-info')
assert(mcpServerInfo, 500, 'DurableMcpServer has not been initialized')
const { deployment } = mcpServerInfo
override async init() {
const { consumer, deployment, pricingPlan } = this.props
const { originAdapter } = deployment
const { projectIdentifier } = parseDeploymentIdentifier(
deployment.identifier
)
this.server = new McpServer({
name: projectIdentifier,
version: deployment.version ?? '0.0.0'
})
const server = new Server(
{ name: projectIdentifier, version: deployment.version ?? '0.0.0' },
{
capabilities: {
tools: {}
}
}
)
this._serverP.resolve(server)
for (const tool of deployment.tools) {
this.server.registerTool(
tool.name,
{
description: tool.description,
inputSchema: tool.inputSchema as any, // TODO: investigate types
outputSchema: tool.outputSchema as any, // TODO: investigate types
annotations: tool.annotations
},
(_args: Record<string, unknown>) => {
assert(false, 500, `Tool call not implemented: ${tool.name}`)
const tools = deployment.tools
.map((tool) => {
const toolConfig = deployment.toolConfigs.find(
(toolConfig) => toolConfig.name === tool.name
)
// TODO???
return {
content: [],
_meta: {
toolName: 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
}
}
)
}
const transport = new StreamableHTTPServerTransport({
sessionIdGenerator: () => {
// TODO: improve this
return crypto.randomUUID()
},
onsessioninitialized: (sessionId) => {
// TODO: improve this
// eslint-disable-next-line no-console
console.log(`Session initialized: ${sessionId}`)
return tool
})
.filter(Boolean)
server.setRequestHandler(ListToolsRequestSchema, async () => ({
tools
}))
server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { name, arguments: args } = 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 originResponse = await fetch(originRequest)
const { type: mimeType } = contentType.safeParse(
originResponse.headers.get('content-type') ||
'application/octet-stream'
)
// eslint-disable-next-line no-console
console.log('httpOriginResponse', {
tool: tool.name,
toolCallArgs,
url: originRequest.url,
method: originRequest.method,
originResponse: {
mimeType,
status: originResponse.status,
headers: Object.fromEntries(originResponse.headers.entries())
}
})
if (originResponse.status >= 400) {
let message = originResponse.statusText
try {
message = await originResponse.text()
} catch {}
// eslint-disable-next-line no-console
console.error('httpOriginResponse ERROR', {
tool: tool.name,
toolCallArgs,
url: originRequest.url,
method: originRequest.method,
originResponse: {
mimeType,
status: originResponse.status,
headers: Object.fromEntries(originResponse.headers.entries()),
message
}
})
throw new HttpError({
statusCode: originResponse.status,
message,
cause: originResponse
})
}
// TODO
// 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)
// })
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 sessionId = this.ctx.id.toString()
const id: DurableObjectId =
this.env.DO_MCP_CLIENT.idFromName(sessionId)
const originMcpClient = this.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)
}
}
})
this.serverConnectionP = this.server.connect(transport)
return this.serverConnectionP
}
// async fetch(request: Request) {
// await this.ensureServerConnection()
// const { readable, writable } = new TransformStream()
// const writer = writable.getWriter()
// const encoder = new TextEncoder()
// const response = new Response(readable, {
// headers: {
// 'Content-Type': 'text/event-stream',
// 'Cache-Control': 'no-cache',
// Connection: 'keep-alive'
// // 'mcp-session-id': sessionId
// }
// })
// await this.serverTransport!.handleRequest(request, response)
// }
async onRequest(message: JSONRPCRequest) {
await this.ensureServerConnection()
// We need to map every incoming message to the connection that it came in on
// so that we can send relevant responses and notifications back on the same connection
// if (isJSONRPCRequest(message)) {
// this._requestIdToConnectionId.set(message.id.toString(), connection.id);
// }
this.serverTransport!.onmessage?.(message)
override onStateUpdate(state: State) {
// eslint-disable-next-line no-console
console.log({ stateUpdate: state })
}
}

Wyświetl plik

@ -7,7 +7,6 @@ import {
import { z } from 'zod'
import type { DurableMcpClient } from './durable-mcp-client'
import type { DurableMcpServer } from './durable-mcp-server'
import type { DurableRateLimiter } from './durable-rate-limiter'
export const envSchema = baseEnvSchema
@ -19,7 +18,7 @@ export const envSchema = baseEnvSchema
(ns) => isDurableObjectNamespace(ns)
),
DO_MCP_SERVER: z.custom<DurableObjectNamespace<DurableMcpServer>>((ns) =>
DO_MCP_SERVER: z.custom<DurableObjectNamespace>((ns) =>
isDurableObjectNamespace(ns)
),

Wyświetl plik

@ -0,0 +1,129 @@
// import type { AdminDeployment, PricingPlan } from '@agentic/platform-types'
// import type { JSONRPCRequest } from '@modelcontextprotocol/sdk/types.js'
// // import type { JSONRPCRequest } from '@modelcontextprotocol/sdk/types.js'
// import { assert } from '@agentic/platform-core'
// import { parseDeploymentIdentifier } from '@agentic/platform-validators'
// import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'
// import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js'
// import { DurableObject } from 'cloudflare:workers'
// import type { AdminConsumer } from './types'
// export type DurableMcpServerInfo = {
// deployment: AdminDeployment
// consumer?: AdminConsumer
// pricingPlan?: PricingPlan
// }
// export class DurableMcpServer extends DurableObject {
// protected server?: McpServer
// protected serverTransport?: StreamableHTTPServerTransport
// protected serverConnectionP?: Promise<void>
// async init(mcpServerInfo: DurableMcpServerInfo) {
// const existingMcpServerInfo =
// await this.ctx.storage.get<DurableMcpServerInfo>('mcp-server-info')
// if (!existingMcpServerInfo) {
// await this.ctx.storage.put('mcp-server-info', mcpServerInfo)
// } else {
// assert(
// mcpServerInfo.deployment.id === existingMcpServerInfo.deployment.id,
// 500,
// `DurableMcpServerInfo deployment id mismatch: "${mcpServerInfo.deployment.id}" vs "${existingMcpServerInfo.deployment.id}"`
// )
// }
// return this.ensureServerConnection(mcpServerInfo)
// }
// async isInitialized(): Promise<boolean> {
// return !!(await this.ctx.storage.get('mcp-server-info'))
// }
// async ensureServerConnection(mcpServerInfo?: DurableMcpServerInfo) {
// if (this.serverConnectionP) return this.serverConnectionP
// mcpServerInfo ??=
// await this.ctx.storage.get<DurableMcpServerInfo>('mcp-server-info')
// assert(mcpServerInfo, 500, 'DurableMcpServer has not been initialized')
// const { deployment } = mcpServerInfo
// const { projectIdentifier } = parseDeploymentIdentifier(
// deployment.identifier
// )
// this.server = new McpServer({
// name: projectIdentifier,
// version: deployment.version ?? '0.0.0'
// })
// for (const tool of deployment.tools) {
// this.server.registerTool(
// tool.name,
// {
// description: tool.description,
// inputSchema: tool.inputSchema as any, // TODO: investigate types
// outputSchema: tool.outputSchema as any, // TODO: investigate types
// annotations: tool.annotations
// },
// (_args: Record<string, unknown>) => {
// assert(false, 500, `Tool call not implemented: ${tool.name}`)
// // TODO???
// return {
// content: [],
// _meta: {
// toolName: tool.name
// }
// }
// }
// )
// }
// const transport = new StreamableHTTPServerTransport({
// sessionIdGenerator: () => {
// // TODO: improve this
// return crypto.randomUUID()
// },
// onsessioninitialized: (sessionId) => {
// // TODO: improve this
// // eslint-disable-next-line no-console
// console.log(`Session initialized: ${sessionId}`)
// }
// })
// this.serverConnectionP = this.server.connect(transport)
// return this.serverConnectionP
// }
// // async fetch(request: Request) {
// // await this.ensureServerConnection()
// // const { readable, writable } = new TransformStream()
// // const writer = writable.getWriter()
// // const encoder = new TextEncoder()
// // const response = new Response(readable, {
// // headers: {
// // 'Content-Type': 'text/event-stream',
// // 'Cache-Control': 'no-cache',
// // Connection: 'keep-alive'
// // // 'mcp-session-id': sessionId
// // }
// // })
// // await this.serverTransport!.handleRequest(request, response)
// // }
// async onRequest(message: JSONRPCRequest) {
// await this.ensureServerConnection()
// // We need to map every incoming message to the connection that it came in on
// // so that we can send relevant responses and notifications back on the same connection
// // if (isJSONRPCRequest(message)) {
// // this._requestIdToConnectionId.set(message.id.toString(), connection.id);
// // }
// this.serverTransport!.onmessage?.(message)
// }
// }

Wyświetl plik

@ -1,5 +1,6 @@
// import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'
// import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js'
// import type { Transport } from '@modelcontextprotocol/sdk/shared/transport.js'
import { assert, JsonRpcError } from '@agentic/platform-core'
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js'
import {
@ -33,6 +34,86 @@ import { resolveMcpEdgeRequest } from './lib/resolve-mcp-edge-request'
// }
// })
// class McpStreamableHttpTransport implements Transport {
// onclose?: () => void
// onerror?: (error: Error) => void
// onmessage?: (message: JSONRPCMessage) => void
// sessionId?: string
// // TODO: If there is an open connection to send server-initiated messages
// // back, we should use that connection
// private _getWebSocketForGetRequest: () => WebSocket | null
// // Get the appropriate websocket connection for a given message id
// private _getWebSocketForMessageID: (id: string) => WebSocket | null
// // Notify the server that a response has been sent for a given message id
// // so that it may clean up it's mapping of message ids to connections
// // once they are no longer needed
// private _notifyResponseIdSent: (id: string) => void
// private _started = false
// constructor(
// getWebSocketForMessageID: (id: string) => WebSocket | null,
// notifyResponseIdSent: (id: string | number) => void
// ) {
// this._getWebSocketForMessageID = getWebSocketForMessageID
// this._notifyResponseIdSent = notifyResponseIdSent
// // TODO
// this._getWebSocketForGetRequest = () => null
// }
// async start() {
// // The transport does not manage the WebSocket connection since it's terminated
// // by the Durable Object in order to allow hibernation. There's nothing to initialize.
// if (this._started) {
// throw new Error('Transport already started')
// }
// this._started = true
// }
// async send(message: JSONRPCMessage) {
// if (!this._started) {
// throw new Error('Transport not started')
// }
// let websocket: WebSocket | null = null
// if (isJSONRPCResponse(message) || isJSONRPCError(message)) {
// websocket = this._getWebSocketForMessageID(message.id.toString())
// if (!websocket) {
// throw new Error(
// `Could not find WebSocket for message id: ${message.id}`
// )
// }
// } else if (isJSONRPCRequest(message)) {
// // requests originating from the server must be sent over the
// // the connection created by a GET request
// websocket = this._getWebSocketForGetRequest()
// } else if (isJSONRPCNotification(message)) {
// // notifications do not have an id
// // but do have a relatedRequestId field
// // so that they can be sent to the correct connection
// websocket = null
// }
// try {
// websocket?.send(JSON.stringify(message))
// if (isJSONRPCResponse(message)) {
// this._notifyResponseIdSent(message.id.toString())
// }
// } catch (err) {
// this.onerror?.(err as Error)
// throw err
// }
// }
// async close() {
// // Similar to start, the only thing to do is to pass the event on to the server
// this.onclose?.()
// }
// }
const MAXIMUM_MESSAGE_SIZE_BYTES = 4 * 1024 * 1024 // 4MB
export async function handleMcpRequest(ctx: GatewayHonoContext) {
@ -282,6 +363,10 @@ export async function handleMcpRequest(ctx: GatewayHonoContext) {
await transport.send(message)
}
// console.log('>>> waiting...')
// await new Promise((resolve) => setTimeout(resolve, 2000))
// console.log('<<< waiting...')
// Return the streamable http response.
return new Response(readable, {
headers: {

Wyświetl plik

@ -1,4 +1,7 @@
// import { parseToolIdentifier } from '@agentic/platform-validators'
import { app } from './app'
// import { DurableMcpServer } from './lib/durable-mcp-server'
import { type Env, parseEnv } from './lib/env'
// Export Durable Objects for cloudflare
@ -27,6 +30,20 @@ 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)
}

Wyświetl plik

@ -459,7 +459,7 @@ export interface components {
JsonSchemaObject: {
/** @enum {string} */
type: "object";
properties?: Record<string, never>;
properties?: Record<string, unknown>;
required?: string[];
};
Tool: {

Wyświetl plik

@ -34,7 +34,8 @@ export const toolNameSchema = z
export const jsonSchemaObjectSchema = z
.object({
type: z.literal('object'),
properties: z.object({}).passthrough().optional(),
// TODO: improve this schema
properties: z.record(z.string(), z.any()).optional(),
required: z.array(z.string()).optional()
})
.passthrough()

Wyświetl plik

@ -433,6 +433,9 @@ importers:
'@modelcontextprotocol/sdk':
specifier: 'catalog:'
version: 1.12.1
agents:
specifier: ^0.0.95
version: 0.0.95(@cloudflare/workers-types@4.20250604.0)(react@19.1.0)
fast-content-type-parse:
specifier: 'catalog:'
version: 3.0.0
@ -777,6 +780,32 @@ packages:
arctic: ^2.2.2
hono: ^4.0.0
'@ai-sdk/provider-utils@2.2.8':
resolution: {integrity: sha512-fqhG+4sCVv8x7nFzYnFo19ryhAa3w096Kmc3hWxMQfW/TubPOmt3A6tYZhl4mUfQWWQMsuSkLrtjlWuXBVSGQA==}
engines: {node: '>=18'}
peerDependencies:
zod: ^3.23.8
'@ai-sdk/provider@1.1.3':
resolution: {integrity: sha512-qZMxYJ0qqX/RfnuIaab+zp8UAeJn/ygXXAffR5I4N0n1IrvA6qBsjc8hXLmBiMV2zoXlifkacF7sEFnYnjBcqg==}
engines: {node: '>=18'}
'@ai-sdk/react@1.2.12':
resolution: {integrity: sha512-jK1IZZ22evPZoQW3vlkZ7wvjYGYF+tRBKXtrcolduIkQ/m/sOAVcVeVDUDvh1T91xCnWCdUGCPZg2avZ90mv3g==}
engines: {node: '>=18'}
peerDependencies:
react: ^18 || ^19 || ^19.0.0-rc
zod: ^3.23.8
peerDependenciesMeta:
zod:
optional: true
'@ai-sdk/ui-utils@1.2.11':
resolution: {integrity: sha512-3zcwCc8ezzFlwp3ZD15wAPjf2Au4s3vAbKsXQVyhxODHcmu0iyPO2Eua6D/vicq/AUm/BAo60r97O6HU+EI0+w==}
engines: {node: '>=18'}
peerDependencies:
zod: ^3.23.8
'@apideck/better-ajv-errors@0.3.6':
resolution: {integrity: sha512-P+ZygBLZtkp0qqOAJJVX4oX/sFo5JR3eBWwwuqHHhK0GIgQOKWrAfiAaWX0aArHkRWHMuggFEgAZNxVPwPZYaA==}
engines: {node: '>=10'}
@ -2463,6 +2492,9 @@ packages:
'@types/deep-eql@4.0.2':
resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==}
'@types/diff-match-patch@1.0.36':
resolution: {integrity: sha512-xFdR6tkm0MWvBfO8xXCSsinYxHcqkQUlcHeSpMC2ukzOb6lwQAfDmW+Qt0AvlGd8HpsS28qKsB+oPeJn9I39jg==}
'@types/estree@1.0.7':
resolution: {integrity: sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==}
@ -2640,6 +2672,21 @@ packages:
resolution: {integrity: sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw==}
engines: {node: '>= 14'}
agents@0.0.95:
resolution: {integrity: sha512-qj0GVnoyjhj9gGgulShlMYvF6xDI2PpmeE7rpFCWkLxbeJPecUwmDU9Vm7F8ub9Yt58zieA3F0zASx/Z50aYhA==}
peerDependencies:
react: '*'
ai@4.3.16:
resolution: {integrity: sha512-KUDwlThJ5tr2Vw0A1ZkbDKNME3wzWhuVfAOwIvFUzl1TPVDFAXDFTXio3p+jaKneB+dKNCvFFlolYmmgHttG1g==}
engines: {node: '>=18'}
peerDependencies:
react: ^18 || ^19 || ^19.0.0-rc
zod: ^3.23.8
peerDependenciesMeta:
react:
optional: true
ajv-formats@3.0.1:
resolution: {integrity: sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==}
peerDependencies:
@ -2964,6 +3011,10 @@ packages:
resolution: {integrity: sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==}
engines: {node: '>= 0.10'}
cron-schedule@5.0.4:
resolution: {integrity: sha512-nH0a49E/kSVk6BeFgKZy4uUsy6D2A16p120h5bYD9ILBhQu7o2sJFH+WI4R731TSBQ0dB1Ik7inB/dRAB4C8QQ==}
engines: {node: '>=18'}
cross-spawn@7.0.6:
resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==}
engines: {node: '>= 8'}
@ -3078,10 +3129,17 @@ packages:
resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==}
engines: {node: '>= 0.8'}
dequal@2.0.3:
resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==}
engines: {node: '>=6'}
detect-libc@2.0.4:
resolution: {integrity: sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==}
engines: {node: '>=8'}
diff-match-patch@1.0.5:
resolution: {integrity: sha512-IayShXAgj/QMXgB0IWmKx+rOPuGMhqm5w6jvFxmVenXKIzRqTAAsbBPT3kWQeGANj3jGgvcvv4yK6SxqYmikgw==}
doctrine@2.1.0:
resolution: {integrity: sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==}
engines: {node: '>=0.10.0'}
@ -3461,6 +3519,9 @@ packages:
resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==}
engines: {node: '>= 0.6'}
event-target-polyfill@0.0.4:
resolution: {integrity: sha512-Gs6RLjzlLRdT8X9ZipJdIZI/Y6/HhRLyq9RdDlCsnpxr/+Nn6bU2EFGuC94GjxqhM+Nmij2Vcq98yoHrU8uNFQ==}
eventemitter3@5.0.1:
resolution: {integrity: sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==}
@ -4067,6 +4128,11 @@ packages:
resolution: {integrity: sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==}
hasBin: true
jsondiffpatch@0.6.0:
resolution: {integrity: sha512-3QItJOXp2AP1uv7waBkao5nCvhEv+QmJAd38Ybq7wNI74Q+BBmnLn4EDKz6yI9xGAIQoUF87qHt+kc1IVxB4zQ==}
engines: {node: ^18.0.0 || >=20.0.0}
hasBin: true
jsonpointer@5.0.1:
resolution: {integrity: sha512-p/nXbhSEcu3pZRdkW1OfJhpsVtW1gd4Wa1fnQc9YLiTfAjn0312eMKimbdIQzuZl9aa9xUGaRlP9T/CJE/ditQ==}
engines: {node: '>=0.10.0'}
@ -4294,6 +4360,11 @@ packages:
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
hasBin: true
nanoid@5.1.5:
resolution: {integrity: sha512-Ir/+ZpE9fDsNH0hQ3C68uyThDXzYcim2EqcZ8zn8Chtt1iylPT9xXJB0kPCnqzgcEGikO9RxSrh63MsmVCU7Fw==}
engines: {node: ^18 || >=20}
hasBin: true
natural-compare@1.4.0:
resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==}
@ -4470,6 +4541,14 @@ packages:
resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==}
engines: {node: '>= 0.8'}
partyserver@0.0.71:
resolution: {integrity: sha512-PJZoX08tyNcNJVXqWJedZ6Jzj8EOFGBA/PJ37KhAnWmTkq6A8SqA4u2ol+zq8zwSfRy9FPvVgABCY0yLpe62Dg==}
peerDependencies:
'@cloudflare/workers-types': ^4.20240729.0
partysocket@1.1.4:
resolution: {integrity: sha512-jXP7PFj2h5/v4UjDS8P7MZy6NJUQ7sspiFyxL4uc/+oKOL+KdtXzHnTV8INPGxBrLTXgalyG3kd12Qm7WrYc3A==}
path-exists@4.0.0:
resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==}
engines: {node: '>=8'}
@ -4801,6 +4880,9 @@ packages:
scheduler@0.26.0:
resolution: {integrity: sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA==}
secure-json-parse@2.7.0:
resolution: {integrity: sha512-6aU+Rwsezw7VR8/nyvKTx8QpWH9FrcYiXXlqC4z5d5XQBDRqtbfsRjnwGyqbi3gddNtWHuEk9OANUotL26qKUw==}
selderee@0.11.0:
resolution: {integrity: sha512-5TF+l7p4+OsnP8BCCvSyZiSPc4x4//p5uPwK8TCnVPJYRmU2aYKMpOXvw8zM5a5JvuuCGN1jmsMwuU2W02ukfA==}
@ -5079,6 +5161,11 @@ packages:
resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==}
engines: {node: '>= 0.4'}
swr@2.3.3:
resolution: {integrity: sha512-dshNvs3ExOqtZ6kJBaAsabhPdHyeY4P2cKwRCniDVifBMoG/SVI7tfLWqPXriVspf2Rg4tPzXJTnwaihIeFw2A==}
peerDependencies:
react: ^16.11.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
thenify-all@1.6.0:
resolution: {integrity: sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==}
engines: {node: '>=0.8'}
@ -5086,6 +5173,10 @@ packages:
thenify@3.3.1:
resolution: {integrity: sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==}
throttleit@2.1.0:
resolution: {integrity: sha512-nt6AMGKW1p/70DF/hGBdJB57B8Tspmbp5gfJ8ilhLnt7kkr2ye7hzD6NVG8GGErk2HWF34igrL2CXmNIkzKqKw==}
engines: {node: '>=18'}
tinybench@2.9.0:
resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==}
@ -5318,6 +5409,11 @@ packages:
uri-templates@0.2.0:
resolution: {integrity: sha512-EWkjYEN0L6KOfEoOH6Wj4ghQqU7eBZMJqRHQnxQAq+dSEzRPClkWjf8557HkWQXF6BrAUoLSAyy9i3RVTliaNg==}
use-sync-external-store@1.5.0:
resolution: {integrity: sha512-Rb46I4cGGVBmjamjphe8L/UnvJD+uPPtTkNvX5mZgqdbavhI4EbgIWJiIHXJ8bc/i9EQGPRh4DwEURJ552Do0A==}
peerDependencies:
react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
uuid@8.3.2:
resolution: {integrity: sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==}
hasBin: true
@ -5598,6 +5694,34 @@ snapshots:
hono: 4.7.11
jose: 5.9.6
'@ai-sdk/provider-utils@2.2.8(zod@3.25.51)':
dependencies:
'@ai-sdk/provider': 1.1.3
nanoid: 3.3.11
secure-json-parse: 2.7.0
zod: 3.25.51
'@ai-sdk/provider@1.1.3':
dependencies:
json-schema: 0.4.0
'@ai-sdk/react@1.2.12(react@19.1.0)(zod@3.25.51)':
dependencies:
'@ai-sdk/provider-utils': 2.2.8(zod@3.25.51)
'@ai-sdk/ui-utils': 1.2.11(zod@3.25.51)
react: 19.1.0
swr: 2.3.3(react@19.1.0)
throttleit: 2.1.0
optionalDependencies:
zod: 3.25.51
'@ai-sdk/ui-utils@1.2.11(zod@3.25.51)':
dependencies:
'@ai-sdk/provider': 1.1.3
'@ai-sdk/provider-utils': 2.2.8(zod@3.25.51)
zod: 3.25.51
zod-to-json-schema: 3.24.5(zod@3.25.51)
'@apideck/better-ajv-errors@0.3.6(ajv@8.17.1)':
dependencies:
ajv: 8.17.1
@ -7069,6 +7193,8 @@ snapshots:
'@types/deep-eql@4.0.2': {}
'@types/diff-match-patch@1.0.36': {}
'@types/estree@1.0.7': {}
'@types/json-schema@7.0.15': {}
@ -7280,6 +7406,32 @@ snapshots:
agent-base@7.1.3: {}
agents@0.0.95(@cloudflare/workers-types@4.20250604.0)(react@19.1.0):
dependencies:
'@modelcontextprotocol/sdk': 1.12.1
ai: 4.3.16(react@19.1.0)(zod@3.25.51)
cron-schedule: 5.0.4
nanoid: 5.1.5
partyserver: 0.0.71(@cloudflare/workers-types@4.20250604.0)
partysocket: 1.1.4
react: 19.1.0
zod: 3.25.51
transitivePeerDependencies:
- '@cloudflare/workers-types'
- supports-color
ai@4.3.16(react@19.1.0)(zod@3.25.51):
dependencies:
'@ai-sdk/provider': 1.1.3
'@ai-sdk/provider-utils': 2.2.8(zod@3.25.51)
'@ai-sdk/react': 1.2.12(react@19.1.0)(zod@3.25.51)
'@ai-sdk/ui-utils': 1.2.11(zod@3.25.51)
'@opentelemetry/api': 1.9.0
jsondiffpatch: 0.6.0
zod: 3.25.51
optionalDependencies:
react: 19.1.0
ajv-formats@3.0.1(ajv@8.17.1):
optionalDependencies:
ajv: 8.17.1
@ -7617,6 +7769,8 @@ snapshots:
object-assign: 4.1.1
vary: 1.1.2
cron-schedule@5.0.4: {}
cross-spawn@7.0.6:
dependencies:
path-key: 3.1.1
@ -7716,8 +7870,12 @@ snapshots:
depd@2.0.0: {}
dequal@2.0.3: {}
detect-libc@2.0.4: {}
diff-match-patch@1.0.5: {}
doctrine@2.1.0:
dependencies:
esutils: 2.0.3
@ -8196,6 +8354,8 @@ snapshots:
etag@1.8.1: {}
event-target-polyfill@0.0.4: {}
eventemitter3@5.0.1: {}
eventid@2.0.1:
@ -8827,6 +8987,12 @@ snapshots:
dependencies:
minimist: 1.2.8
jsondiffpatch@0.6.0:
dependencies:
'@types/diff-match-patch': 1.0.36
chalk: 5.4.1
diff-match-patch: 1.0.5
jsonpointer@5.0.1: {}
jsx-ast-utils@3.3.5:
@ -9059,6 +9225,8 @@ snapshots:
nanoid@3.3.11: {}
nanoid@5.1.5: {}
natural-compare@1.4.0: {}
negotiator@0.6.3: {}
@ -9288,6 +9456,15 @@ snapshots:
parseurl@1.3.3: {}
partyserver@0.0.71(@cloudflare/workers-types@4.20250604.0):
dependencies:
'@cloudflare/workers-types': 4.20250604.0
nanoid: 5.1.5
partysocket@1.1.4:
dependencies:
event-target-polyfill: 0.0.4
path-exists@4.0.0: {}
path-key@3.1.1: {}
@ -9629,6 +9806,8 @@ snapshots:
scheduler@0.26.0: {}
secure-json-parse@2.7.0: {}
selderee@0.11.0:
dependencies:
parseley: 0.12.1
@ -9997,6 +10176,12 @@ snapshots:
supports-preserve-symlinks-flag@1.0.0: {}
swr@2.3.3(react@19.1.0):
dependencies:
dequal: 2.0.3
react: 19.1.0
use-sync-external-store: 1.5.0(react@19.1.0)
thenify-all@1.6.0:
dependencies:
thenify: 3.3.1
@ -10005,6 +10190,8 @@ snapshots:
dependencies:
any-promise: 1.3.0
throttleit@2.1.0: {}
tinybench@2.9.0: {}
tinyexec@0.3.2: {}
@ -10244,6 +10431,10 @@ snapshots:
uri-templates@0.2.0: {}
use-sync-external-store@1.5.0(react@19.1.0):
dependencies:
react: 19.1.0
uuid@8.3.2: {}
vary@1.1.2: {}