kopia lustrzana https://github.com/transitive-bullshit/chatgpt-api
pull/715/head
rodzic
4b298ccdbf
commit
077a88bb90
|
@ -1,5 +1,5 @@
|
||||||
import type { AdminDeployment, PricingPlan } from '@agentic/platform-types'
|
import type { AdminDeployment, PricingPlan } from '@agentic/platform-types'
|
||||||
import { assert, HttpError } from '@agentic/platform-core'
|
import { assert } from '@agentic/platform-core'
|
||||||
import { parseDeploymentIdentifier } from '@agentic/platform-validators'
|
import { parseDeploymentIdentifier } from '@agentic/platform-validators'
|
||||||
// import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'
|
// import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'
|
||||||
import { Server } from '@modelcontextprotocol/sdk/server/index.js'
|
import { Server } from '@modelcontextprotocol/sdk/server/index.js'
|
||||||
|
@ -8,7 +8,6 @@ import {
|
||||||
ListToolsRequestSchema
|
ListToolsRequestSchema
|
||||||
} from '@modelcontextprotocol/sdk/types.js'
|
} from '@modelcontextprotocol/sdk/types.js'
|
||||||
import { McpAgent } from 'agents/mcp'
|
import { McpAgent } from 'agents/mcp'
|
||||||
import contentType from 'fast-content-type-parse'
|
|
||||||
|
|
||||||
import type { RawEnv } from './env'
|
import type { RawEnv } from './env'
|
||||||
import type {
|
import type {
|
||||||
|
@ -18,15 +17,16 @@ import type {
|
||||||
} from './types'
|
} from './types'
|
||||||
import { cfValidateJsonSchema } from './cf-validate-json-schema'
|
import { cfValidateJsonSchema } from './cf-validate-json-schema'
|
||||||
import { createRequestForOpenAPIOperation } from './create-request-for-openapi-operation'
|
import { createRequestForOpenAPIOperation } from './create-request-for-openapi-operation'
|
||||||
|
import { transformHttpResponseToMcpToolCallResponse } from './transform-http-response-to-mcp-tool-call-response'
|
||||||
// import { fetchCache } from './fetch-cache'
|
// import { fetchCache } from './fetch-cache'
|
||||||
// import { getRequestCacheKey } from './get-request-cache-key'
|
// import { getRequestCacheKey } from './get-request-cache-key'
|
||||||
import { updateOriginRequest } from './update-origin-request'
|
import { updateOriginRequest } from './update-origin-request'
|
||||||
|
|
||||||
type State = { counter: number }
|
// type State = { counter: number }
|
||||||
|
|
||||||
export class DurableMcpServer extends McpAgent<
|
export class DurableMcpServer extends McpAgent<
|
||||||
RawEnv,
|
RawEnv,
|
||||||
State,
|
never, // TODO: do we need local state?
|
||||||
{
|
{
|
||||||
deployment: AdminDeployment
|
deployment: AdminDeployment
|
||||||
consumer?: AdminConsumer
|
consumer?: AdminConsumer
|
||||||
|
@ -36,9 +36,9 @@ export class DurableMcpServer extends McpAgent<
|
||||||
protected _serverP = Promise.withResolvers<Server>()
|
protected _serverP = Promise.withResolvers<Server>()
|
||||||
override server = this._serverP.promise
|
override server = this._serverP.promise
|
||||||
|
|
||||||
override initialState: State = {
|
// override initialState: State = {
|
||||||
counter: 1
|
// counter: 1
|
||||||
}
|
// }
|
||||||
|
|
||||||
override async init() {
|
override async init() {
|
||||||
const { consumer, deployment, pricingPlan } = this.props
|
const { consumer, deployment, pricingPlan } = this.props
|
||||||
|
@ -131,54 +131,7 @@ export class DurableMcpServer extends McpAgent<
|
||||||
|
|
||||||
updateOriginRequest(originRequest, { consumer, deployment })
|
updateOriginRequest(originRequest, { consumer, deployment })
|
||||||
|
|
||||||
const originResponse = await fetch(originRequest)
|
// TODO: re-add caching support
|
||||||
|
|
||||||
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)
|
// const cacheKey = await getRequestCacheKey(ctx, originRequest)
|
||||||
|
|
||||||
// // TODO: transform origin 5XX errors to 502 errors...
|
// // TODO: transform origin 5XX errors to 502 errors...
|
||||||
|
@ -188,69 +141,14 @@ export class DurableMcpServer extends McpAgent<
|
||||||
// fetchResponse: () => fetch(originRequest)
|
// fetchResponse: () => fetch(originRequest)
|
||||||
// })
|
// })
|
||||||
|
|
||||||
if (tool.outputSchema) {
|
const originResponse = await fetch(originRequest)
|
||||||
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({
|
return transformHttpResponseToMcpToolCallResponse({
|
||||||
schema: tool.outputSchema,
|
originRequest,
|
||||||
data: res as Record<string, unknown>,
|
originResponse,
|
||||||
coerce: false,
|
tool,
|
||||||
// TODO: double-check MCP schema on whether additional properties are allowed
|
toolCallArgs
|
||||||
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') {
|
} else if (originAdapter.type === 'mcp') {
|
||||||
const sessionId = this.ctx.id.toString()
|
const sessionId = this.ctx.id.toString()
|
||||||
const id: DurableObjectId =
|
const id: DurableObjectId =
|
||||||
|
@ -308,8 +206,8 @@ export class DurableMcpServer extends McpAgent<
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
override onStateUpdate(state: State) {
|
// override onStateUpdate(state: State) {
|
||||||
// eslint-disable-next-line no-console
|
// // eslint-disable-next-line no-console
|
||||||
console.log({ stateUpdate: state })
|
// console.log({ stateUpdate: state })
|
||||||
}
|
// }
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,126 @@
|
||||||
|
import type { Tool } from '@agentic/platform-types'
|
||||||
|
import { assert, HttpError } from '@agentic/platform-core'
|
||||||
|
import contentType from 'fast-content-type-parse'
|
||||||
|
|
||||||
|
import type { McpToolCallResponse, ToolCallArgs } from './types'
|
||||||
|
import { cfValidateJsonSchema } from './cf-validate-json-schema'
|
||||||
|
|
||||||
|
export async function transformHttpResponseToMcpToolCallResponse({
|
||||||
|
originRequest,
|
||||||
|
originResponse,
|
||||||
|
tool,
|
||||||
|
toolCallArgs
|
||||||
|
}: {
|
||||||
|
originRequest: Request
|
||||||
|
originResponse: Response
|
||||||
|
tool: Tool
|
||||||
|
toolCallArgs: ToolCallArgs
|
||||||
|
}) {
|
||||||
|
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
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
Ładowanie…
Reference in New Issue