kopia lustrzana https://github.com/transitive-bullshit/chatgpt-api
pull/715/head
rodzic
077a88bb90
commit
b97a210602
|
@ -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: {
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
|
||||||
}
|
|
|
@ -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'
|
|
||||||
)
|
|
||||||
}
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
Ładowanie…
Reference in New Issue