kopia lustrzana https://github.com/transitive-bullshit/chatgpt-api
feat: wip gateway mcp work
rodzic
b4d6af670e
commit
52101d15a4
|
@ -22,7 +22,7 @@
|
|||
* Useful to see console.logs
|
||||
*/
|
||||
"console": "integratedTerminal",
|
||||
"internalConsoleOptions": "openOnFirstSessionStart",
|
||||
"internalConsoleOptions": "neverOpen",
|
||||
|
||||
// Files to exclude from debugger (e.g. call stack)
|
||||
"skipFiles": [
|
||||
|
@ -33,5 +33,44 @@
|
|||
// "${workspaceFolder}/node_modules/**"
|
||||
]
|
||||
}
|
||||
// Wrangler's vscode support seems to be extremely buggy. It sometimes works
|
||||
// 1/10th of the time, but nothing I tried could improve that consistency.
|
||||
// Will use browser debugger instead for now.
|
||||
// {
|
||||
// "name": "gateway",
|
||||
// "type": "node",
|
||||
// "request": "attach",
|
||||
// "port": 9229,
|
||||
// "cwd": "${workspaceFolder}/apps/gateway",
|
||||
// // "cwd": "${workspaceFolder}",
|
||||
// // "cwd": "/",
|
||||
// "attachExistingChildren": false,
|
||||
// "autoAttachChildProcesses": false,
|
||||
// "sourceMaps": true,
|
||||
// "outFiles": ["${workspaceFolder}/apps/gateway/.wrangler/tmp/**/*"],
|
||||
// "resolveSourceMapLocations": null,
|
||||
// // "resolveSourceMapLocations": ["**", "!**/node_modules/**"],
|
||||
// "skipFiles": ["<node_internals>/**"],
|
||||
// "internalConsoleOptions": "neverOpen",
|
||||
// "restart": true
|
||||
// },
|
||||
// {
|
||||
// "name": "Wrangler",
|
||||
// "type": "node",
|
||||
// "request": "attach",
|
||||
// "port": 9229,
|
||||
// "cwd": "/",
|
||||
// "resolveSourceMapLocations": null,
|
||||
// "attachExistingChildren": false,
|
||||
// "autoAttachChildProcesses": false,
|
||||
// "sourceMaps": true // works with or without this line (supposedly)
|
||||
// }
|
||||
]
|
||||
// "compounds": [
|
||||
// {
|
||||
// "name": "Debug Workers",
|
||||
// "configurations": ["gateway"],
|
||||
// "stopAll": true
|
||||
// }
|
||||
// ]
|
||||
}
|
||||
|
|
|
@ -15,7 +15,9 @@
|
|||
"test": "run-s test:*",
|
||||
"test:lint": "eslint .",
|
||||
"test:typecheck": "tsc --noEmit",
|
||||
"test-e2e": "vitest run"
|
||||
"test-e2e": "vitest run",
|
||||
"test-http-e2e": "vitest run src/http-e2e.test.ts",
|
||||
"test-mcp-e2e": "vitest run src/mcp-e2e.test.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"ky": "catalog:",
|
||||
|
@ -26,6 +28,7 @@
|
|||
"@agentic/platform-api-client": "workspace:*",
|
||||
"@agentic/platform-core": "workspace:*",
|
||||
"@agentic/platform-fixtures": "workspace:*",
|
||||
"fast-content-type-parse": "catalog:"
|
||||
"fast-content-type-parse": "catalog:",
|
||||
"@modelcontextprotocol/sdk": "catalog:"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,7 +3,7 @@ import defaultKy from 'ky'
|
|||
import { describe, expect, test } from 'vitest'
|
||||
|
||||
import { env } from './env'
|
||||
import { fixtureSuites } from './fixtures'
|
||||
import { fixtureSuites } from './http-fixtures'
|
||||
|
||||
const ky = defaultKy.extend({
|
||||
prefixUrl: env.AGENTIC_GATEWAY_BASE_URL,
|
||||
|
@ -28,7 +28,8 @@ for (const [i, fixtureSuite] of fixtureSuites.entries()) {
|
|||
status = 200,
|
||||
contentType: expectedContentType = 'application/json',
|
||||
headers: expectedHeaders,
|
||||
body: expectedBody
|
||||
body: expectedBody,
|
||||
validate
|
||||
} = fixture.response ?? {}
|
||||
const snapshot =
|
||||
fixture.response?.snapshot ??
|
||||
|
@ -85,6 +86,10 @@ for (const [i, fixtureSuite] of fixtureSuites.entries()) {
|
|||
expect(body).toEqual(expectedBody)
|
||||
}
|
||||
|
||||
if (validate) {
|
||||
await Promise.resolve(validate(body))
|
||||
}
|
||||
|
||||
if (snapshot) {
|
||||
expect(body).toMatchSnapshot()
|
||||
}
|
|
@ -26,7 +26,7 @@ export type E2ETestFixture = {
|
|||
contentType?: string
|
||||
headers?: Record<string, string>
|
||||
body?: any
|
||||
validate?: (body: any) => void
|
||||
validate?: (body: any) => void | Promise<void>
|
||||
/** @default true */
|
||||
snapshot?: boolean
|
||||
}
|
|
@ -0,0 +1,89 @@
|
|||
import { Client as McpClient } from '@modelcontextprotocol/sdk/client/index.js'
|
||||
import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js'
|
||||
import { afterAll, beforeAll, describe, expect, test } from 'vitest'
|
||||
|
||||
import { env } from './env'
|
||||
import { fixtureSuites } from './mcp-fixtures'
|
||||
|
||||
for (const [i, fixtureSuite] of fixtureSuites.entries()) {
|
||||
const { title, fixtures, compareResponseBodies = false } = fixtureSuite
|
||||
|
||||
const describeFn = fixtureSuite.only ? describe.only : describe
|
||||
describeFn(title, () => {
|
||||
let fixtureResult: any | undefined
|
||||
|
||||
let client: McpClient
|
||||
beforeAll(async () => {
|
||||
client = new McpClient({
|
||||
name: fixtureSuite.path,
|
||||
version: '0.0.0'
|
||||
})
|
||||
|
||||
const transport = new StreamableHTTPClientTransport(
|
||||
new URL(fixtureSuite.path, env.AGENTIC_GATEWAY_BASE_URL)
|
||||
)
|
||||
await client.connect(transport)
|
||||
}, 120_000)
|
||||
|
||||
afterAll(async () => {
|
||||
await client.close()
|
||||
})
|
||||
|
||||
for (const [j, fixture] of fixtures.entries()) {
|
||||
const { isError, body: expectedBody, validate } = fixture.response ?? {}
|
||||
const snapshot =
|
||||
fixture.response?.snapshot ?? fixtureSuite.snapshot ?? !isError
|
||||
const debugFixture = !!(fixture.debug ?? fixtureSuite.debug)
|
||||
const fixtureName = `${i}.${j}: ${fixtureSuite.path} ${fixture.request.name}`
|
||||
|
||||
let testFn = fixture.only ? test.only : test
|
||||
if (fixtureSuite.sequential) {
|
||||
testFn = testFn.sequential
|
||||
}
|
||||
|
||||
testFn(
|
||||
fixtureName,
|
||||
{
|
||||
timeout: fixture.timeout ?? 60_000
|
||||
},
|
||||
// eslint-disable-next-line no-loop-func
|
||||
async () => {
|
||||
const { tools } = await client.listTools()
|
||||
console.log('tools', tools)
|
||||
expect(tools.map((t) => t.name)).toContain(fixture.request.name)
|
||||
|
||||
const result = await client.callTool(fixture.request)
|
||||
if (isError) {
|
||||
expect(result.isError).toBeTruthy()
|
||||
} else {
|
||||
expect(result.isError).toBeFalsy()
|
||||
}
|
||||
|
||||
if (expectedBody) {
|
||||
expect(result).toEqual(expectedBody)
|
||||
}
|
||||
|
||||
if (snapshot) {
|
||||
expect(result).toMatchSnapshot()
|
||||
}
|
||||
|
||||
if (validate) {
|
||||
await Promise.resolve(validate(result))
|
||||
}
|
||||
|
||||
if (compareResponseBodies && !isError) {
|
||||
if (!fixtureResult) {
|
||||
fixtureResult = result
|
||||
} else {
|
||||
expect(result).toEqual(fixtureResult)
|
||||
}
|
||||
}
|
||||
|
||||
if (debugFixture) {
|
||||
console.log(fixtureName, '=>', fixtureResult)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
|
@ -0,0 +1,110 @@
|
|||
export type MCPE2ETestFixture = {
|
||||
/** @default 60_000 milliseconds */
|
||||
timeout?: number
|
||||
|
||||
/** @default false */
|
||||
only?: boolean
|
||||
|
||||
/** @default false */
|
||||
debug?: boolean
|
||||
|
||||
request: {
|
||||
name: string
|
||||
args: Record<string, unknown>
|
||||
}
|
||||
|
||||
response?: {
|
||||
isError?: boolean
|
||||
body?: any
|
||||
validate?: (result: any) => void | Promise<void>
|
||||
/** @default true */
|
||||
snapshot?: boolean
|
||||
}
|
||||
}
|
||||
|
||||
export type MCPE2ETestFixtureSuite = {
|
||||
title: string
|
||||
path: string
|
||||
fixtures: MCPE2ETestFixture[]
|
||||
|
||||
/** @default false */
|
||||
only?: boolean
|
||||
|
||||
/** @default false */
|
||||
sequential?: boolean
|
||||
|
||||
/** @default false */
|
||||
compareResponseBodies?: boolean
|
||||
|
||||
/** @default false */
|
||||
debug?: boolean
|
||||
|
||||
/** @default undefined */
|
||||
snapshot?: boolean
|
||||
}
|
||||
|
||||
const now = Date.now()
|
||||
|
||||
export const fixtureSuites: MCPE2ETestFixtureSuite[] = [
|
||||
{
|
||||
title: 'Basic MCP => OpenAPI get_post success',
|
||||
path: '@dev/test-basic-openapi/mcp',
|
||||
fixtures: [
|
||||
{
|
||||
request: {
|
||||
name: 'get_post',
|
||||
args: {
|
||||
postId: 1
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
// {
|
||||
// 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}`
|
||||
// })
|
||||
// }
|
||||
// ]
|
||||
// }
|
||||
// }
|
||||
// ]
|
||||
// }
|
||||
]
|
|
@ -20,7 +20,6 @@
|
|||
"scripts": {
|
||||
"deploy": "wrangler deploy",
|
||||
"dev": "wrangler dev",
|
||||
"start": "wrangler dev",
|
||||
"cf-clear-cache": "del .wrangler",
|
||||
"clean": "del dist",
|
||||
"test": "run-s test:*",
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import type { AdminDeployment, PricingPlan } from '@agentic/platform-types'
|
||||
import { assert } from '@agentic/platform-core'
|
||||
import { parseDeploymentIdentifier } from '@agentic/platform-validators'
|
||||
import { parseToolIdentifier } from '@agentic/platform-validators'
|
||||
|
||||
import type { AdminConsumer, GatewayHonoContext } from './types'
|
||||
import { getAdminConsumer } from './get-admin-consumer'
|
||||
|
@ -16,7 +16,7 @@ export async function resolveMcpEdgeRequest(ctx: GatewayHonoContext): Promise<{
|
|||
const requestedDeploymentIdentifier = pathname
|
||||
.replace(/^\//, '')
|
||||
.replace(/\/$/, '')
|
||||
const { deploymentIdentifier } = parseDeploymentIdentifier(
|
||||
const { deploymentIdentifier } = parseToolIdentifier(
|
||||
requestedDeploymentIdentifier
|
||||
)
|
||||
|
||||
|
|
|
@ -231,6 +231,9 @@ export async function handleMcpRequest(ctx: GatewayHonoContext) {
|
|||
|
||||
// eslint-disable-next-line unicorn/prefer-add-event-listener
|
||||
transport.onmessage = async (message) => {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('onmessage', message)
|
||||
|
||||
// validate that the message is a valid JSONRPC message
|
||||
const result = JSONRPCMessageSchema.safeParse(message)
|
||||
if (!result.success) {
|
||||
|
@ -251,6 +254,7 @@ export async function handleMcpRequest(ctx: GatewayHonoContext) {
|
|||
// If we have received all the responses, close the connection
|
||||
if (!requestIds.size) {
|
||||
ctx.executionCtx.waitUntil(transport.close())
|
||||
await writer.close()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"extends": "@fisch0920/config/tsconfig-node",
|
||||
"compilerOptions": {
|
||||
"types": ["@cloudflare/workers-types/experimental"]
|
||||
"types": ["@cloudflare/workers-types"]
|
||||
},
|
||||
"include": ["src", "*.config.ts"],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
|
|
|
@ -43,6 +43,8 @@
|
|||
*/
|
||||
"placement": { "mode": "smart" },
|
||||
|
||||
"upload_source_maps": true,
|
||||
|
||||
/**
|
||||
* Bindings
|
||||
* Bindings allow your Worker to interact with resources on the Cloudflare Developer Platform, including
|
||||
|
|
|
@ -68,7 +68,7 @@ export function errorHandler(
|
|||
}
|
||||
|
||||
/** Error codes defined by the JSON-RPC specification. */
|
||||
export declare enum JsonRpcErrorCodes {
|
||||
export enum JsonRpcErrorCodes {
|
||||
ConnectionClosed = -32_000,
|
||||
RequestTimeout = -32_001,
|
||||
ParseError = -32_700,
|
||||
|
|
|
@ -397,6 +397,9 @@ importers:
|
|||
'@agentic/platform-fixtures':
|
||||
specifier: workspace:*
|
||||
version: link:../../packages/fixtures
|
||||
'@modelcontextprotocol/sdk':
|
||||
specifier: 'catalog:'
|
||||
version: 1.12.1
|
||||
fast-content-type-parse:
|
||||
specifier: 'catalog:'
|
||||
version: 3.0.0
|
||||
|
|
Ładowanie…
Reference in New Issue