@copilotkit/aimock 1.43.0 → 1.45.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/CHANGELOG.md +46 -0
- package/README.md +14 -2
- package/dist/agui-handler.d.ts.map +1 -1
- package/dist/aimock-cli.cjs +31 -5
- package/dist/aimock-cli.cjs.map +1 -1
- package/dist/aimock-cli.d.cts.map +1 -1
- package/dist/aimock-cli.d.ts.map +1 -1
- package/dist/aimock-cli.js +31 -5
- package/dist/aimock-cli.js.map +1 -1
- package/dist/bedrock-converse.cjs +344 -43
- package/dist/bedrock-converse.cjs.map +1 -1
- package/dist/bedrock-converse.d.cts.map +1 -1
- package/dist/bedrock-converse.d.ts.map +1 -1
- package/dist/bedrock-converse.js +345 -44
- package/dist/bedrock-converse.js.map +1 -1
- package/dist/bedrock.cjs +195 -42
- package/dist/bedrock.cjs.map +1 -1
- package/dist/bedrock.d.cts.map +1 -1
- package/dist/bedrock.d.ts.map +1 -1
- package/dist/bedrock.js +196 -43
- package/dist/bedrock.js.map +1 -1
- package/dist/chaos.cjs +9 -5
- package/dist/chaos.cjs.map +1 -1
- package/dist/chaos.d.cts.map +1 -1
- package/dist/chaos.d.ts.map +1 -1
- package/dist/chaos.js +10 -7
- package/dist/chaos.js.map +1 -1
- package/dist/cli.cjs +238 -9
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +242 -13
- package/dist/cli.js.map +1 -1
- package/dist/cohere.cjs +159 -72
- package/dist/cohere.cjs.map +1 -1
- package/dist/cohere.d.cts.map +1 -1
- package/dist/cohere.d.ts.map +1 -1
- package/dist/cohere.js +160 -73
- package/dist/cohere.js.map +1 -1
- package/dist/config-loader.cjs +63 -5
- package/dist/config-loader.cjs.map +1 -1
- package/dist/config-loader.d.cts +37 -4
- package/dist/config-loader.d.cts.map +1 -1
- package/dist/config-loader.d.ts +37 -4
- package/dist/config-loader.d.ts.map +1 -1
- package/dist/config-loader.js +63 -5
- package/dist/config-loader.js.map +1 -1
- package/dist/constants.cjs +8 -0
- package/dist/constants.cjs.map +1 -1
- package/dist/constants.d.cts +6 -2
- package/dist/constants.d.ts +6 -2
- package/dist/constants.js +8 -1
- package/dist/constants.js.map +1 -1
- package/dist/echo-text.cjs +65 -0
- package/dist/echo-text.cjs.map +1 -0
- package/dist/echo-text.d.cts +22 -0
- package/dist/echo-text.d.cts.map +1 -0
- package/dist/echo-text.d.ts +22 -0
- package/dist/echo-text.d.ts.map +1 -0
- package/dist/echo-text.js +65 -0
- package/dist/echo-text.js.map +1 -0
- package/dist/embeddings.cjs +18 -1
- package/dist/embeddings.cjs.map +1 -1
- package/dist/embeddings.d.cts +1 -1
- package/dist/embeddings.d.cts.map +1 -1
- package/dist/embeddings.d.ts +1 -1
- package/dist/embeddings.d.ts.map +1 -1
- package/dist/embeddings.js +18 -1
- package/dist/embeddings.js.map +1 -1
- package/dist/fakes-plugin.cjs +131 -0
- package/dist/fakes-plugin.cjs.map +1 -0
- package/dist/fakes-plugin.d.cts +23 -0
- package/dist/fakes-plugin.d.cts.map +1 -0
- package/dist/fakes-plugin.d.ts +23 -0
- package/dist/fakes-plugin.d.ts.map +1 -0
- package/dist/fakes-plugin.js +127 -0
- package/dist/fakes-plugin.js.map +1 -0
- package/dist/files.cjs +9 -11
- package/dist/files.cjs.map +1 -1
- package/dist/files.d.cts.map +1 -1
- package/dist/files.d.ts.map +1 -1
- package/dist/files.js +10 -13
- package/dist/files.js.map +1 -1
- package/dist/fine-tuning.cjs +1 -19
- package/dist/fine-tuning.cjs.map +1 -1
- package/dist/fine-tuning.d.cts.map +1 -1
- package/dist/fine-tuning.d.ts.map +1 -1
- package/dist/fine-tuning.js +2 -20
- package/dist/fine-tuning.js.map +1 -1
- package/dist/fixture-loader-services.cjs +116 -0
- package/dist/fixture-loader-services.cjs.map +1 -0
- package/dist/fixture-loader-services.d.cts +48 -0
- package/dist/fixture-loader-services.d.cts.map +1 -0
- package/dist/fixture-loader-services.d.ts +48 -0
- package/dist/fixture-loader-services.d.ts.map +1 -0
- package/dist/fixture-loader-services.js +114 -0
- package/dist/fixture-loader-services.js.map +1 -0
- package/dist/fixture-loader.cjs +552 -29
- package/dist/fixture-loader.cjs.map +1 -1
- package/dist/fixture-loader.d.cts +140 -2
- package/dist/fixture-loader.d.cts.map +1 -1
- package/dist/fixture-loader.d.ts +140 -2
- package/dist/fixture-loader.d.ts.map +1 -1
- package/dist/fixture-loader.js +542 -31
- package/dist/fixture-loader.js.map +1 -1
- package/dist/gemini-embeddings.cjs +42 -2
- package/dist/gemini-embeddings.cjs.map +1 -1
- package/dist/gemini-embeddings.js +43 -3
- package/dist/gemini-embeddings.js.map +1 -1
- package/dist/gemini-interactions.cjs +288 -52
- package/dist/gemini-interactions.cjs.map +1 -1
- package/dist/gemini-interactions.d.cts.map +1 -1
- package/dist/gemini-interactions.d.ts.map +1 -1
- package/dist/gemini-interactions.js +289 -53
- package/dist/gemini-interactions.js.map +1 -1
- package/dist/gemini.cjs +234 -38
- package/dist/gemini.cjs.map +1 -1
- package/dist/gemini.d.cts.map +1 -1
- package/dist/gemini.d.ts.map +1 -1
- package/dist/gemini.js +235 -39
- package/dist/gemini.js.map +1 -1
- package/dist/helpers.cjs +1157 -12
- package/dist/helpers.cjs.map +1 -1
- package/dist/helpers.d.cts +85 -4
- package/dist/helpers.d.cts.map +1 -1
- package/dist/helpers.d.ts +85 -4
- package/dist/helpers.d.ts.map +1 -1
- package/dist/helpers.js +1113 -14
- package/dist/helpers.js.map +1 -1
- package/dist/images.cjs +3 -2
- package/dist/images.cjs.map +1 -1
- package/dist/images.js +3 -2
- package/dist/images.js.map +1 -1
- package/dist/index.cjs +39 -1
- package/dist/index.d.cts +11 -4
- package/dist/index.d.ts +11 -4
- package/dist/index.js +11 -4
- package/dist/jest.cjs +43 -14
- package/dist/jest.cjs.map +1 -1
- package/dist/jest.d.cts +47 -5
- package/dist/jest.d.cts.map +1 -1
- package/dist/jest.d.ts +47 -5
- package/dist/jest.d.ts.map +1 -1
- package/dist/jest.js +45 -16
- package/dist/jest.js.map +1 -1
- package/dist/journal.cjs +155 -13
- package/dist/journal.cjs.map +1 -1
- package/dist/journal.d.cts +54 -4
- package/dist/journal.d.cts.map +1 -1
- package/dist/journal.d.ts +54 -4
- package/dist/journal.d.ts.map +1 -1
- package/dist/journal.js +155 -13
- package/dist/journal.js.map +1 -1
- package/dist/live-sanitize.cjs +4 -32
- package/dist/live-sanitize.cjs.map +1 -1
- package/dist/live-sanitize.js +4 -32
- package/dist/live-sanitize.js.map +1 -1
- package/dist/llmock.cjs +204 -39
- package/dist/llmock.cjs.map +1 -1
- package/dist/llmock.d.cts +51 -1
- package/dist/llmock.d.cts.map +1 -1
- package/dist/llmock.d.ts +51 -1
- package/dist/llmock.d.ts.map +1 -1
- package/dist/llmock.js +206 -41
- package/dist/llmock.js.map +1 -1
- package/dist/mcp-fakes-mount.cjs +217 -0
- package/dist/mcp-fakes-mount.cjs.map +1 -0
- package/dist/mcp-fakes-mount.js +211 -0
- package/dist/mcp-fakes-mount.js.map +1 -0
- package/dist/mcp-fakes-report.cjs +111 -0
- package/dist/mcp-fakes-report.cjs.map +1 -0
- package/dist/mcp-fakes-report.d.cts +40 -0
- package/dist/mcp-fakes-report.d.cts.map +1 -0
- package/dist/mcp-fakes-report.d.ts +40 -0
- package/dist/mcp-fakes-report.d.ts.map +1 -0
- package/dist/mcp-fakes-report.js +107 -0
- package/dist/mcp-fakes-report.js.map +1 -0
- package/dist/mcp-fakes.cjs +1942 -0
- package/dist/mcp-fakes.cjs.map +1 -0
- package/dist/mcp-fakes.d.cts +504 -0
- package/dist/mcp-fakes.d.cts.map +1 -0
- package/dist/mcp-fakes.d.ts +504 -0
- package/dist/mcp-fakes.d.ts.map +1 -0
- package/dist/mcp-fakes.js +1937 -0
- package/dist/mcp-fakes.js.map +1 -0
- package/dist/mcp-handler.cjs +534 -27
- package/dist/mcp-handler.cjs.map +1 -1
- package/dist/mcp-handler.d.cts +49 -0
- package/dist/mcp-handler.d.cts.map +1 -0
- package/dist/mcp-handler.d.ts +49 -0
- package/dist/mcp-handler.d.ts.map +1 -0
- package/dist/mcp-handler.js +530 -28
- package/dist/mcp-handler.js.map +1 -1
- package/dist/mcp-mock.cjs +326 -47
- package/dist/mcp-mock.cjs.map +1 -1
- package/dist/mcp-mock.d.cts +97 -1
- package/dist/mcp-mock.d.cts.map +1 -1
- package/dist/mcp-mock.d.ts +97 -1
- package/dist/mcp-mock.d.ts.map +1 -1
- package/dist/mcp-mock.js +327 -48
- package/dist/mcp-mock.js.map +1 -1
- package/dist/mcp-recorder.cjs +1224 -0
- package/dist/mcp-recorder.cjs.map +1 -0
- package/dist/mcp-recorder.d.cts +108 -0
- package/dist/mcp-recorder.d.cts.map +1 -0
- package/dist/mcp-recorder.d.ts +108 -0
- package/dist/mcp-recorder.d.ts.map +1 -0
- package/dist/mcp-recorder.js +1215 -0
- package/dist/mcp-recorder.js.map +1 -0
- package/dist/mcp-stub.cjs +5 -1
- package/dist/mcp-stub.d.cts +3 -1
- package/dist/mcp-stub.d.ts +3 -1
- package/dist/mcp-stub.js +3 -1
- package/dist/mcp-types.d.cts +6 -0
- package/dist/mcp-types.d.ts +6 -0
- package/dist/message-text.cjs +192 -0
- package/dist/message-text.cjs.map +1 -0
- package/dist/message-text.d.cts +31 -0
- package/dist/message-text.d.cts.map +1 -0
- package/dist/message-text.d.ts +31 -0
- package/dist/message-text.d.ts.map +1 -0
- package/dist/message-text.js +184 -0
- package/dist/message-text.js.map +1 -0
- package/dist/messages.cjs +400 -56
- package/dist/messages.cjs.map +1 -1
- package/dist/messages.d.cts.map +1 -1
- package/dist/messages.d.ts.map +1 -1
- package/dist/messages.js +401 -58
- package/dist/messages.js.map +1 -1
- package/dist/metrics.cjs +46 -66
- package/dist/metrics.cjs.map +1 -1
- package/dist/metrics.d.cts +14 -6
- package/dist/metrics.d.cts.map +1 -1
- package/dist/metrics.d.ts +14 -6
- package/dist/metrics.d.ts.map +1 -1
- package/dist/metrics.js +32 -40
- package/dist/metrics.js.map +1 -1
- package/dist/misbehavior.cjs +1523 -0
- package/dist/misbehavior.cjs.map +1 -0
- package/dist/misbehavior.d.cts +43 -0
- package/dist/misbehavior.d.cts.map +1 -0
- package/dist/misbehavior.d.ts +43 -0
- package/dist/misbehavior.d.ts.map +1 -0
- package/dist/misbehavior.js +1511 -0
- package/dist/misbehavior.js.map +1 -0
- package/dist/ollama.cjs +315 -113
- package/dist/ollama.cjs.map +1 -1
- package/dist/ollama.d.cts.map +1 -1
- package/dist/ollama.d.ts.map +1 -1
- package/dist/ollama.js +316 -114
- package/dist/ollama.js.map +1 -1
- package/dist/openapi.cjs +3405 -0
- package/dist/openapi.cjs.map +1 -0
- package/dist/openapi.d.cts +30 -0
- package/dist/openapi.d.cts.map +1 -0
- package/dist/openapi.d.ts +30 -0
- package/dist/openapi.d.ts.map +1 -0
- package/dist/openapi.js +3404 -0
- package/dist/openapi.js.map +1 -0
- package/dist/openrouter-video.cjs +4 -4
- package/dist/openrouter-video.cjs.map +1 -1
- package/dist/openrouter-video.d.cts.map +1 -1
- package/dist/openrouter-video.d.ts.map +1 -1
- package/dist/openrouter-video.js +4 -4
- package/dist/openrouter-video.js.map +1 -1
- package/dist/record-sanitize.cjs +231 -0
- package/dist/record-sanitize.cjs.map +1 -0
- package/dist/record-sanitize.js +224 -0
- package/dist/record-sanitize.js.map +1 -0
- package/dist/recorder.cjs +115 -6
- package/dist/recorder.cjs.map +1 -1
- package/dist/recorder.d.cts +2 -1
- package/dist/recorder.d.cts.map +1 -1
- package/dist/recorder.d.ts +2 -1
- package/dist/recorder.d.ts.map +1 -1
- package/dist/recorder.js +116 -8
- package/dist/recorder.js.map +1 -1
- package/dist/responses.cjs +701 -166
- package/dist/responses.cjs.map +1 -1
- package/dist/responses.d.cts +5 -13
- package/dist/responses.d.cts.map +1 -1
- package/dist/responses.d.ts +5 -13
- package/dist/responses.d.ts.map +1 -1
- package/dist/responses.js +699 -169
- package/dist/responses.js.map +1 -1
- package/dist/route-registry.cjs +951 -0
- package/dist/route-registry.cjs.map +1 -0
- package/dist/route-registry.d.cts +33 -0
- package/dist/route-registry.d.cts.map +1 -0
- package/dist/route-registry.d.ts +33 -0
- package/dist/route-registry.d.ts.map +1 -0
- package/dist/route-registry.js +866 -0
- package/dist/route-registry.js.map +1 -0
- package/dist/router.cjs +24 -2
- package/dist/router.cjs.map +1 -1
- package/dist/router.d.cts.map +1 -1
- package/dist/router.d.ts.map +1 -1
- package/dist/router.js +25 -4
- package/dist/router.js.map +1 -1
- package/dist/server.cjs +1159 -264
- package/dist/server.cjs.map +1 -1
- package/dist/server.d.cts +4 -1
- package/dist/server.d.cts.map +1 -1
- package/dist/server.d.ts +4 -1
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +1067 -172
- package/dist/server.js.map +1 -1
- package/dist/speech.cjs +4 -2
- package/dist/speech.cjs.map +1 -1
- package/dist/speech.js +4 -2
- package/dist/speech.js.map +1 -1
- package/dist/stream-collapse.cjs +236 -34
- package/dist/stream-collapse.cjs.map +1 -1
- package/dist/stream-collapse.d.cts +39 -4
- package/dist/stream-collapse.d.cts.map +1 -1
- package/dist/stream-collapse.d.ts +39 -4
- package/dist/stream-collapse.d.ts.map +1 -1
- package/dist/stream-collapse.js +235 -35
- package/dist/stream-collapse.js.map +1 -1
- package/dist/suite.cjs +1 -1
- package/dist/suite.js +1 -1
- package/dist/transcription.cjs +2 -2
- package/dist/transcription.cjs.map +1 -1
- package/dist/transcription.d.cts.map +1 -1
- package/dist/transcription.d.ts.map +1 -1
- package/dist/transcription.js +2 -2
- package/dist/transcription.js.map +1 -1
- package/dist/types.cjs +18 -0
- package/dist/types.cjs.map +1 -0
- package/dist/types.d.cts +516 -5
- package/dist/types.d.cts.map +1 -1
- package/dist/types.d.ts +516 -5
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +17 -0
- package/dist/types.js.map +1 -0
- package/dist/validate-cli.cjs +123 -33
- package/dist/validate-cli.cjs.map +1 -1
- package/dist/validate-cli.js +125 -35
- package/dist/validate-cli.js.map +1 -1
- package/dist/vector-stores.cjs +1180 -0
- package/dist/vector-stores.cjs.map +1 -0
- package/dist/vector-stores.d.cts +106 -0
- package/dist/vector-stores.d.cts.map +1 -0
- package/dist/vector-stores.d.ts +106 -0
- package/dist/vector-stores.d.ts.map +1 -0
- package/dist/vector-stores.js +1164 -0
- package/dist/vector-stores.js.map +1 -0
- package/dist/vector-types.d.cts.map +1 -1
- package/dist/version.cjs +45 -0
- package/dist/version.cjs.map +1 -0
- package/dist/version.js +44 -0
- package/dist/version.js.map +1 -0
- package/dist/video.cjs +19 -0
- package/dist/video.cjs.map +1 -1
- package/dist/video.d.cts.map +1 -1
- package/dist/video.d.ts.map +1 -1
- package/dist/video.js +19 -0
- package/dist/video.js.map +1 -1
- package/dist/vitest.cjs +77 -16
- package/dist/vitest.cjs.map +1 -1
- package/dist/vitest.d.cts +38 -5
- package/dist/vitest.d.cts.map +1 -1
- package/dist/vitest.d.ts +38 -5
- package/dist/vitest.d.ts.map +1 -1
- package/dist/vitest.js +78 -17
- package/dist/vitest.js.map +1 -1
- package/dist/watcher.cjs +6 -2
- package/dist/watcher.cjs.map +1 -1
- package/dist/watcher.js +6 -2
- package/dist/watcher.js.map +1 -1
- package/dist/ws-gemini-live.cjs +210 -101
- package/dist/ws-gemini-live.cjs.map +1 -1
- package/dist/ws-gemini-live.d.cts +9 -3
- package/dist/ws-gemini-live.d.cts.map +1 -1
- package/dist/ws-gemini-live.d.ts +9 -3
- package/dist/ws-gemini-live.d.ts.map +1 -1
- package/dist/ws-gemini-live.js +211 -102
- package/dist/ws-gemini-live.js.map +1 -1
- package/dist/ws-realtime.cjs +175 -62
- package/dist/ws-realtime.cjs.map +1 -1
- package/dist/ws-realtime.d.cts +8 -3
- package/dist/ws-realtime.d.cts.map +1 -1
- package/dist/ws-realtime.d.ts +8 -3
- package/dist/ws-realtime.d.ts.map +1 -1
- package/dist/ws-realtime.js +176 -63
- package/dist/ws-realtime.js.map +1 -1
- package/dist/ws-responses.cjs +121 -16
- package/dist/ws-responses.cjs.map +1 -1
- package/dist/ws-responses.d.cts +8 -12
- package/dist/ws-responses.d.cts.map +1 -1
- package/dist/ws-responses.d.ts +8 -12
- package/dist/ws-responses.d.ts.map +1 -1
- package/dist/ws-responses.js +123 -18
- package/dist/ws-responses.js.map +1 -1
- package/package.json +13 -2
- package/skills/write-fixtures/SKILL.md +179 -21
package/dist/mcp-handler.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"mcp-handler.js","names":[],"sources":["../src/mcp-handler.ts"],"sourcesContent":["import type * as http from \"node:http\";\nimport { randomUUID } from \"node:crypto\";\nimport { createJsonRpcDispatcher } from \"./jsonrpc.js\";\nimport type {\n MCPToolDefinition,\n MCPResourceDefinition,\n MCPResourceContent,\n MCPPromptDefinition,\n MCPPromptResult,\n MCPContent,\n MCPSession,\n} from \"./mcp-types.js\";\n\nexport interface MCPState {\n serverInfo: { name: string; version: string };\n tools: Map<string, { def: MCPToolDefinition; handler?: (...args: unknown[]) => unknown }>;\n resources: Map<string, { def: MCPResourceDefinition; content?: MCPResourceContent }>;\n prompts: Map<\n string,\n {\n def: MCPPromptDefinition;\n handler?: (...args: unknown[]) => MCPPromptResult | Promise<MCPPromptResult>;\n }\n >;\n sessions: Map<string, MCPSession>;\n}\n\nfunction jsonRpcResult(id: string | number, result: unknown) {\n return { jsonrpc: \"2.0\" as const, id, result };\n}\n\nfunction jsonRpcError(id: string | number | null, code: number, message: string) {\n return { jsonrpc: \"2.0\" as const, id, error: { code, message } };\n}\n\nexport function createMCPRequestHandler(state: MCPState) {\n const dispatcher = createJsonRpcDispatcher({\n methods: {\n // initialize is handled directly in the outer function — this entry is\n // only here so the dispatcher doesn't return \"Method not found\" if the\n // request somehow reaches it.\n initialize: async (_params, id) => {\n return jsonRpcResult(id, {\n protocolVersion: \"2025-03-26\",\n capabilities: { tools: {}, resources: {}, prompts: {} },\n serverInfo: state.serverInfo,\n });\n },\n\n \"notifications/initialized\": async (_params, _id, req) => {\n const sessionId = req.headers[\"mcp-session-id\"] as string;\n const session = state.sessions.get(sessionId);\n if (session) {\n session.initialized = true;\n }\n return null;\n },\n\n ping: async (_params, id) => {\n return jsonRpcResult(id, {});\n },\n\n \"tools/list\": async (_params, id) => {\n const tools: MCPToolDefinition[] = [];\n for (const { def } of state.tools.values()) {\n tools.push(def);\n }\n return jsonRpcResult(id, { tools });\n },\n\n \"tools/call\": async (params, id) => {\n const { name, arguments: args } = (params ?? {}) as { name?: string; arguments?: unknown };\n if (!name) {\n return jsonRpcError(id, -32602, \"Missing tool name\");\n }\n const entry = state.tools.get(name);\n if (!entry) {\n return jsonRpcError(id, -32602, `Unknown tool: ${name}`);\n }\n if (entry.handler) {\n try {\n const result = await entry.handler(args);\n const content: MCPContent[] = Array.isArray(result)\n ? result\n : [{ type: \"text\", text: String(result) }];\n return jsonRpcResult(id, { content, isError: false });\n } catch (err: unknown) {\n const message = err instanceof Error ? err.message : String(err);\n return jsonRpcResult(id, {\n content: [{ type: \"text\", text: message }],\n isError: true,\n });\n }\n }\n // No handler — return empty content\n return jsonRpcResult(id, { content: [], isError: false });\n },\n\n \"resources/list\": async (_params, id) => {\n const resources: MCPResourceDefinition[] = [];\n for (const { def } of state.resources.values()) {\n resources.push(def);\n }\n return jsonRpcResult(id, { resources });\n },\n\n \"resources/read\": async (params, id) => {\n const { uri } = (params ?? {}) as { uri?: string };\n if (!uri) {\n return jsonRpcError(id, -32602, \"Missing resource URI\");\n }\n const entry = state.resources.get(uri);\n if (!entry) {\n return jsonRpcError(id, -32602, `Unknown resource: ${uri}`);\n }\n return jsonRpcResult(id, {\n contents: [\n {\n uri,\n ...(entry.content?.text !== undefined && { text: entry.content.text }),\n ...(entry.content?.blob !== undefined && { blob: entry.content.blob }),\n ...(entry.content?.mimeType !== undefined && { mimeType: entry.content.mimeType }),\n },\n ],\n });\n },\n\n \"prompts/list\": async (_params, id) => {\n const prompts: MCPPromptDefinition[] = [];\n for (const { def } of state.prompts.values()) {\n prompts.push(def);\n }\n return jsonRpcResult(id, { prompts });\n },\n\n \"prompts/get\": async (params, id) => {\n const { name, arguments: args } = (params ?? {}) as { name?: string; arguments?: unknown };\n if (!name) {\n return jsonRpcError(id, -32602, \"Missing prompt name\");\n }\n const entry = state.prompts.get(name);\n if (!entry) {\n return jsonRpcError(id, -32602, `Unknown prompt: ${name}`);\n }\n if (entry.handler) {\n try {\n const result = await entry.handler(args);\n return jsonRpcResult(id, result);\n } catch (err: unknown) {\n const message = err instanceof Error ? err.message : String(err);\n return jsonRpcError(id, -32603, `Prompt handler error: ${message}`);\n }\n }\n // No handler — return empty messages\n return jsonRpcResult(id, { messages: [] });\n },\n },\n });\n\n return async (\n req: http.IncomingMessage,\n res: http.ServerResponse,\n body: string,\n ): Promise<void> => {\n // DELETE handler: session teardown\n if (req.method === \"DELETE\") {\n const sessionId = req.headers[\"mcp-session-id\"] as string | undefined;\n if (!sessionId) {\n res.writeHead(400, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify({ error: \"Missing mcp-session-id header\" }));\n return;\n }\n if (!state.sessions.has(sessionId)) {\n res.writeHead(404, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify({ error: \"Session not found\" }));\n return;\n }\n state.sessions.delete(sessionId);\n res.writeHead(200, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify({ ok: true }));\n return;\n }\n\n // Parse the body to determine method for session validation\n let parsed: unknown;\n try {\n parsed = JSON.parse(body);\n } catch {\n // Let the dispatcher handle parse errors\n await dispatcher(req, res, body);\n return;\n }\n\n const method =\n typeof parsed === \"object\" && parsed !== null && \"method\" in parsed\n ? (parsed as { method: unknown }).method\n : undefined;\n\n // Handle initialize directly to control response headers\n if (method === \"initialize\") {\n const id =\n typeof parsed === \"object\" && parsed !== null && \"id\" in parsed\n ? (parsed as { id: unknown }).id\n : null;\n\n const sessionId = randomUUID();\n state.sessions.set(sessionId, {\n id: sessionId,\n initialized: false,\n createdAt: Date.now(),\n });\n\n const response = {\n jsonrpc: \"2.0\",\n id,\n result: {\n protocolVersion: \"2025-03-26\",\n capabilities: { tools: {}, resources: {}, prompts: {} },\n serverInfo: state.serverInfo,\n },\n };\n\n res.writeHead(200, {\n \"Content-Type\": \"application/json\",\n \"Mcp-Session-Id\": sessionId,\n });\n res.end(JSON.stringify(response));\n return;\n }\n\n // Session validation for all other methods\n const sessionId = req.headers[\"mcp-session-id\"] as string | undefined;\n if (!sessionId) {\n res.writeHead(400, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify({ error: \"Missing mcp-session-id header\" }));\n return;\n }\n if (!state.sessions.has(sessionId)) {\n res.writeHead(404, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify({ error: \"Session not found\" }));\n return;\n }\n\n // Enforce initialization: only allow notifications/initialized through\n // before the session is fully initialized\n const session = state.sessions.get(sessionId)!;\n if (!session.initialized && method !== \"notifications/initialized\") {\n res.writeHead(400, { \"Content-Type\": \"application/json\" });\n res.end(\n JSON.stringify(\n jsonRpcError(\n typeof parsed === \"object\" && parsed !== null && \"id\" in parsed\n ? ((parsed as { id: unknown }).id as string | number)\n : null,\n -32002,\n \"Session not initialized\",\n ),\n ),\n );\n return;\n }\n\n // Delegate to the JSON-RPC dispatcher for all other methods\n await dispatcher(req, res, body);\n };\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"mcp-handler.js","names":[],"sources":["../src/mcp-handler.ts"],"sourcesContent":["import type * as http from \"node:http\";\nimport { randomUUID } from \"node:crypto\";\nimport { createJsonRpcDispatcher } from \"./jsonrpc.js\";\nimport type {\n MCPToolDefinition,\n MCPResourceDefinition,\n MCPResourceContent,\n MCPPromptDefinition,\n MCPPromptResult,\n MCPContent,\n MCPSession,\n} from \"./mcp-types.js\";\nimport type { McpFakeClaim, McpFakeStore } from \"./mcp-fakes.js\";\nimport {\n MCP_FAKE_ERROR_CODES,\n type McpFakeErrorCode,\n type McpFakeIdentity,\n type McpFakeNotification,\n type McpFakeOutcome,\n} from \"./types.js\";\nimport { decodeMcpHeaderValue, resolveMcpIdentity, type McpIdentityResolution } from \"./helpers.js\";\nimport { Message, build, fixed, jsonText, msg, plainText, quote } from \"./message-text.js\";\n\n/**\n * `data.aimock.code` of a `tools/call` whose fake claim threw: an aimock\n * defect (a validated entry whose result could not be built or sent), not a\n * scenario failure. Answered with the JSON-RPC internal-error code `-32603`;\n * the entry stays unconsumed.\n */\nexport const MCP_FAKE_INTERNAL_ERROR = \"MCP_FAKE_INTERNAL_ERROR\";\nconst MCP_FAKE_INTERNAL_ERROR_RPC_CODE = -32603;\n\n/** What every fake event carries: who called which tool on which mount (I9). */\ninterface McpFakeEventBase {\n identity: McpFakeIdentity;\n tool: string;\n /** The mount path the request came in on (I9). */\n mount: string;\n /** The call's `arguments` as received (RP2). */\n args: unknown;\n}\n\n/**\n * One MCP fake outcome of a `tools/call`, for the mount's side channels\n * (metric and log line; spec 9.3). `answered` names the entry; every other\n * kind is a failure with its `data.aimock.code` and the JSON-RPC error\n * message sent to the client. `evicted` also carries the evicted test id and\n * the cap; `internal_error` is a claim that threw (`MCP_FAKE_INTERNAL_ERROR`).\n */\nexport type McpFakeEvent = McpFakeEventBase &\n (\n | { kind: \"answered\"; entryId: string }\n | {\n kind: Exclude<McpFakeOutcome, \"answered\" | \"evicted\">;\n code: McpFakeErrorCode;\n message: string;\n }\n | { kind: \"evicted\"; code: \"MCP_FAKE_EVICTED\"; message: string; testId: string; cap: number }\n | { kind: \"internal_error\"; code: typeof MCP_FAKE_INTERNAL_ERROR; message: string }\n );\n\n/** An identity field whose value can fall back to the raw value (H1). */\nexport type McpDecodeField = \"testId\" | \"context\";\n\n/**\n * A decode fallback of one request, for L2: a test id or context value that\n * is not valid percent-encoding (on the header or the query; `used` is false\n * for a query value the header overrode), or query names that are not valid\n * percent-encoding and so match no identity field.\n */\nexport type McpDecodeFallback =\n | { kind: \"value\"; field: McpDecodeField; source: \"header\" | \"query\"; raw: string; used: boolean }\n | { kind: \"names\"; count: number };\n\n/** One JSON-RPC message of a request, for one journal entry (B2). */\nexport interface MCPMessageRecord {\n /** The JSON-RPC request object of a `tools/call`, else `null`. */\n body: Record<string, unknown> | null;\n /** Set when a fake answered or failed this `tools/call`. */\n mcpFake: { id: string | null; outcome: McpFakeOutcome } | null;\n /** What the mount's own code threw while serving this message, else `null`. */\n error: string | null;\n}\n\n/**\n * What one request handled by `createMCPRequestHandler` resolved, for the\n * mount's journal (B2, B3). `identity` is `null` when the request had none to\n * resolve (a rejected identity; a DELETE with a missing or unknown session).\n * `messages` has one record per JSON-RPC message: one for a single message,\n * one per element of a batch, so each `tools/call` of a batch is journaled\n * with its own body and fake outcome. The mount writes one journal entry per\n * record, and one with no body when there is none (a request answered before\n * any message was read, or an empty batch).\n */\nexport interface MCPRequestRecord {\n identity: McpFakeIdentity | null;\n messages: MCPMessageRecord[];\n}\n\n/** An empty record, for the caller to pass in and read even if the handler throws. */\nexport function newMCPRequestRecord(): MCPRequestRecord {\n return { identity: null, messages: [] };\n}\n\nexport interface MCPState {\n serverInfo: { name: string; version: string };\n tools: Map<string, { def: MCPToolDefinition; handler?: (...args: unknown[]) => unknown }>;\n resources: Map<string, { def: MCPResourceDefinition; content?: MCPResourceContent }>;\n prompts: Map<\n string,\n {\n def: MCPPromptDefinition;\n handler?: (...args: unknown[]) => MCPPromptResult | Promise<MCPPromptResult>;\n }\n >;\n sessions: Map<string, MCPSession>;\n /** The mount's fakes (one store per mount). */\n fakes: McpFakeStore;\n /** Whether any fake block is loaded on the mount (gates L2). */\n hasFakes(): boolean;\n /** A fake answered or failed a `tools/call` (metric, L1/L10 log). */\n onFakeEvent(evt: McpFakeEvent): void;\n /**\n * A request of `sessionId` sent an identity value (or query name) that is\n * not valid percent-encoding (H1). Called only when `hasFakes()`; the mount\n * logs L2 once per session per field and source.\n */\n onDecodeFallback(sessionId: string, fallback: McpDecodeFallback): void;\n /** `sessionId` was deleted (DELETE): the mount drops its per-session state. */\n onSessionClosed(sessionId: string): void;\n /** RP2: a tools/call no fake applied to (fakes:10 step 3), with who answered. */\n onUnfaked(evt: {\n identity: McpFakeIdentity;\n tool: string;\n mount: string;\n args: unknown;\n answeredBy: \"handler\" | \"config\" | \"empty\" | \"unknown-tool\";\n }): void;\n /** T1: the replay speed recorded timing plays at (`value / speed`). */\n replaySpeed(): number;\n}\n\n/**\n * C8: the `onToolCall` handlers that answer a config `mcp.tools[].result`.\n * `tools/call` reports such an answer as `answeredBy: \"config\"`. Internal:\n * not exported from the package.\n */\nexport const configToolHandlers = new WeakSet<(...args: unknown[]) => unknown>();\n\n/** C7: the syslog levels of `logging/setLevel`, lowest first. */\nconst LOG_LEVELS = [\n \"debug\",\n \"info\",\n \"notice\",\n \"warning\",\n \"error\",\n \"critical\",\n \"alert\",\n \"emergency\",\n] as const;\n\n/** C7: the rank of a syslog level name, or -1 when it is not one. */\nfunction logLevelRank(level: unknown): number {\n return typeof level === \"string\" ? (LOG_LEVELS as readonly string[]).indexOf(level) : -1;\n}\n\nfunction jsonRpcResult(id: string | number, result: unknown) {\n return { jsonrpc: \"2.0\" as const, id, result };\n}\n\nfunction jsonRpcError(id: string | number | null, code: number, message: string, data?: unknown) {\n return {\n jsonrpc: \"2.0\" as const,\n id,\n error: data === undefined ? { code, message } : { code, message, data },\n };\n}\n\n/**\n * The identity part of a fake message or log line (I9): `testId \"<id>\"`,\n * `context \"<ctx>\"`, both, or `no testId or context`.\n */\nexport function describeMcpIdentity(identity: McpFakeIdentity): Message {\n const { testId, context } = identity;\n if (testId !== null && context !== null) {\n return msg`testId ${quote(testId)}, context ${quote(context)}`;\n }\n if (testId !== null) return msg`testId ${quote(testId)}`;\n if (context !== null) return msg`context ${quote(context)}`;\n return msg`no testId or context`;\n}\n\n/** A value as JSON text for a message part. */\nfunction jsonPart(value: unknown): ReturnType<typeof jsonText> {\n return jsonText(JSON.stringify(value) ?? \"null\");\n}\n\n/** The outcome of one `tools/call` invocation, for its message record. */\ntype CallOutcome = Omit<MCPMessageRecord, \"body\">;\n\n/**\n * What `tools/call` reads of one request (its resolved identity and mount,\n * and which invocations are notifications) and writes back: one outcome per\n * `tools/call` invocation, in order, so a batch's calls each keep their own.\n */\ninterface FakeRequestContext {\n identity: McpFakeIdentity;\n mount: string;\n /**\n * Per `tools/call` invocation, in order: true when its message has no `id`\n * member. The dispatcher passes a null id both for a notification and for\n * a request whose id is null or not a string or number, so the handler\n * cannot tell them apart from its id.\n */\n notifications: boolean[];\n calls: CallOutcome[];\n /**\n * Set by the outer handler for a single `tools/call` it already claimed\n * (MR15 interception), so the method does not claim twice: `reply` is the\n * answer or failure `callFake` built, `none` means no fake applied. The\n * outcome of that call is already in `calls`.\n */\n precomputed?:\n | {\n kind: \"reply\";\n value: ReturnType<typeof jsonRpcResult> | ReturnType<typeof jsonRpcError>;\n }\n | { kind: \"none\" };\n}\n\n/**\n * A fake failure: the JSON-RPC error to send and the event to report; an\n * `evicted` one also carries the evicted test id and the cap.\n */\ntype FakeFailure = { message: string; aimock: Record<string, unknown> } & (\n | { outcome: Exclude<McpFakeOutcome, \"answered\" | \"evicted\">; code: McpFakeErrorCode }\n | { outcome: \"evicted\"; code: \"MCP_FAKE_EVICTED\"; evicted: { testId: string; cap: number } }\n);\n\n/**\n * The 9.2 error for a failed claim (`mismatch`, `exhausted`, `evicted`), or\n * for an undeclared tool under `deny` (`not_declared`).\n */\nexport function fakeFailure(\n claim: Exclude<McpFakeClaim, { kind: \"answer\" } | { kind: \"none\" }> | { kind: \"not_declared\" },\n tool: string,\n ctx: FakeRequestContext,\n declaredTools: () => string[],\n): FakeFailure {\n const who = describeMcpIdentity(ctx.identity);\n const base = {\n tool,\n testId: ctx.identity.testId,\n context: ctx.identity.context,\n mount: ctx.mount,\n };\n switch (claim.kind) {\n case \"mismatch\": {\n const declared = claim.declared.map((d) =>\n d.args !== undefined\n ? msg`${plainText(d.id)} ${jsonPart(d.args)}`\n : msg`${plainText(d.id)} anyArgs`,\n );\n const list = declared.reduce<Message>(\n (acc, part, i) => (i === 0 ? part : msg`${acc}; ${part}`),\n msg`none`,\n );\n return {\n code: \"MCP_FAKE_MISMATCH\",\n outcome: \"mismatch\",\n message: build(\n msg`aimock MCP fake mismatch: tool ${quote(tool)} was called with arguments that match no declared fake (${who}). Received ${jsonPart(claim.received)}. Declared: ${list}.`,\n ),\n aimock: {\n code: \"MCP_FAKE_MISMATCH\",\n ...base,\n received: claim.received,\n declared: claim.declared,\n firstDifference: claim.firstDifference,\n },\n };\n }\n case \"exhausted\": {\n const used =\n claim.matchingDeclared === 1\n ? msg`the 1 matching fake is`\n : msg`all ${claim.matchingDeclared} matching fakes are`;\n return {\n code: \"MCP_FAKE_EXHAUSTED\",\n outcome: \"exhausted\",\n message: build(\n msg`aimock MCP fake exhausted: tool ${quote(tool)} with ${jsonPart(claim.received)} was called again, but ${used} already used (${who}).`,\n ),\n aimock: {\n code: \"MCP_FAKE_EXHAUSTED\",\n ...base,\n received: claim.received,\n matchingDeclared: claim.matchingDeclared,\n matchingConsumed: claim.matchingConsumed,\n matchingIds: claim.matchingIds,\n },\n };\n }\n case \"evicted\":\n // Only a test id is ever evicted (never the untagged state), so the\n // claim's test id is the request's.\n return {\n code: \"MCP_FAKE_EVICTED\",\n outcome: \"evicted\",\n evicted: { testId: claim.testId, cap: claim.maxTestIds },\n message: build(\n msg`aimock MCP fake evicted: consumption state for testId ${quote(claim.testId)} was evicted by the per-mount test-id cap (${claim.maxTestIds}); reset before reusing this test id (${who}).`,\n ),\n aimock: { code: \"MCP_FAKE_EVICTED\", ...base, testId: claim.testId, cap: claim.maxTestIds },\n };\n case \"not_declared\":\n return {\n code: \"MCP_FAKE_NOT_DECLARED\",\n outcome: \"not_declared\",\n message: build(\n msg`Unknown tool: ${plainText(tool)} (aimock MCP fake not declared: this scenario denies undeclared tools; ${who}).`,\n ),\n aimock: { code: \"MCP_FAKE_NOT_DECLARED\", ...base, declaredTools: declaredTools() },\n };\n }\n}\n\nexport function createMCPRequestHandler(state: MCPState) {\n /** Per-request fake context, set by the outer handler for the dispatcher's methods. */\n const contexts = new WeakMap<http.IncomingMessage, FakeRequestContext>();\n /** C7: each session's `logging/setLevel` level; dropped on DELETE. */\n const logLevels = new Map<string, string>();\n\n /** The initialize capabilities: `logging` only on a mount with recorded logs (C7). */\n const capabilities = (): Record<string, unknown> =>\n state.fakes.hasRecordedLogs()\n ? { tools: {}, resources: {}, prompts: {}, logging: {} }\n : { tools: {}, resources: {}, prompts: {} };\n\n /**\n * Spec 10 steps 1-2 for `tools/call`: a fake answer or fake failure, or\n * `null` to fall through to today's behavior (step 3).\n */\n const callFake = (\n ctx: FakeRequestContext,\n outcome: CallOutcome,\n tool: string,\n args: unknown,\n id: string | number,\n ): ReturnType<typeof jsonRpcResult> | ReturnType<typeof jsonRpcError> | null => {\n const event = { identity: ctx.identity, tool, mount: ctx.mount, args };\n let claim: McpFakeClaim;\n try {\n claim = state.fakes.claim(tool, args, ctx.identity);\n } catch (err) {\n // A validated entry whose result cannot be built or sent: an aimock\n // defect. Fail loud on every side channel, never a generic -32603.\n const thrown = err instanceof Error ? err.message : String(err);\n const message = build(\n msg`aimock MCP fake internal error: tool ${quote(tool)} could not be answered (${describeMcpIdentity(ctx.identity)}): ${plainText(thrown)}`,\n );\n outcome.error = message;\n state.onFakeEvent({\n ...event,\n kind: \"internal_error\",\n code: MCP_FAKE_INTERNAL_ERROR,\n message,\n });\n return jsonRpcError(id, MCP_FAKE_INTERNAL_ERROR_RPC_CODE, message, {\n aimock: {\n code: MCP_FAKE_INTERNAL_ERROR,\n tool,\n testId: ctx.identity.testId,\n context: ctx.identity.context,\n mount: ctx.mount,\n error: thrown,\n },\n });\n }\n if (claim.kind === \"answer\") {\n outcome.mcpFake = { id: claim.entry.id, outcome: \"answered\" };\n state.onFakeEvent({ ...event, kind: \"answered\", entryId: claim.entry.id });\n return jsonRpcResult(id, claim.result);\n }\n let failure: FakeFailure;\n if (claim.kind === \"none\") {\n if (state.fakes.policy(ctx.identity) !== \"deny\") return null;\n failure = fakeFailure({ kind: \"not_declared\" }, tool, ctx, () =>\n state.fakes.declaredTools(ctx.identity),\n );\n } else {\n failure = fakeFailure(claim, tool, ctx, () => []);\n }\n outcome.mcpFake = { id: null, outcome: failure.outcome };\n state.onFakeEvent(\n failure.outcome === \"evicted\"\n ? {\n ...event,\n kind: \"evicted\",\n code: failure.code,\n message: failure.message,\n ...failure.evicted,\n }\n : { ...event, kind: failure.outcome, code: failure.code, message: failure.message },\n );\n return jsonRpcError(id, MCP_FAKE_ERROR_CODES[failure.code], failure.message, {\n aimock: failure.aimock,\n });\n };\n\n const dispatcher = createJsonRpcDispatcher({\n methods: {\n // initialize is handled directly in the outer function — this entry is\n // only here so the dispatcher doesn't return \"Method not found\" if the\n // request somehow reaches it.\n initialize: async (_params, id) => {\n return jsonRpcResult(id, {\n protocolVersion: \"2025-03-26\",\n capabilities: capabilities(),\n serverInfo: state.serverInfo,\n });\n },\n\n \"notifications/initialized\": async (_params, _id, req) => {\n const sessionId = req.headers[\"mcp-session-id\"] as string;\n const session = state.sessions.get(sessionId);\n if (session) {\n session.initialized = true;\n }\n return null;\n },\n\n ping: async (_params, id) => {\n return jsonRpcResult(id, {});\n },\n\n // C7: only a mount with recorded log lines serves logging/setLevel;\n // every other mount answers exactly as the dispatcher does for an\n // unknown method (src/jsonrpc.ts).\n \"logging/setLevel\": async (params, id, req) => {\n if (!state.fakes.hasRecordedLogs()) return jsonRpcError(id, -32601, \"Method not found\");\n const level = (params ?? {}) as { level?: unknown };\n if (logLevelRank(level.level) === -1) {\n return jsonRpcError(id, -32602, \"Invalid log level\");\n }\n const sessionId = req.headers[\"mcp-session-id\"];\n if (typeof sessionId === \"string\") logLevels.set(sessionId, String(level.level));\n return jsonRpcResult(id, {});\n },\n\n \"tools/list\": async (_params, id, req) => {\n const tools: MCPToolDefinition[] = [];\n for (const { def } of state.tools.values()) {\n tools.push(def);\n }\n const ctx = contexts.get(req);\n // AM7: a recorded list comes first, in recorded order (its definition\n // wins), then registered tools missing from it, then the other fake\n // tools.\n const recorded = ctx ? state.fakes.recordedList(ctx.identity) : null;\n if (ctx && recorded) {\n const listed: unknown[] = [...recorded];\n const names = new Set(recorded.map((t) => t.name));\n for (const def of tools) {\n if (!names.has(def.name)) {\n names.add(def.name);\n listed.push(def);\n }\n }\n for (const fake of state.fakes.listTools(ctx.identity)) {\n if (!names.has(fake.name)) {\n names.add(fake.name);\n listed.push(fake);\n }\n }\n return jsonRpcResult(id, { tools: listed });\n }\n // Spec 10: the union with the applicable fake tools; a registered\n // definition wins for metadata.\n if (ctx) {\n const registered = new Set(tools.map((t) => t.name));\n for (const fake of state.fakes.listTools(ctx.identity)) {\n if (!registered.has(fake.name)) tools.push(fake);\n }\n }\n return jsonRpcResult(id, { tools });\n },\n\n \"tools/call\": async (params, id, req) => {\n // One outcome per invocation, in order, even when nothing is claimed,\n // so each message of a batch is matched with its own.\n const ctx = contexts.get(req);\n // A single call the outer handler already claimed: its outcome is\n // already in `calls`, and its claim is not run again.\n const precomputed = ctx?.precomputed;\n if (ctx) ctx.precomputed = undefined;\n const isNotification = !precomputed && ctx?.notifications[ctx.calls.length] === true;\n if (!precomputed) ctx?.calls.push({ mcpFake: null, error: null });\n const outcome: CallOutcome = ctx?.calls[ctx.calls.length - 1] ?? {\n mcpFake: null,\n error: null,\n };\n // A notification (no `id` member) gets no answer (JSON-RPC 2.0) and\n // claims no fake entry, but a registered handler still runs for its\n // side effects, as on origin/main. A request whose id is null or of\n // another type is still served, with id null.\n const { name: rawName, arguments: args } = (params ?? {}) as {\n name?: unknown;\n arguments?: unknown;\n };\n if (!rawName) {\n return jsonRpcError(id, -32602, \"Missing tool name\");\n }\n if (typeof rawName !== \"string\") {\n const got = Array.isArray(rawName) ? \"array\" : typeof rawName;\n return jsonRpcError(\n id,\n -32602,\n build(msg`Invalid tool name: expected a string, got ${fixed(got)}`),\n );\n }\n const name = rawName;\n // Spec 10 steps 1-2: fakes, then the deny policy; never a handler\n // after a fake failure.\n if (precomputed?.kind === \"reply\") return precomputed.value;\n if (ctx && !isNotification && !precomputed) {\n const fake = callFake(ctx, outcome, name, args, id);\n if (fake) return fake;\n }\n const entry = state.tools.get(name);\n // RP2: every tools/call no fake applied to is logged with who answers it.\n if (ctx && !isNotification) {\n state.onUnfaked({\n identity: ctx.identity,\n tool: name,\n mount: ctx.mount,\n args,\n answeredBy: !entry\n ? \"unknown-tool\"\n : entry.handler && configToolHandlers.has(entry.handler)\n ? \"config\"\n : entry.handler\n ? \"handler\"\n : \"empty\",\n });\n }\n if (!entry) {\n return jsonRpcError(id, -32602, `Unknown tool: ${name}`);\n }\n if (entry.handler) {\n try {\n const result = await entry.handler(args);\n const content: MCPContent[] = Array.isArray(result)\n ? result\n : [{ type: \"text\", text: String(result) }];\n return jsonRpcResult(id, { content, isError: false });\n } catch (err: unknown) {\n const message = err instanceof Error ? err.message : String(err);\n return jsonRpcResult(id, {\n content: [{ type: \"text\", text: message }],\n isError: true,\n });\n }\n }\n // No handler — return empty content\n return jsonRpcResult(id, { content: [], isError: false });\n },\n\n \"resources/list\": async (_params, id) => {\n const resources: MCPResourceDefinition[] = [];\n for (const { def } of state.resources.values()) {\n resources.push(def);\n }\n return jsonRpcResult(id, { resources });\n },\n\n \"resources/read\": async (params, id) => {\n const { uri } = (params ?? {}) as { uri?: string };\n if (!uri) {\n return jsonRpcError(id, -32602, \"Missing resource URI\");\n }\n const entry = state.resources.get(uri);\n if (!entry) {\n return jsonRpcError(id, -32602, `Unknown resource: ${uri}`);\n }\n return jsonRpcResult(id, {\n contents: [\n {\n uri,\n ...(entry.content?.text !== undefined && { text: entry.content.text }),\n ...(entry.content?.blob !== undefined && { blob: entry.content.blob }),\n ...(entry.content?.mimeType !== undefined && { mimeType: entry.content.mimeType }),\n },\n ],\n });\n },\n\n \"prompts/list\": async (_params, id) => {\n const prompts: MCPPromptDefinition[] = [];\n for (const { def } of state.prompts.values()) {\n prompts.push(def);\n }\n return jsonRpcResult(id, { prompts });\n },\n\n \"prompts/get\": async (params, id) => {\n const { name, arguments: args } = (params ?? {}) as { name?: string; arguments?: unknown };\n if (!name) {\n return jsonRpcError(id, -32602, \"Missing prompt name\");\n }\n const entry = state.prompts.get(name);\n if (!entry) {\n return jsonRpcError(id, -32602, `Unknown prompt: ${name}`);\n }\n if (entry.handler) {\n try {\n const result = await entry.handler(args);\n return jsonRpcResult(id, result);\n } catch (err: unknown) {\n const message = err instanceof Error ? err.message : String(err);\n return jsonRpcError(id, -32603, `Prompt handler error: ${message}`);\n }\n }\n // No handler — return empty messages\n return jsonRpcResult(id, { messages: [] });\n },\n },\n });\n\n /** Answer a rejected identity (H4, H5) with HTTP 400, in the session-error shape. */\n const rejectIdentity = (\n res: http.ServerResponse,\n resolution: Extract<McpIdentityResolution, { ok: false }>,\n ): void => {\n res.writeHead(resolution.error.status, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify({ error: resolution.error.message, code: resolution.error.code }));\n };\n\n /**\n * L2: report each identity value that fell back to the raw value (H1),\n * naming its source (header or query) and the value itself, and any query\n * name that did not decode. `fellBack` says only that some value of the\n * field fell back; the header is checked on its own, and the query by the\n * same resolver given the query alone, so the rules are the resolver's.\n */\n const reportFallback = (\n sessionId: string,\n req: http.IncomingMessage,\n resolution: Extract<McpIdentityResolution, { ok: true }>,\n ): void => {\n if (!state.hasFakes()) return;\n const { fellBack } = resolution;\n if (fellBack.testId || fellBack.context) {\n const queryOnly = resolveMcpIdentity({ headers: {}, url: req.url });\n for (const field of [\"testId\", \"context\"] as const) {\n if (!fellBack[field]) continue;\n const header = req.headers[IDENTITY_HEADERS[field]];\n const headerSent = typeof header === \"string\" && header !== \"\";\n if (headerSent && decodeMcpHeaderValue(header).fellBack) {\n state.onDecodeFallback(sessionId, {\n kind: \"value\",\n field,\n source: \"header\",\n raw: header,\n used: true,\n });\n }\n const fromQuery =\n queryOnly.ok && queryOnly.fellBack[field] ? queryOnly.identity[field] : null;\n if (fromQuery !== null) {\n state.onDecodeFallback(sessionId, {\n kind: \"value\",\n field,\n source: \"query\",\n raw: fromQuery,\n used: !headerSent,\n });\n }\n }\n }\n if (resolution.undecodedQueryNames) {\n state.onDecodeFallback(sessionId, { kind: \"names\", count: resolution.undecodedQueryNames });\n }\n };\n\n /**\n * Handle one MCP request. `mount` is the path the mount is served at (I9;\n * `/` for a standalone server). Resolves with what the request resolved,\n * for the mount's journal entry.\n */\n return async (\n req: http.IncomingMessage,\n res: http.ServerResponse,\n body: string,\n mount = \"/\",\n record: MCPRequestRecord = newMCPRequestRecord(),\n ): Promise<MCPRequestRecord> => {\n /** T1: recorded `atMs` / `durationMs` are measured from here. */\n const arrivedAt = performance.now();\n // DELETE handler: session teardown\n if (req.method === \"DELETE\") {\n const sessionId = req.headers[\"mcp-session-id\"] as string | undefined;\n if (!sessionId) {\n res.writeHead(400, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify({ error: \"Missing mcp-session-id header\" }));\n return record;\n }\n if (!state.sessions.has(sessionId)) {\n res.writeHead(404, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify({ error: \"Session not found\" }));\n return record;\n }\n // Journal only: identity inputs are not checked on a session teardown.\n const resolution = resolveMcpIdentity(req, state.sessions.get(sessionId));\n if (resolution.ok) record.identity = resolution.identity;\n state.sessions.delete(sessionId);\n logLevels.delete(sessionId);\n state.onSessionClosed(sessionId);\n res.writeHead(200, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify({ ok: true }));\n return record;\n }\n\n // Identity inputs (I1-I3, H4, H5): on every POST, a bad undeclared\n // override or a repeated field is rejected before the request is served.\n const headerSessionId = req.headers[\"mcp-session-id\"] as string | undefined;\n const knownSession = headerSessionId ? state.sessions.get(headerSessionId) : undefined;\n\n // Parse the body to determine method for session validation\n let parsed: unknown;\n try {\n parsed = JSON.parse(body);\n } catch {\n const resolution = resolveMcpIdentity(req, knownSession);\n if (!resolution.ok) {\n rejectIdentity(res, resolution);\n return record;\n }\n record.identity = resolution.identity;\n if (headerSessionId && knownSession) reportFallback(headerSessionId, req, resolution);\n // Let the dispatcher handle parse errors\n await dispatcher(req, res, body);\n return record;\n }\n\n const method =\n typeof parsed === \"object\" && parsed !== null && \"method\" in parsed\n ? (parsed as { method: unknown }).method\n : undefined;\n\n // Handle initialize directly to control response headers\n if (method === \"initialize\") {\n const id =\n typeof parsed === \"object\" && parsed !== null && \"id\" in parsed\n ? (parsed as { id: unknown }).id\n : null;\n\n // I3: only the values the initialize request itself supplies are bound.\n const resolution = resolveMcpIdentity(req);\n if (!resolution.ok) {\n rejectIdentity(res, resolution);\n return record;\n }\n record.identity = resolution.identity;\n const { identity, supplied } = resolution;\n\n const sessionId = randomUUID();\n state.sessions.set(sessionId, {\n id: sessionId,\n initialized: false,\n createdAt: Date.now(),\n ...(supplied.testId && identity.testId !== null ? { testId: identity.testId } : {}),\n ...(supplied.context && identity.context !== null ? { context: identity.context } : {}),\n ...(supplied.undeclared && identity.undeclared !== null\n ? { undeclared: identity.undeclared }\n : {}),\n });\n reportFallback(sessionId, req, resolution);\n\n const response = {\n jsonrpc: \"2.0\",\n id,\n result: {\n protocolVersion: \"2025-03-26\",\n capabilities: capabilities(),\n serverInfo: state.serverInfo,\n },\n };\n\n res.writeHead(200, {\n \"Content-Type\": \"application/json\",\n \"Mcp-Session-Id\": sessionId,\n });\n res.end(JSON.stringify(response));\n return record;\n }\n\n const resolution = resolveMcpIdentity(req, knownSession);\n if (!resolution.ok) {\n rejectIdentity(res, resolution);\n return record;\n }\n record.identity = resolution.identity;\n\n // Session validation for all other methods\n const sessionId = headerSessionId;\n if (!sessionId) {\n res.writeHead(400, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify({ error: \"Missing mcp-session-id header\" }));\n return record;\n }\n if (!state.sessions.has(sessionId)) {\n res.writeHead(404, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify({ error: \"Session not found\" }));\n return record;\n }\n reportFallback(sessionId, req, resolution);\n\n // Enforce initialization: only allow notifications/initialized through\n // before the session is fully initialized\n const session = state.sessions.get(sessionId)!;\n if (!session.initialized && method !== \"notifications/initialized\") {\n res.writeHead(400, { \"Content-Type\": \"application/json\" });\n res.end(\n JSON.stringify(\n jsonRpcError(\n typeof parsed === \"object\" && parsed !== null && \"id\" in parsed\n ? ((parsed as { id: unknown }).id as string | number)\n : null,\n -32002,\n \"Session not initialized\",\n ),\n ),\n );\n return record;\n }\n\n // Delegate to the JSON-RPC dispatcher for all other methods\n // The dispatcher invokes `tools/call` once per message that is an object\n // with `jsonrpc: \"2.0\"` and that method, in order (see B2 below).\n const notifications = (Array.isArray(parsed) ? parsed : [parsed])\n .filter((m): m is Record<string, unknown> => isToolsCall(m) && m.jsonrpc === \"2.0\")\n .map((m) => !(\"id\" in m));\n const ctx: FakeRequestContext = {\n identity: resolution.identity,\n mount,\n notifications,\n calls: [],\n };\n contexts.set(req, ctx);\n // MR15 / T1: a single tools/call is claimed here, before the dispatcher,\n // because an answer with recorded timing or notifications is written by\n // `writeFakeToolAnswer` (SSE or delayed JSON), which the dispatcher's\n // JSON-only writer cannot do. Any other outcome is handed to the method\n // through `ctx.precomputed`, so the claim runs once.\n const single = singleToolsCall(parsed);\n if (single) {\n const outcome: CallOutcome = { mcpFake: null, error: null };\n ctx.calls.push(outcome);\n const fake = callFake(ctx, outcome, single.name, single.args, single.id);\n const replay =\n fake && outcome.mcpFake?.outcome === \"answered\" && outcome.mcpFake.id !== null\n ? state.fakes.replayOf(outcome.mcpFake.id)\n : null;\n if (\n fake &&\n \"result\" in fake &&\n replay &&\n (replay.notifications.length > 0 ||\n (replay.timing === \"recorded\" && replay.durationMs !== undefined))\n ) {\n await writeFakeToolAnswer(\n req,\n res,\n single.id,\n fake.result,\n replay,\n single.params,\n state.replaySpeed(),\n logLevels.get(sessionId) ?? null,\n arrivedAt,\n );\n record.messages = [\n { body: single.message, mcpFake: outcome.mcpFake, error: outcome.error },\n ];\n return record;\n }\n ctx.precomputed = fake ? { kind: \"reply\", value: fake } : { kind: \"none\" };\n }\n await dispatcher(req, res, body);\n // B2: each tools/call message is journaled with its body and its own\n // outcome. The dispatcher invokes `tools/call` once per message that is\n // an object with `jsonrpc: \"2.0\"` and that method, in order, so the n-th\n // such message takes the n-th outcome.\n let next = 0;\n const messageOf = (message: unknown): MCPMessageRecord => {\n if (!isToolsCall(message)) return { body: null, mcpFake: null, error: null };\n const outcome = message.jsonrpc === \"2.0\" ? ctx.calls[next++] : undefined;\n return { body: message, mcpFake: outcome?.mcpFake ?? null, error: outcome?.error ?? null };\n };\n record.messages = Array.isArray(parsed) ? parsed.map(messageOf) : [messageOf(parsed)];\n return record;\n };\n}\n\n/**\n * A single JSON-RPC `tools/call` request (not a batch, not a notification)\n * with a string or number id and a string tool name, else `null`. Only such\n * a message is claimed by the outer handler (MR15).\n */\nfunction singleToolsCall(parsed: unknown): {\n message: Record<string, unknown>;\n id: string | number;\n name: string;\n args: unknown;\n params: unknown;\n} | null {\n if (!isToolsCall(parsed) || parsed.jsonrpc !== \"2.0\" || !(\"id\" in parsed)) return null;\n const id = parsed.id;\n if (typeof id !== \"string\" && typeof id !== \"number\") return null;\n const params = parsed.params;\n if (typeof params !== \"object\" || params === null) return null;\n const name: unknown = (params as { name?: unknown }).name;\n if (typeof name !== \"string\" || name === \"\") return null;\n return { message: parsed, id, name, args: (params as { arguments?: unknown }).arguments, params };\n}\n\n/** Resolve after `ms` (no wait when `ms <= 0`). */\nfunction sleep(ms: number): Promise<void> {\n return ms > 0 ? new Promise((resolve) => setTimeout(resolve, ms)) : Promise.resolve();\n}\n\n/** The client's `_meta.progressToken` of a request's params, else `undefined`. */\nfunction progressTokenOf(params: unknown): string | number | undefined {\n if (typeof params !== \"object\" || params === null) return undefined;\n const meta: unknown = (params as { _meta?: unknown })._meta;\n if (typeof meta !== \"object\" || meta === null) return undefined;\n const token: unknown = (meta as { progressToken?: unknown }).progressToken;\n return typeof token === \"string\" || typeof token === \"number\" ? token : undefined;\n}\n\n/**\n * FA3 gate of one recorded notification for this request: the params to send,\n * or `null` when it does not replay. `notifications/progress` replays only\n * when the request carries a progress token, which replaces the recorded one;\n * `notifications/message` only when the session set a log level (C7) and the\n * line is at or above it. No other method replays.\n */\nfunction replayedParams(\n notification: McpFakeNotification,\n progressToken: string | number | undefined,\n logLevel: string | null,\n): Record<string, unknown> | null {\n if (notification.method === \"notifications/progress\") {\n return progressToken === undefined ? null : { ...notification.params, progressToken };\n }\n if (notification.method === \"notifications/message\") {\n const min = logLevelRank(logLevel);\n const rank = logLevelRank(notification.params.level);\n return min !== -1 && rank >= min ? { ...notification.params } : null;\n }\n return null;\n}\n\n/**\n * Write a fake's tools/call answer. SSE iff the entry has a notification whose\n * FA3 gate passes AND Accept lists text/event-stream; else JSON. Timing per T1\n * (value / speed) unless the block's timing is \"immediate\". `logLevel` is the\n * session's `logging/setLevel` level (C7), `arrivedAt` the `performance.now()`\n * of the request's arrival, which `atMs` and `durationMs` are measured from.\n */\nexport async function writeFakeToolAnswer(\n req: http.IncomingMessage,\n res: http.ServerResponse,\n id: string | number | null,\n result: unknown,\n replay: ReturnType<McpFakeStore[\"replayOf\"]>,\n params: unknown,\n speed: number,\n logLevel: string | null = null,\n arrivedAt: number = performance.now(),\n): Promise<void> {\n const timed = replay !== null && replay.timing === \"recorded\";\n const waitUntil = (atMs: number): Promise<void> =>\n timed ? sleep(atMs / speed - (performance.now() - arrivedAt)) : Promise.resolve();\n const token = progressTokenOf(params);\n const notifications = [...(replay?.notifications ?? [])]\n .sort((a, b) => a.atMs - b.atMs)\n .flatMap((n) => {\n const sent = replayedParams(n, token, logLevel);\n return sent ? [{ atMs: n.atMs, method: n.method, params: sent }] : [];\n });\n const accept = req.headers.accept ?? \"\";\n const answer = { jsonrpc: \"2.0\" as const, id, result };\n if (notifications.length > 0 && accept.includes(\"text/event-stream\")) {\n res.writeHead(200, { \"Content-Type\": \"text/event-stream\", \"Cache-Control\": \"no-cache\" });\n res.flushHeaders();\n for (const n of notifications) {\n await waitUntil(n.atMs);\n if (res.destroyed) return;\n res.write(\n `event: message\\ndata: ${JSON.stringify({ jsonrpc: \"2.0\", method: n.method, params: n.params })}\\n\\n`,\n );\n }\n if (replay?.durationMs !== undefined) await waitUntil(replay.durationMs);\n if (res.destroyed) return;\n res.end(`event: message\\ndata: ${JSON.stringify(answer)}\\n\\n`);\n return;\n }\n if (replay?.durationMs !== undefined) await waitUntil(replay.durationMs);\n if (res.destroyed) return;\n res.writeHead(200, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify(answer));\n}\n\n/** The identity header of each field whose value can fall back (H1). */\nconst IDENTITY_HEADERS: Record<McpDecodeField, string> = {\n testId: \"x-test-id\",\n context: \"x-aimock-context\",\n};\n\n/** A JSON-RPC message object whose method is `tools/call`. */\nfunction isToolsCall(message: unknown): message is Record<string, unknown> {\n return (\n typeof message === \"object\" &&\n message !== null &&\n !Array.isArray(message) &&\n \"method\" in message &&\n message.method === \"tools/call\"\n );\n}\n"],"mappings":"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"}
|
package/dist/mcp-mock.cjs
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
const require_runtime = require('./_virtual/_rolldown/runtime.cjs');
|
|
2
|
+
const require_message_text = require('./message-text.cjs');
|
|
2
3
|
const require_helpers = require('./helpers.cjs');
|
|
4
|
+
const require_record_sanitize = require('./record-sanitize.cjs');
|
|
3
5
|
const require_mcp_handler = require('./mcp-handler.cjs');
|
|
6
|
+
const require_mcp_fakes = require('./mcp-fakes.cjs');
|
|
7
|
+
const require_mcp_recorder = require('./mcp-recorder.cjs');
|
|
4
8
|
let node_http = require("node:http");
|
|
5
9
|
node_http = require_runtime.__toESM(node_http);
|
|
6
10
|
|
|
@@ -10,9 +14,25 @@ var MCPMock = class {
|
|
|
10
14
|
resources = /* @__PURE__ */ new Map();
|
|
11
15
|
prompts = /* @__PURE__ */ new Map();
|
|
12
16
|
sessions = /* @__PURE__ */ new Map();
|
|
17
|
+
/** This mount's fakes; kept across `reset()` so the id counters are never rewound. */
|
|
18
|
+
fakes = new require_mcp_fakes.McpFakeStore();
|
|
19
|
+
/** Fake blocks loaded since the last clear. */
|
|
20
|
+
fakeBlocks = 0;
|
|
21
|
+
/**
|
|
22
|
+
* Decode fallbacks already reported (L2), per live session: one key per
|
|
23
|
+
* field and source, plus one for undecodable query names. Dropped when the
|
|
24
|
+
* session is deleted.
|
|
25
|
+
*/
|
|
26
|
+
decodeWarned = /* @__PURE__ */ new Map();
|
|
13
27
|
server = null;
|
|
14
28
|
journal = null;
|
|
15
29
|
registry = null;
|
|
30
|
+
/** Mount logger for L1, L2 and L10; none (lines dropped) until `setLogger`. */
|
|
31
|
+
logger = null;
|
|
32
|
+
/** T1: recorded timing (`durationMs`, `atMs`) plays at value / speed. */
|
|
33
|
+
replaySpeed = 1;
|
|
34
|
+
/** MR1: the recorder while recording is on, else null (AM1). */
|
|
35
|
+
recorder = null;
|
|
16
36
|
options;
|
|
17
37
|
requestHandler;
|
|
18
38
|
constructor(options) {
|
|
@@ -46,10 +66,140 @@ var MCPMock = class {
|
|
|
46
66
|
});
|
|
47
67
|
return this;
|
|
48
68
|
}
|
|
69
|
+
/**
|
|
70
|
+
* Load MCP fakes from code: one `mcpFakes` value (a block, or an array of
|
|
71
|
+
* blocks), in the fixture-file shape, validated here. One run-time addition
|
|
72
|
+
* (`code#<n>` entry ids). Always appends. Returns the shadowed-entry
|
|
73
|
+
* warnings; on any bad block throws one `McpFakesAddError` and adds nothing.
|
|
74
|
+
*/
|
|
75
|
+
loadFakes(blocks) {
|
|
76
|
+
const sources = Array.isArray(blocks) ? blocks.map((raw, blockIndex) => ({
|
|
77
|
+
source: "code",
|
|
78
|
+
blockIndex,
|
|
79
|
+
raw
|
|
80
|
+
})) : [{
|
|
81
|
+
source: "code",
|
|
82
|
+
blockIndex: null,
|
|
83
|
+
raw: blocks
|
|
84
|
+
}];
|
|
85
|
+
return this.addMcpFakes(sources, { kind: "code" });
|
|
86
|
+
}
|
|
87
|
+
addMcpFakes(blocks, origin) {
|
|
88
|
+
return this.addFakes(blocks, origin);
|
|
89
|
+
}
|
|
90
|
+
/** `addMcpFakes`, plus the internal `record` origin of the MCP recorder. */
|
|
91
|
+
addFakes(blocks, origin) {
|
|
92
|
+
const result = this.fakes.add(blocks, origin);
|
|
93
|
+
this.fakeBlocks += blocks.length;
|
|
94
|
+
return result;
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* The blocks on this mount that apply to `testId` and `context` (spec 6.5,
|
|
98
|
+
* I9), with every entry id and its consumed state for that test id. `null`
|
|
99
|
+
* means "none sent". Read-only: the result is deeply frozen.
|
|
100
|
+
*/
|
|
101
|
+
fakesSnapshot(testId = null, context = null) {
|
|
102
|
+
return this.fakes.snapshot(testId, context);
|
|
103
|
+
}
|
|
104
|
+
clearMcpFakes() {
|
|
105
|
+
this.fakes.clear();
|
|
106
|
+
this.fakeBlocks = 0;
|
|
107
|
+
this.recorder?.reset();
|
|
108
|
+
}
|
|
109
|
+
resetScenarioState(testId) {
|
|
110
|
+
this.fakes.resetState(testId);
|
|
111
|
+
this.recorder?.reset(testId);
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Record a real upstream MCP server (MR1): from now on every request to
|
|
115
|
+
* this mount is forwarded to `config.upstream` (all paths and methods), a
|
|
116
|
+
* fake that applies still answers locally, and each forwarded `tools/list`
|
|
117
|
+
* and `tools/call` is written to an `mcpFakes` file under
|
|
118
|
+
* `config.fixturePath`. `secretValues` and `upstreamAuth` that are
|
|
119
|
+
* `undefined` come from `AIMOCK_RECORD_SECRET_VALUES` and
|
|
120
|
+
* `AIMOCK_MCP_UPSTREAM_AUTH`, as on the CLI and config paths. Throws on an
|
|
121
|
+
* invalid upstream URL, a malformed `upstreamAuth` (or env auth), or a
|
|
122
|
+
* `secretValues` entry shorter than 8 characters.
|
|
123
|
+
*/
|
|
124
|
+
enableRecording(config) {
|
|
125
|
+
let resolved = config;
|
|
126
|
+
if (config.secretValues === void 0 || config.upstreamAuth === void 0) {
|
|
127
|
+
const fromEnv = require_mcp_recorder.mcpRecordEnv(process.env);
|
|
128
|
+
resolved = {
|
|
129
|
+
...config,
|
|
130
|
+
secretValues: config.secretValues ?? fromEnv.secretValues,
|
|
131
|
+
upstreamAuth: config.upstreamAuth ?? fromEnv.upstreamAuth
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
require_record_sanitize.validateSecretValues(resolved.secretValues ?? []);
|
|
135
|
+
const recorder = new require_mcp_recorder.McpRecorder(this.recorderHost(), resolved);
|
|
136
|
+
this.recorder?.close();
|
|
137
|
+
this.recorder = recorder;
|
|
138
|
+
return this;
|
|
139
|
+
}
|
|
140
|
+
/** Stop recording: the mount serves exactly as before `enableRecording`. */
|
|
141
|
+
disableRecording() {
|
|
142
|
+
this.recorder?.close();
|
|
143
|
+
this.recorder = null;
|
|
144
|
+
return this;
|
|
145
|
+
}
|
|
146
|
+
/** @internal MR13: the recorder's write hashes and write listener, or null when not recording. */
|
|
147
|
+
recorderEvents() {
|
|
148
|
+
return this.recorder;
|
|
149
|
+
}
|
|
150
|
+
setLogger(logger) {
|
|
151
|
+
this.logger = logger;
|
|
152
|
+
}
|
|
153
|
+
/** T1: the speed recorded fake timing plays at; a non-positive or non-finite value is ignored. */
|
|
154
|
+
setReplaySpeed(speed) {
|
|
155
|
+
if (Number.isFinite(speed) && speed > 0) this.replaySpeed = speed;
|
|
156
|
+
}
|
|
157
|
+
/** @internal RP2 report part for this mount (src/mcp-fakes-report.ts reads it). */
|
|
158
|
+
fakesReportPart(testId, context, mount) {
|
|
159
|
+
return this.fakes.reportPart(testId, context, mount);
|
|
160
|
+
}
|
|
49
161
|
async handleRequest(req, res, pathname) {
|
|
162
|
+
if (this.recorder) {
|
|
163
|
+
await this.recorder.handle(req, res, mountPathOf(req.url, pathname), pathname);
|
|
164
|
+
return true;
|
|
165
|
+
}
|
|
50
166
|
if (pathname !== "/" && pathname !== "") return false;
|
|
51
|
-
if (req.method
|
|
52
|
-
|
|
167
|
+
if (req.method === "GET") {
|
|
168
|
+
this.answerMethodNotAllowed(req, res);
|
|
169
|
+
return true;
|
|
170
|
+
}
|
|
171
|
+
if (!isServedMethod(req.method)) return false;
|
|
172
|
+
await this.serve(req, res, mountPathOf(req.url, pathname), false);
|
|
173
|
+
return true;
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* Serve one request, in both modes (mounted: a POST or DELETE on the mount
|
|
177
|
+
* root; standalone: any method but GET on any path): count it, run the
|
|
178
|
+
* handler, journal it. A throw from the mount's own code is logged with the
|
|
179
|
+
* identity resolved so far, answered 500 and journaled with its message,
|
|
180
|
+
* never left to a generic catch.
|
|
181
|
+
*/
|
|
182
|
+
async serve(req, res, mount, standalone) {
|
|
183
|
+
const record = require_mcp_handler.newMCPRequestRecord();
|
|
184
|
+
try {
|
|
185
|
+
const body = await require_helpers.readBody(req);
|
|
186
|
+
this.countRequest(req, body);
|
|
187
|
+
await this.requestHandler(req, res, body, mount, record);
|
|
188
|
+
this.journalRequest(req, res, record);
|
|
189
|
+
} catch (err) {
|
|
190
|
+
const thrown = err instanceof Error ? err.message : String(err);
|
|
191
|
+
const line = require_message_text.build(require_message_text.msg`MCPMock request error (${record.identity ? require_mcp_handler.describeMcpIdentity(record.identity) : require_message_text.msg`no identity resolved`}, mount ${require_message_text.plainText(mount)}): ${require_message_text.plainText(thrown)}`);
|
|
192
|
+
if (standalone || !this.logger) console.error(line);
|
|
193
|
+
else this.logger.error(line);
|
|
194
|
+
if (!res.headersSent) {
|
|
195
|
+
res.writeHead(500);
|
|
196
|
+
res.end("Internal server error");
|
|
197
|
+
} else if (!res.writableEnded) res.end();
|
|
198
|
+
this.journalRequest(req, res, record, thrown);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
/** `aimock_mcp_requests_total` by JSON-RPC method (`session/delete` for DELETE). */
|
|
202
|
+
countRequest(req, body) {
|
|
53
203
|
if (this.registry) if (req.method === "DELETE") this.registry.incrementCounter("aimock_mcp_requests_total", { method: "session/delete" });
|
|
54
204
|
else try {
|
|
55
205
|
const parsed = JSON.parse(body);
|
|
@@ -58,19 +208,19 @@ var MCPMock = class {
|
|
|
58
208
|
} catch {
|
|
59
209
|
this.registry.incrementCounter("aimock_mcp_requests_total", { method: "unknown" });
|
|
60
210
|
}
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
fixture: null
|
|
71
|
-
}
|
|
211
|
+
}
|
|
212
|
+
/**
|
|
213
|
+
* B1: answer 405 (`Allow: POST, DELETE`), journaled. Mounted, this is a GET
|
|
214
|
+
* on the mount root; standalone, a GET on any path.
|
|
215
|
+
*/
|
|
216
|
+
answerMethodNotAllowed(req, res) {
|
|
217
|
+
res.writeHead(405, {
|
|
218
|
+
"Content-Type": "application/json",
|
|
219
|
+
Allow: "POST, DELETE"
|
|
72
220
|
});
|
|
73
|
-
|
|
221
|
+
res.end(JSON.stringify({ error: require_message_text.build(require_message_text.msg`Method not allowed: use POST or DELETE`) }));
|
|
222
|
+
req.resume();
|
|
223
|
+
this.journalRequest(req, res, null);
|
|
74
224
|
}
|
|
75
225
|
health() {
|
|
76
226
|
return {
|
|
@@ -83,6 +233,7 @@ var MCPMock = class {
|
|
|
83
233
|
}
|
|
84
234
|
setJournal(journal) {
|
|
85
235
|
this.journal = journal;
|
|
236
|
+
this.fakes.setMaxTestIds(journal.fixtureCountsMaxTestIdsCap);
|
|
86
237
|
}
|
|
87
238
|
setRegistry(registry) {
|
|
88
239
|
this.registry = registry;
|
|
@@ -93,40 +244,20 @@ var MCPMock = class {
|
|
|
93
244
|
const port = this.options.port ?? 0;
|
|
94
245
|
return new Promise((resolve, reject) => {
|
|
95
246
|
const srv = node_http.createServer((req, res) => {
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
req.on("end", () => {
|
|
99
|
-
const body = Buffer.concat(chunks).toString();
|
|
100
|
-
this.requestHandler(req, res, body).then(() => {
|
|
101
|
-
if (this.journal) this.journal.add({
|
|
102
|
-
method: req.method ?? "POST",
|
|
103
|
-
path: req.url ?? "/",
|
|
104
|
-
headers: require_helpers.flattenHeaders(req.headers),
|
|
105
|
-
body: null,
|
|
106
|
-
service: "mcp",
|
|
107
|
-
response: {
|
|
108
|
-
status: res.statusCode,
|
|
109
|
-
fixture: null
|
|
110
|
-
}
|
|
111
|
-
});
|
|
112
|
-
}).catch((err) => {
|
|
247
|
+
if (this.recorder) {
|
|
248
|
+
this.recorder.handle(req, res, "/", "/").catch((err) => {
|
|
113
249
|
console.error("MCPMock request error:", err);
|
|
114
|
-
if (!res.
|
|
115
|
-
res.writeHead(500);
|
|
116
|
-
res.end("Internal server error");
|
|
117
|
-
} else if (!res.writableEnded) res.end();
|
|
118
|
-
if (this.journal) this.journal.add({
|
|
119
|
-
method: req.method ?? "POST",
|
|
120
|
-
path: req.url ?? "/",
|
|
121
|
-
headers: require_helpers.flattenHeaders(req.headers),
|
|
122
|
-
body: null,
|
|
123
|
-
service: "mcp",
|
|
124
|
-
response: {
|
|
125
|
-
status: res.statusCode,
|
|
126
|
-
fixture: null
|
|
127
|
-
}
|
|
128
|
-
});
|
|
250
|
+
if (!res.writableEnded) res.end();
|
|
129
251
|
});
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
if (req.method === "GET") {
|
|
255
|
+
this.answerMethodNotAllowed(req, res);
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
this.serve(req, res, "/", true).catch((err) => {
|
|
259
|
+
console.error("MCPMock request error:", err);
|
|
260
|
+
if (!res.writableEnded) res.end();
|
|
130
261
|
});
|
|
131
262
|
});
|
|
132
263
|
srv.listen(port, host, () => {
|
|
@@ -158,9 +289,106 @@ var MCPMock = class {
|
|
|
158
289
|
this.resources.clear();
|
|
159
290
|
this.prompts.clear();
|
|
160
291
|
this.sessions.clear();
|
|
292
|
+
this.decodeWarned.clear();
|
|
293
|
+
this.clearMcpFakes();
|
|
161
294
|
this.requestHandler = this.buildHandler();
|
|
162
295
|
return this;
|
|
163
296
|
}
|
|
297
|
+
/**
|
|
298
|
+
* The journal entries of one handled request (B2, B3): one per JSON-RPC
|
|
299
|
+
* message (each element of a batch), or one with no body when the request
|
|
300
|
+
* carried none that was read; none without a journal (W5). `error` is what
|
|
301
|
+
* the mount's own code threw while serving it.
|
|
302
|
+
*/
|
|
303
|
+
journalRequest(req, res, record, error) {
|
|
304
|
+
if (!this.journal) return;
|
|
305
|
+
const messages = record?.messages.length ? record.messages : [{
|
|
306
|
+
body: null,
|
|
307
|
+
mcpFake: null,
|
|
308
|
+
error: null
|
|
309
|
+
}];
|
|
310
|
+
for (const message of messages) {
|
|
311
|
+
const failed = message.error ?? error;
|
|
312
|
+
this.journal.add({
|
|
313
|
+
method: req.method ?? "POST",
|
|
314
|
+
path: req.url ?? "/",
|
|
315
|
+
headers: require_helpers.flattenHeaders(req.headers),
|
|
316
|
+
body: message.body,
|
|
317
|
+
service: "mcp",
|
|
318
|
+
...record?.identity ? {
|
|
319
|
+
testId: record.identity.testId ?? require_mcp_fakes.MCP_FAKES_DEFAULT_TEST_ID,
|
|
320
|
+
context: record.identity.context
|
|
321
|
+
} : {},
|
|
322
|
+
response: {
|
|
323
|
+
status: res.statusCode,
|
|
324
|
+
fixture: null,
|
|
325
|
+
...message.mcpFake ? { mcpFake: message.mcpFake } : {},
|
|
326
|
+
...failed !== void 0 && failed !== null ? { error: failed } : {}
|
|
327
|
+
}
|
|
328
|
+
});
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
/**
|
|
332
|
+
* Side channels of a fake outcome (9.3): the failure metric and the L1 /
|
|
333
|
+
* L10 line. A claim that threw (`internal_error`) is counted and logged
|
|
334
|
+
* like a failure, under `MCP_FAKE_INTERNAL_ERROR`.
|
|
335
|
+
*/
|
|
336
|
+
onFakeEvent(evt) {
|
|
337
|
+
this.fakes.logEvent(evt.identity, toReportEvent(evt));
|
|
338
|
+
if (evt.kind === "answered") return;
|
|
339
|
+
this.registry?.incrementCounter("aimock_mcp_fake_failures_total", { code: evt.code });
|
|
340
|
+
if (!this.logger) return;
|
|
341
|
+
const where = require_message_text.msg`mount ${require_message_text.plainText(evt.mount)}`;
|
|
342
|
+
const kind = evt.kind.replace("_", " ");
|
|
343
|
+
const line = evt.kind === "evicted" ? require_message_text.msg`MCP-FAKE: evicted testId ${require_message_text.quote(evt.testId)} (cap ${evt.cap}) for tools/call ${require_message_text.plainText(evt.tool)} (${where})` : require_message_text.msg`MCP-FAKE: ${require_message_text.fixed(kind)} for tools/call ${require_message_text.plainText(evt.tool)} (${require_mcp_handler.describeMcpIdentity(evt.identity)}, ${where}): ${require_message_text.plainText(evt.message)}`;
|
|
344
|
+
this.logger.error(require_message_text.build(line));
|
|
345
|
+
}
|
|
346
|
+
/** RP2, C9: a tools/call no fake applied to is always logged, with who answered it. */
|
|
347
|
+
logUnfaked(evt) {
|
|
348
|
+
this.fakes.logEvent(evt.identity, {
|
|
349
|
+
outcome: "unfaked",
|
|
350
|
+
answeredBy: evt.answeredBy,
|
|
351
|
+
tool: evt.tool,
|
|
352
|
+
args: evt.args,
|
|
353
|
+
context: evt.identity.context,
|
|
354
|
+
mount: evt.mount
|
|
355
|
+
});
|
|
356
|
+
}
|
|
357
|
+
/**
|
|
358
|
+
* L2: one warning per session for each identity field and source whose
|
|
359
|
+
* value is not valid percent-encoding (H1), naming that source and value,
|
|
360
|
+
* and one for query names that are not valid percent-encoding.
|
|
361
|
+
*/
|
|
362
|
+
onDecodeFallback(sessionId, fallback) {
|
|
363
|
+
let warned = this.decodeWarned.get(sessionId);
|
|
364
|
+
if (!warned) {
|
|
365
|
+
warned = /* @__PURE__ */ new Set();
|
|
366
|
+
this.decodeWarned.set(sessionId, warned);
|
|
367
|
+
}
|
|
368
|
+
const key = fallback.kind === "names" ? "names" : `${fallback.field}:${fallback.source}`;
|
|
369
|
+
if (warned.has(key)) return;
|
|
370
|
+
warned.add(key);
|
|
371
|
+
const session = require_message_text.quote(sessionId);
|
|
372
|
+
if (fallback.kind === "names") {
|
|
373
|
+
const one = fallback.count === 1;
|
|
374
|
+
this.logger?.warn(require_message_text.build(require_message_text.msg`MCP-FAKE: ${fallback.count} query parameter ${require_message_text.fixed(one ? "name" : "names")} on session ${session} ${require_message_text.fixed(one ? "is" : "are")} not valid percent-encoding; ${require_message_text.fixed(one ? "it matches" : "they match")} no identity field`));
|
|
375
|
+
return;
|
|
376
|
+
}
|
|
377
|
+
const label = DECODE_LABELS[fallback.field][fallback.source];
|
|
378
|
+
const fate = fallback.used ? "it is used as sent" : "it is ignored (the header value is used)";
|
|
379
|
+
this.logger?.warn(require_message_text.build(require_message_text.msg`MCP-FAKE: ${require_message_text.fixed(label)} value ${require_message_text.quote(fallback.raw)} on session ${session} is not valid percent-encoding; ${require_message_text.fixed(fate)}`));
|
|
380
|
+
}
|
|
381
|
+
/** The recorder's view of this mount (fakes, journal, logger, replay speed, side channels). */
|
|
382
|
+
recorderHost() {
|
|
383
|
+
return {
|
|
384
|
+
fakes: this.fakes,
|
|
385
|
+
addMcpFakes: (blocks, origin) => this.addFakes(blocks, origin),
|
|
386
|
+
journalEntry: (entry) => void this.journal?.add(entry),
|
|
387
|
+
logger: () => this.logger,
|
|
388
|
+
replaySpeed: () => this.replaySpeed,
|
|
389
|
+
onFakeEvent: (evt) => this.onFakeEvent(evt)
|
|
390
|
+
};
|
|
391
|
+
}
|
|
164
392
|
buildHandler() {
|
|
165
393
|
return require_mcp_handler.createMCPRequestHandler({
|
|
166
394
|
serverInfo: this.options.serverInfo ?? {
|
|
@@ -170,10 +398,61 @@ var MCPMock = class {
|
|
|
170
398
|
tools: this.tools,
|
|
171
399
|
resources: this.resources,
|
|
172
400
|
prompts: this.prompts,
|
|
173
|
-
sessions: this.sessions
|
|
401
|
+
sessions: this.sessions,
|
|
402
|
+
fakes: this.fakes,
|
|
403
|
+
hasFakes: () => this.fakeBlocks > 0,
|
|
404
|
+
onFakeEvent: (evt) => this.onFakeEvent(evt),
|
|
405
|
+
onDecodeFallback: (sessionId, fallback) => this.onDecodeFallback(sessionId, fallback),
|
|
406
|
+
onSessionClosed: (sessionId) => void this.decodeWarned.delete(sessionId),
|
|
407
|
+
onUnfaked: (evt) => this.logUnfaked(evt),
|
|
408
|
+
replaySpeed: () => this.replaySpeed
|
|
174
409
|
});
|
|
175
410
|
}
|
|
176
411
|
};
|
|
412
|
+
/** RP2: a fake outcome as an event-log entry (the store assigns `seq`). */
|
|
413
|
+
function toReportEvent(evt) {
|
|
414
|
+
const base = {
|
|
415
|
+
tool: evt.tool,
|
|
416
|
+
args: evt.args,
|
|
417
|
+
context: evt.identity.context,
|
|
418
|
+
mount: evt.mount
|
|
419
|
+
};
|
|
420
|
+
return evt.kind === "answered" ? {
|
|
421
|
+
...base,
|
|
422
|
+
outcome: "answered",
|
|
423
|
+
entryId: evt.entryId
|
|
424
|
+
} : {
|
|
425
|
+
...base,
|
|
426
|
+
outcome: evt.kind,
|
|
427
|
+
code: evt.code
|
|
428
|
+
};
|
|
429
|
+
}
|
|
430
|
+
/** B1: the only methods the mounted mount root serves; every other one is 405. */
|
|
431
|
+
function isServedMethod(method) {
|
|
432
|
+
return method === "POST" || method === "DELETE";
|
|
433
|
+
}
|
|
434
|
+
/** The L2 label of each identity field's header and query source. */
|
|
435
|
+
const DECODE_LABELS = {
|
|
436
|
+
testId: {
|
|
437
|
+
header: "X-Test-Id header",
|
|
438
|
+
query: "?testId= query parameter"
|
|
439
|
+
},
|
|
440
|
+
context: {
|
|
441
|
+
header: "X-AIMock-Context header",
|
|
442
|
+
query: "?context= query parameter"
|
|
443
|
+
}
|
|
444
|
+
};
|
|
445
|
+
/**
|
|
446
|
+
* The path a mount is served at (I9): the request path without the
|
|
447
|
+
* sub-path the server handed the mount (`/mcp` for `/mcp` or `/mcp/`), `/`
|
|
448
|
+
* at the root.
|
|
449
|
+
*/
|
|
450
|
+
function mountPathOf(url, subPath) {
|
|
451
|
+
const full = new URL(url ?? "/", "http://mount.invalid").pathname;
|
|
452
|
+
const sub = subPath === "" ? "/" : subPath;
|
|
453
|
+
const mount = full.endsWith(sub) ? full.slice(0, full.length - sub.length) : full;
|
|
454
|
+
return mount === "" ? "/" : mount;
|
|
455
|
+
}
|
|
177
456
|
|
|
178
457
|
//#endregion
|
|
179
458
|
exports.MCPMock = MCPMock;
|