@copilotkit/aimock 1.42.1 → 1.44.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 +65 -0
- package/README.md +108 -16
- package/dist/agui-handler.d.ts.map +1 -1
- package/dist/agui-mock.cjs +1 -1
- package/dist/agui-mock.cjs.map +1 -1
- package/dist/agui-mock.d.cts.map +1 -1
- package/dist/agui-mock.d.ts.map +1 -1
- package/dist/agui-mock.js +1 -1
- package/dist/agui-mock.js.map +1 -1
- package/dist/agui-recorder.cjs +33 -11
- package/dist/agui-recorder.cjs.map +1 -1
- package/dist/agui-recorder.d.cts +10 -1
- package/dist/agui-recorder.d.cts.map +1 -1
- package/dist/agui-recorder.d.ts +10 -1
- package/dist/agui-recorder.d.ts.map +1 -1
- package/dist/agui-recorder.js +33 -11
- package/dist/agui-recorder.js.map +1 -1
- package/dist/agui-types.d.cts +1 -0
- package/dist/agui-types.d.cts.map +1 -1
- package/dist/agui-types.d.ts +1 -0
- package/dist/agui-types.d.ts.map +1 -1
- package/dist/aimock-cli.cjs +38 -10
- package/dist/aimock-cli.cjs.map +1 -1
- package/dist/aimock-cli.d.cts +5 -0
- package/dist/aimock-cli.d.cts.map +1 -1
- package/dist/aimock-cli.d.ts +5 -0
- package/dist/aimock-cli.d.ts.map +1 -1
- package/dist/aimock-cli.js +38 -10
- package/dist/aimock-cli.js.map +1 -1
- package/dist/batches.cjs +458 -0
- package/dist/batches.cjs.map +1 -0
- package/dist/batches.d.cts +57 -0
- package/dist/batches.d.cts.map +1 -0
- package/dist/batches.d.ts +57 -0
- package/dist/batches.d.ts.map +1 -0
- package/dist/batches.js +453 -0
- package/dist/batches.js.map +1 -0
- package/dist/bedrock-converse.cjs +44 -12
- 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 +46 -14
- package/dist/bedrock-converse.js.map +1 -1
- package/dist/bedrock.cjs +10 -8
- 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 +12 -10
- package/dist/bedrock.js.map +1 -1
- package/dist/byteplus-video.cjs +3 -3
- package/dist/byteplus-video.cjs.map +1 -1
- package/dist/byteplus-video.js +4 -4
- package/dist/byteplus-video.js.map +1 -1
- package/dist/chaos.cjs +625 -72
- package/dist/chaos.cjs.map +1 -1
- package/dist/chaos.d.cts +164 -7
- package/dist/chaos.d.cts.map +1 -1
- package/dist/chaos.d.ts +164 -7
- package/dist/chaos.d.ts.map +1 -1
- package/dist/chaos.js +616 -74
- package/dist/chaos.js.map +1 -1
- package/dist/cli.cjs +131 -30
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +132 -31
- package/dist/cli.js.map +1 -1
- package/dist/cohere.cjs +37 -9
- 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 +39 -11
- package/dist/cohere.js.map +1 -1
- package/dist/config-loader.cjs +1 -1
- package/dist/config-loader.js +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/convert.cjs +1 -1
- package/dist/convert.cjs.map +1 -1
- package/dist/convert.js +1 -1
- package/dist/convert.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/elevenlabs-audio.cjs +3 -3
- package/dist/elevenlabs-audio.cjs.map +1 -1
- package/dist/elevenlabs-audio.js +4 -4
- package/dist/elevenlabs-audio.js.map +1 -1
- package/dist/elevenlabs-voice.cjs +1053 -0
- package/dist/elevenlabs-voice.cjs.map +1 -0
- package/dist/elevenlabs-voice.d.cts +72 -0
- package/dist/elevenlabs-voice.d.cts.map +1 -0
- package/dist/elevenlabs-voice.d.ts +72 -0
- package/dist/elevenlabs-voice.d.ts.map +1 -0
- package/dist/elevenlabs-voice.js +1048 -0
- package/dist/elevenlabs-voice.js.map +1 -0
- package/dist/embeddings.cjs +26 -5
- 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 +27 -6
- package/dist/embeddings.js.map +1 -1
- package/dist/fal-audio.cjs +3 -3
- package/dist/fal-audio.cjs.map +1 -1
- package/dist/fal-audio.js +4 -4
- package/dist/fal-audio.js.map +1 -1
- package/dist/fal.cjs +28 -4
- package/dist/fal.cjs.map +1 -1
- package/dist/fal.d.cts +16 -1
- package/dist/fal.d.cts.map +1 -1
- package/dist/fal.d.ts +16 -1
- package/dist/fal.d.ts.map +1 -1
- package/dist/fal.js +28 -5
- package/dist/fal.js.map +1 -1
- package/dist/files.cjs +1069 -0
- package/dist/files.cjs.map +1 -0
- package/dist/files.d.cts +68 -0
- package/dist/files.d.cts.map +1 -0
- package/dist/files.d.ts +68 -0
- package/dist/files.d.ts.map +1 -0
- package/dist/files.js +1056 -0
- package/dist/files.js.map +1 -0
- package/dist/fine-tuning.cjs +1033 -0
- package/dist/fine-tuning.cjs.map +1 -0
- package/dist/fine-tuning.d.cts +96 -0
- package/dist/fine-tuning.d.cts.map +1 -0
- package/dist/fine-tuning.d.ts +96 -0
- package/dist/fine-tuning.d.ts.map +1 -0
- package/dist/fine-tuning.js +1026 -0
- package/dist/fine-tuning.js.map +1 -0
- 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 +0 -0
- package/dist/fixture-loader.cjs.map +1 -1
- package/dist/fixture-loader.d.cts +131 -5
- package/dist/fixture-loader.d.cts.map +1 -1
- package/dist/fixture-loader.d.ts +131 -5
- package/dist/fixture-loader.d.ts.map +1 -1
- package/dist/fixture-loader.js +0 -0
- package/dist/fixture-loader.js.map +1 -1
- package/dist/gemini-embeddings.cjs +44 -4
- package/dist/gemini-embeddings.cjs.map +1 -1
- package/dist/gemini-embeddings.js +46 -6
- package/dist/gemini-embeddings.js.map +1 -1
- package/dist/gemini-interactions.cjs +51 -7
- 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 +53 -9
- package/dist/gemini-interactions.js.map +1 -1
- package/dist/gemini.cjs +19 -8
- 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 +21 -10
- package/dist/gemini.js.map +1 -1
- package/dist/grok-video.cjs +14 -14
- package/dist/grok-video.cjs.map +1 -1
- package/dist/grok-video.js +15 -15
- package/dist/grok-video.js.map +1 -1
- package/dist/helpers.cjs +497 -10
- package/dist/helpers.cjs.map +1 -1
- package/dist/helpers.d.cts +38 -2
- package/dist/helpers.d.cts.map +1 -1
- package/dist/helpers.d.ts +38 -2
- package/dist/helpers.d.ts.map +1 -1
- package/dist/helpers.js +487 -12
- package/dist/helpers.js.map +1 -1
- package/dist/images.cjs +57 -125
- package/dist/images.cjs.map +1 -1
- package/dist/images.d.cts.map +1 -1
- package/dist/images.d.ts.map +1 -1
- package/dist/images.js +58 -126
- package/dist/images.js.map +1 -1
- package/dist/index.cjs +71 -9
- package/dist/index.d.cts +17 -7
- package/dist/index.d.ts +17 -7
- package/dist/index.js +21 -11
- package/dist/jest.cjs +24 -12
- package/dist/jest.cjs.map +1 -1
- package/dist/jest.d.cts +6 -3
- package/dist/jest.d.cts.map +1 -1
- package/dist/jest.d.ts +6 -3
- package/dist/jest.d.ts.map +1 -1
- package/dist/jest.js +25 -13
- package/dist/jest.js.map +1 -1
- package/dist/journal.cjs +61 -4
- package/dist/journal.cjs.map +1 -1
- package/dist/journal.d.cts +60 -3
- package/dist/journal.d.cts.map +1 -1
- package/dist/journal.d.ts +60 -3
- package/dist/journal.d.ts.map +1 -1
- package/dist/journal.js +61 -5
- package/dist/journal.js.map +1 -1
- package/dist/live-fixture.cjs +1192 -0
- package/dist/live-fixture.cjs.map +1 -0
- package/dist/live-fixture.js +1187 -0
- package/dist/live-fixture.js.map +1 -0
- package/dist/live-recorder.cjs +256 -0
- package/dist/live-recorder.cjs.map +1 -0
- package/dist/live-recorder.js +254 -0
- package/dist/live-recorder.js.map +1 -0
- package/dist/live-replay.cjs +286 -0
- package/dist/live-replay.cjs.map +1 -0
- package/dist/live-replay.js +285 -0
- package/dist/live-replay.js.map +1 -0
- package/dist/live-sanitize.cjs +196 -0
- package/dist/live-sanitize.cjs.map +1 -0
- package/dist/live-sanitize.js +196 -0
- package/dist/live-sanitize.js.map +1 -0
- package/dist/live-types.d.cts +71 -0
- package/dist/live-types.d.cts.map +1 -0
- package/dist/live-types.d.ts +71 -0
- package/dist/live-types.d.ts.map +1 -0
- package/dist/llmock.cjs +132 -49
- package/dist/llmock.cjs.map +1 -1
- package/dist/llmock.d.cts +51 -5
- package/dist/llmock.d.cts.map +1 -1
- package/dist/llmock.d.ts +51 -5
- package/dist/llmock.d.ts.map +1 -1
- package/dist/llmock.js +133 -50
- package/dist/llmock.js.map +1 -1
- package/dist/mcp-fakes-mount.cjs +216 -0
- package/dist/mcp-fakes-mount.cjs.map +1 -0
- package/dist/mcp-fakes-mount.js +210 -0
- package/dist/mcp-fakes-mount.js.map +1 -0
- package/dist/mcp-fakes.cjs +1619 -0
- package/dist/mcp-fakes.cjs.map +1 -0
- package/dist/mcp-fakes.d.cts +101 -0
- package/dist/mcp-fakes.d.cts.map +1 -0
- package/dist/mcp-fakes.d.ts +101 -0
- package/dist/mcp-fakes.d.ts.map +1 -0
- package/dist/mcp-fakes.js +1614 -0
- package/dist/mcp-fakes.js.map +1 -0
- package/dist/mcp-handler.cjs +331 -17
- package/dist/mcp-handler.cjs.map +1 -1
- package/dist/mcp-handler.js +330 -18
- package/dist/mcp-handler.js.map +1 -1
- package/dist/mcp-mock.cjs +217 -49
- package/dist/mcp-mock.cjs.map +1 -1
- package/dist/mcp-mock.d.cts +66 -1
- package/dist/mcp-mock.d.cts.map +1 -1
- package/dist/mcp-mock.d.ts +66 -1
- package/dist/mcp-mock.d.ts.map +1 -1
- package/dist/mcp-mock.js +218 -50
- package/dist/mcp-mock.js.map +1 -1
- 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 +49 -7
- 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 +51 -9
- package/dist/messages.js.map +1 -1
- package/dist/metrics.cjs +278 -45
- package/dist/metrics.cjs.map +1 -1
- package/dist/metrics.d.cts +17 -7
- package/dist/metrics.d.cts.map +1 -1
- package/dist/metrics.d.ts +17 -7
- package/dist/metrics.d.ts.map +1 -1
- package/dist/metrics.js +270 -29
- package/dist/metrics.js.map +1 -1
- package/dist/moderation.cjs +10 -1
- package/dist/moderation.cjs.map +1 -1
- package/dist/moderation.d.cts.map +1 -1
- package/dist/moderation.d.ts.map +1 -1
- package/dist/moderation.js +10 -1
- package/dist/moderation.js.map +1 -1
- package/dist/ollama.cjs +10 -6
- 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 +11 -7
- 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 +9 -9
- 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 +10 -10
- package/dist/openrouter-video.js.map +1 -1
- package/dist/recorder.cjs +54 -10
- package/dist/recorder.cjs.map +1 -1
- package/dist/recorder.d.cts.map +1 -1
- package/dist/recorder.d.ts.map +1 -1
- package/dist/recorder.js +52 -12
- package/dist/recorder.js.map +1 -1
- package/dist/responses.cjs +47 -7
- package/dist/responses.cjs.map +1 -1
- package/dist/responses.d.cts.map +1 -1
- package/dist/responses.d.ts.map +1 -1
- package/dist/responses.js +48 -9
- 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 +21 -34
- package/dist/router.cjs.map +1 -1
- package/dist/router.d.cts +4 -1
- package/dist/router.d.cts.map +1 -1
- package/dist/router.d.ts +4 -1
- package/dist/router.d.ts.map +1 -1
- package/dist/router.js +21 -35
- package/dist/router.js.map +1 -1
- package/dist/server.cjs +1252 -777
- package/dist/server.cjs.map +1 -1
- package/dist/server.d.cts +5 -1
- package/dist/server.d.cts.map +1 -1
- package/dist/server.d.ts +5 -1
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +1185 -710
- package/dist/server.js.map +1 -1
- package/dist/speech.cjs +6 -4
- package/dist/speech.cjs.map +1 -1
- package/dist/speech.js +7 -5
- package/dist/speech.js.map +1 -1
- package/dist/suite.cjs +1 -1
- package/dist/suite.js +1 -1
- package/dist/transcription.cjs +6 -6
- 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 +7 -7
- 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 +320 -17
- package/dist/types.d.cts.map +1 -1
- package/dist/types.d.ts +320 -17
- 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 +983 -0
- package/dist/validate-cli.cjs.map +1 -0
- package/dist/validate-cli.js +982 -0
- package/dist/validate-cli.js.map +1 -0
- 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/vector-types.d.ts.map +1 -1
- package/dist/veo-video.cjs +13 -13
- package/dist/veo-video.cjs.map +1 -1
- package/dist/veo-video.js +14 -14
- package/dist/veo-video.js.map +1 -1
- package/dist/video.cjs +23 -4
- package/dist/video.cjs.map +1 -1
- package/dist/video.d.cts +1 -1
- package/dist/video.d.cts.map +1 -1
- package/dist/video.d.ts +1 -1
- package/dist/video.d.ts.map +1 -1
- package/dist/video.js +24 -5
- package/dist/video.js.map +1 -1
- package/dist/vitest.cjs +23 -11
- package/dist/vitest.cjs.map +1 -1
- package/dist/vitest.d.cts +5 -2
- package/dist/vitest.d.cts.map +1 -1
- package/dist/vitest.d.ts +5 -2
- package/dist/vitest.d.ts.map +1 -1
- package/dist/vitest.js +25 -13
- package/dist/vitest.js.map +1 -1
- package/dist/watcher.cjs +2 -1
- package/dist/watcher.cjs.map +1 -1
- package/dist/watcher.js +2 -1
- package/dist/watcher.js.map +1 -1
- package/dist/ws-framing.cjs +150 -35
- package/dist/ws-framing.cjs.map +1 -1
- package/dist/ws-framing.d.cts +18 -3
- package/dist/ws-framing.d.cts.map +1 -1
- package/dist/ws-framing.d.ts +18 -3
- package/dist/ws-framing.d.ts.map +1 -1
- package/dist/ws-framing.js +151 -36
- package/dist/ws-framing.js.map +1 -1
- package/dist/ws-gemini-live.cjs +2 -2
- package/dist/ws-gemini-live.js +2 -2
- package/dist/ws-live.cjs +401 -0
- package/dist/ws-live.cjs.map +1 -0
- package/dist/ws-live.js +400 -0
- package/dist/ws-live.js.map +1 -0
- package/dist/ws-realtime.cjs +46 -2
- package/dist/ws-realtime.cjs.map +1 -1
- package/dist/ws-realtime.d.cts.map +1 -1
- package/dist/ws-realtime.d.ts.map +1 -1
- package/dist/ws-realtime.js +46 -2
- package/dist/ws-realtime.js.map +1 -1
- package/dist/ws-responses.cjs +16 -4
- package/dist/ws-responses.cjs.map +1 -1
- package/dist/ws-responses.js +16 -4
- package/dist/ws-responses.js.map +1 -1
- package/dist/ws-upstream.cjs +118 -0
- package/dist/ws-upstream.cjs.map +1 -0
- package/dist/ws-upstream.js +115 -0
- package/dist/ws-upstream.js.map +1 -0
- package/fixtures/openai-live-client.json +390 -0
- package/fixtures/openai-live-managed.json +709 -0
- package/package.json +4 -1
|
@@ -0,0 +1,1614 @@
|
|
|
1
|
+
import { DEFAULT_TEST_ID, MCP_FAKES_ECHO_LIMIT } from "./constants.js";
|
|
2
|
+
import { build, fixed, fullText, jsonText, msg, quote, quoteJson } from "./message-text.js";
|
|
3
|
+
import { FixtureLoadError } from "./fixture-loader.js";
|
|
4
|
+
|
|
5
|
+
//#region src/mcp-fakes.ts
|
|
6
|
+
/**
|
|
7
|
+
* MCP fakes: the fake model core.
|
|
8
|
+
*
|
|
9
|
+
* Pure module, no I/O. It owns block validation (the bad-block cases (a)-(j)
|
|
10
|
+
* listed in `MCP_FAKES_LOAD_RULES`), entry ids and the per-mount counters in
|
|
11
|
+
* them, block-id and entry-id uniqueness per mount, scope matching, scope
|
|
12
|
+
* tiers, argument equality, the synchronous claim step with its mismatch and
|
|
13
|
+
* exhausted details (`firstDifference`), consumption state per test id with
|
|
14
|
+
* its FIFO cap, the undeclared-tool policy, and the fake tools for
|
|
15
|
+
* `tools/list`. One `McpFakeStore` serves one MCP mount.
|
|
16
|
+
*/
|
|
17
|
+
/** Default `mount` path of a block. */
|
|
18
|
+
const MCP_FAKES_DEFAULT_MOUNT = "/mcp";
|
|
19
|
+
/**
|
|
20
|
+
* Name of the consumption state shared by requests that supply no test id:
|
|
21
|
+
* the journal's `DEFAULT_TEST_ID`. The store keeps that state apart from an
|
|
22
|
+
* explicitly supplied test id equal to this string;
|
|
23
|
+
* `resetState(MCP_FAKES_DEFAULT_TEST_ID)` resets both.
|
|
24
|
+
*/
|
|
25
|
+
const MCP_FAKES_DEFAULT_TEST_ID = DEFAULT_TEST_ID;
|
|
26
|
+
/**
|
|
27
|
+
* Default FIFO cap on distinct test ids (`McpFakeStoreOptions.maxTestIds`),
|
|
28
|
+
* for a mount with no journal. A mount with a journal mirrors the journal's
|
|
29
|
+
* effective cap (`fixtureCountsMaxTestIdsCap`): 500 under `createServer` unless
|
|
30
|
+
* the option is set, and `0` (unbounded) for a bare `Journal` or an explicit `0`.
|
|
31
|
+
*/
|
|
32
|
+
const MCP_FAKES_DEFAULT_MAX_TEST_IDS = 500;
|
|
33
|
+
/**
|
|
34
|
+
* Deepest nesting of objects and arrays accepted in an `mcpFakes` value: the
|
|
35
|
+
* value itself (a block, or the array of blocks) is level 1. A container
|
|
36
|
+
* below this level is not copied; the input snapshot holds a "nested deeper
|
|
37
|
+
* than 64 levels" value there, which the check of the enclosing field reports
|
|
38
|
+
* as its own bad block.
|
|
39
|
+
*/
|
|
40
|
+
const MCP_FAKES_MAX_DEPTH = 64;
|
|
41
|
+
/** The reason a container below `MCP_FAKES_MAX_DEPTH` levels is not copied. */
|
|
42
|
+
const DEPTH_REASON = msg`nested deeper than ${MCP_FAKES_MAX_DEPTH} levels`;
|
|
43
|
+
/** Longest `firstDifference` text (path plus both values), cut the same way. */
|
|
44
|
+
const DIFF_LIMIT = 1e3;
|
|
45
|
+
const BLOCK_KEYS = new Set([
|
|
46
|
+
"scope",
|
|
47
|
+
"mount",
|
|
48
|
+
"undeclaredTools",
|
|
49
|
+
"tools"
|
|
50
|
+
]);
|
|
51
|
+
const TOOL_KEYS = new Set([
|
|
52
|
+
"name",
|
|
53
|
+
"description",
|
|
54
|
+
"inputSchema",
|
|
55
|
+
"calls"
|
|
56
|
+
]);
|
|
57
|
+
const CALL_KEYS = new Set([
|
|
58
|
+
"args",
|
|
59
|
+
"anyArgs",
|
|
60
|
+
"result",
|
|
61
|
+
"error",
|
|
62
|
+
"id"
|
|
63
|
+
]);
|
|
64
|
+
const SCOPE_KEYS = new Set(["testId", "context"]);
|
|
65
|
+
const TIER_ORDER = [
|
|
66
|
+
"testId+context",
|
|
67
|
+
"testId",
|
|
68
|
+
"context",
|
|
69
|
+
"shared"
|
|
70
|
+
];
|
|
71
|
+
/**
|
|
72
|
+
* The error a failed add throws. It is a `FixtureLoadError` whose `rule`,
|
|
73
|
+
* `file`, `blockId` and `entryId` are those of the first error, and whose
|
|
74
|
+
* `message` is the first error's message plus the count of the others.
|
|
75
|
+
* `errors` holds every error found in the input, in order (the first
|
|
76
|
+
* included; see `validateBlock` for the checks that stop early), and
|
|
77
|
+
* `warnings` the shadowed-entry warnings of the blocks that passed every
|
|
78
|
+
* check. Both are frozen, non-writable arrays; `toJSON()` returns copies.
|
|
79
|
+
*/
|
|
80
|
+
var McpFakesAddError = class extends FixtureLoadError {
|
|
81
|
+
name = "McpFakesAddError";
|
|
82
|
+
constructor(errors, warnings) {
|
|
83
|
+
const first = errors[0];
|
|
84
|
+
if (!first) throw new TypeError(build(msg`McpFakesAddError needs at least one error`));
|
|
85
|
+
super({
|
|
86
|
+
rule: first.rule,
|
|
87
|
+
file: first.file,
|
|
88
|
+
blockId: first.blockId,
|
|
89
|
+
entryId: first.entryId,
|
|
90
|
+
detail: ""
|
|
91
|
+
});
|
|
92
|
+
const more = errors.length - 1;
|
|
93
|
+
this.message = more > 0 ? build(msg`${fixed(first.message)} (and ${more} more ${fixed(more === 1 ? "error" : "errors")})`) : first.message;
|
|
94
|
+
Object.defineProperty(this, "errors", {
|
|
95
|
+
value: Object.freeze([...errors]),
|
|
96
|
+
enumerable: true,
|
|
97
|
+
writable: false
|
|
98
|
+
});
|
|
99
|
+
Object.defineProperty(this, "warnings", {
|
|
100
|
+
value: Object.freeze(warnings.map((w) => Object.freeze({ ...w }))),
|
|
101
|
+
enumerable: true,
|
|
102
|
+
writable: false
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
toJSON() {
|
|
106
|
+
return {
|
|
107
|
+
...super.toJSON(),
|
|
108
|
+
name: this.name,
|
|
109
|
+
errors: this.errors.map((e) => e.toJSON()),
|
|
110
|
+
warnings: this.warnings.map((w) => ({ ...w }))
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
};
|
|
114
|
+
/** An object whose prototype is `Object.prototype` or `null` (not an array). */
|
|
115
|
+
function isPlainObject(value) {
|
|
116
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
|
|
117
|
+
const proto = Object.getPrototypeOf(value);
|
|
118
|
+
return proto === Object.prototype || proto === null;
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* The one own-key listing for input data: own, enumerable, string keys.
|
|
122
|
+
* Inherited names (`constructor`, `toString`, `__proto__`) are never listed.
|
|
123
|
+
*
|
|
124
|
+
* @internal
|
|
125
|
+
*/
|
|
126
|
+
function ownKeys(obj) {
|
|
127
|
+
return Object.keys(obj);
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* The one key check: `key` is an own property of `obj`. Unlike `key in obj`,
|
|
131
|
+
* it never sees the prototype chain, so `hasOwnKey({}, "constructor")` is
|
|
132
|
+
* false.
|
|
133
|
+
*
|
|
134
|
+
* @internal
|
|
135
|
+
*/
|
|
136
|
+
function hasOwnKey(obj, key) {
|
|
137
|
+
return Object.prototype.hasOwnProperty.call(obj, key);
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* A user value as one message part: a string is JSON-quoted (quotes and
|
|
141
|
+
* control chars escaped), any other value is rendered as JSON (or as
|
|
142
|
+
* `undefined`, `NaN`, `10n`, ... where JSON cannot at the top level), and a
|
|
143
|
+
* snapshot placeholder, a nested bigint, a cycle, a container too deep, a
|
|
144
|
+
* symbol or function, and a value whose reading throws each as a `<reason>`
|
|
145
|
+
* placeholder whose user text is quoted (see `render`). The builder cuts it
|
|
146
|
+
* once, to its share and to no more than its max, never inside a JSON escape
|
|
147
|
+
* (`\n`, `\u0001`). Whatever the caller's code throws is caught; an error of
|
|
148
|
+
* aimock's own is not.
|
|
149
|
+
*/
|
|
150
|
+
function shown(value, max = MCP_FAKES_ECHO_LIMIT) {
|
|
151
|
+
return jsonText(render(value), max);
|
|
152
|
+
}
|
|
153
|
+
function render(value) {
|
|
154
|
+
if (value === void 0) return "undefined";
|
|
155
|
+
if (typeof value === "number" && !Number.isFinite(value)) return String(value);
|
|
156
|
+
if (typeof value === "bigint") return `${value}n`;
|
|
157
|
+
return renderMember(() => value, "", []) ?? notJson(value);
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* Thrown by `userCode` when code the caller supplied throws: a `toJSON`, a
|
|
161
|
+
* getter, a `Proxy` trap, a `toString`. The renderer catches only this, so a
|
|
162
|
+
* defect in aimock's own rendering code is never hidden as a placeholder.
|
|
163
|
+
*/
|
|
164
|
+
var UserCodeThrew = class {
|
|
165
|
+
constructor(error) {
|
|
166
|
+
this.error = error;
|
|
167
|
+
}
|
|
168
|
+
};
|
|
169
|
+
/**
|
|
170
|
+
* `run()`, which touches the caller's value and so may run the caller's code;
|
|
171
|
+
* whatever that code throws comes out as a `UserCodeThrew`.
|
|
172
|
+
*/
|
|
173
|
+
function userCode(run) {
|
|
174
|
+
try {
|
|
175
|
+
return run();
|
|
176
|
+
} catch (error) {
|
|
177
|
+
throw new UserCodeThrew(error);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
/** A placeholder in a rendered value: `<reason>`, the reason's text whole. */
|
|
181
|
+
function placeholder(reason) {
|
|
182
|
+
return `<${fullText(reason)}>`;
|
|
183
|
+
}
|
|
184
|
+
/** The reason a rendered value holds where it reaches one of its own containers. */
|
|
185
|
+
const CIRCULAR_REASON = msg`a circular reference`;
|
|
186
|
+
/**
|
|
187
|
+
* A value that `renderJson` gives no JSON for (a symbol, a function): a
|
|
188
|
+
* placeholder that names its type and, when it has one, quotes its text.
|
|
189
|
+
*/
|
|
190
|
+
function notJson(value) {
|
|
191
|
+
const reason = msg`not a JSON value (got ${fixed(typeof value)})`;
|
|
192
|
+
let text;
|
|
193
|
+
try {
|
|
194
|
+
text = userCode(() => String(value));
|
|
195
|
+
} catch (err) {
|
|
196
|
+
if (!(err instanceof UserCodeThrew)) throw err;
|
|
197
|
+
return placeholder(reason);
|
|
198
|
+
}
|
|
199
|
+
return placeholder(msg`${reason}: ${quote(text)}`);
|
|
200
|
+
}
|
|
201
|
+
/**
|
|
202
|
+
* One value of a rendered value, read by `read` (which may run the caller's
|
|
203
|
+
* code), rendered by `renderJson`. Where the caller's code throws while it is
|
|
204
|
+
* read or rendered, it renders as an "unreadable value" placeholder that
|
|
205
|
+
* quotes what was thrown, and the rest of the value is kept.
|
|
206
|
+
*/
|
|
207
|
+
function renderMember(read, key, stack) {
|
|
208
|
+
try {
|
|
209
|
+
return renderJson(userCode(read), key, stack);
|
|
210
|
+
} catch (err) {
|
|
211
|
+
if (!(err instanceof UserCodeThrew)) throw err;
|
|
212
|
+
return placeholder(msg`an unreadable value (reading it threw ${describeThrown(err.error)})`);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
/**
|
|
216
|
+
* `value` as `JSON.stringify` renders it, except that a snapshot placeholder,
|
|
217
|
+
* a bigint below the top level, a cycle (found through `stack`, the
|
|
218
|
+
* containers being rendered) and a container more than `MCP_FAKES_MAX_DEPTH`
|
|
219
|
+
* levels down each render as a `<reason>` placeholder. A placeholder is known
|
|
220
|
+
* by its class, never by its text, so no user value can pose as one.
|
|
221
|
+
* `undefined` where `JSON.stringify` gives `undefined`. Every part it joins
|
|
222
|
+
* is `JSON.stringify` output or placeholder text. Every read of the caller's
|
|
223
|
+
* value goes through `userCode`, so the only error it lets out from the
|
|
224
|
+
* caller's code is a `UserCodeThrew`.
|
|
225
|
+
*/
|
|
226
|
+
function renderJson(value, key, stack) {
|
|
227
|
+
if (userCode(() => value instanceof NotPlain)) return placeholder(value.reason);
|
|
228
|
+
if (value !== null && (typeof value === "object" || typeof value === "bigint")) {
|
|
229
|
+
const toJSON = userCode(() => value.toJSON);
|
|
230
|
+
if (typeof toJSON === "function") value = userCode(() => toJSON.call(value, key));
|
|
231
|
+
}
|
|
232
|
+
if (typeof value === "bigint") return placeholder(msg`a bigint (${fixed(`${value}n`)})`);
|
|
233
|
+
if (value === null || typeof value !== "object") return JSON.stringify(value);
|
|
234
|
+
const container = value;
|
|
235
|
+
if (userCode(() => container instanceof Number || container instanceof String || container instanceof Boolean)) return userCode(() => JSON.stringify(container));
|
|
236
|
+
if (stack.includes(container)) return placeholder(CIRCULAR_REASON);
|
|
237
|
+
if (stack.length >= MCP_FAKES_MAX_DEPTH) return placeholder(DEPTH_REASON);
|
|
238
|
+
stack.push(container);
|
|
239
|
+
try {
|
|
240
|
+
if (userCode(() => Array.isArray(container))) {
|
|
241
|
+
const array = container;
|
|
242
|
+
const length = userCode(() => array.length);
|
|
243
|
+
const items = [];
|
|
244
|
+
for (let i = 0; i < length; i++) items.push(renderMember(() => array[i], String(i), stack) ?? "null");
|
|
245
|
+
return `[${items.join(",")}]`;
|
|
246
|
+
}
|
|
247
|
+
const record = container;
|
|
248
|
+
const members = [];
|
|
249
|
+
for (const k of userCode(() => ownKeys(record))) {
|
|
250
|
+
const item = renderMember(() => record[k], k, stack);
|
|
251
|
+
if (item !== void 0) members.push(`${JSON.stringify(k)}:${item}`);
|
|
252
|
+
}
|
|
253
|
+
return `{${members.join(",")}}`;
|
|
254
|
+
} finally {
|
|
255
|
+
stack.pop();
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
/**
|
|
259
|
+
* A thrown value as a message part: its text (user text: a Proxy trap's or a
|
|
260
|
+
* `toJSON`'s message) JSON-quoted; never throws.
|
|
261
|
+
*/
|
|
262
|
+
function describeThrown(err) {
|
|
263
|
+
try {
|
|
264
|
+
return quote(err instanceof Error ? `${err.name}: ${err.message}` : String(err));
|
|
265
|
+
} catch {
|
|
266
|
+
return fixed("an unprintable value");
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
/**
|
|
270
|
+
* A built-in `Object.prototype.toString` tag (`[object Date]`). Any other tag
|
|
271
|
+
* can come from a user `Symbol.toStringTag`, so a message JSON-quotes it: it
|
|
272
|
+
* then cannot pose as JSON structure or a `<...>` placeholder, and holds no
|
|
273
|
+
* backslash that is not a JSON escape.
|
|
274
|
+
*/
|
|
275
|
+
const BUILTIN_TAG = /^\[object [A-Za-z][A-Za-z0-9]*\]$/;
|
|
276
|
+
/** A `toString` tag as a message part: a built-in one as is, any other quoted. */
|
|
277
|
+
function tagPart(tag) {
|
|
278
|
+
return BUILTIN_TAG.test(tag) ? fixed(tag) : quote(tag);
|
|
279
|
+
}
|
|
280
|
+
/**
|
|
281
|
+
* A place in the caller's input that the boundary snapshot could not copy as
|
|
282
|
+
* plain data. It replaces that value in the snapshot, so every later check
|
|
283
|
+
* sees a non-plain value and reports its own rule with `reason`.
|
|
284
|
+
*/
|
|
285
|
+
var NotPlain = class {
|
|
286
|
+
constructor(reason) {
|
|
287
|
+
this.reason = reason;
|
|
288
|
+
Object.freeze(this);
|
|
289
|
+
}
|
|
290
|
+
};
|
|
291
|
+
/**
|
|
292
|
+
* The input boundary: one plain-data copy of a caller's value, made once at
|
|
293
|
+
* `McpFakeStore.add` / `validateMcpFakes` entry; every check after it reads
|
|
294
|
+
* only the copy. It reads own, enumerable, string-keyed data properties only:
|
|
295
|
+
* getters are never run and inherited fields never seen. Never throws. Where
|
|
296
|
+
* the value is not plain data, the copy holds a `NotPlain`: a non-plain
|
|
297
|
+
* object or array (`Date`, `Map`, a class instance, an object with a custom
|
|
298
|
+
* prototype), a symbol key, an accessor property, a sparse array, an array
|
|
299
|
+
* with a non-index key, a cycle, a function, symbol or bigint, or a value
|
|
300
|
+
* whose reading throws (a hostile or revoked `Proxy`). Strings, numbers
|
|
301
|
+
* (non-finite ones too), booleans, `null` and `undefined` are kept as is.
|
|
302
|
+
* Copied objects have a `null` prototype.
|
|
303
|
+
*
|
|
304
|
+
* An object or array below `maxDepth` levels (the root is level 1) is not
|
|
305
|
+
* read: the copy holds a "nested deeper than `MCP_FAKES_MAX_DEPTH` levels"
|
|
306
|
+
* `NotPlain` there, so the copy never recurses deeper than that. Only the
|
|
307
|
+
* reflective reads of the caller's value (`readShape`) are guarded by a
|
|
308
|
+
* `catch`; a throw from aimock's own copy code is a bug and propagates.
|
|
309
|
+
*
|
|
310
|
+
* Each caller object is read once, and copied once per level it is reached
|
|
311
|
+
* at: an object reached through several paths (shared references) is not
|
|
312
|
+
* copied once per path, so the time is linear in the number of objects, not
|
|
313
|
+
* exponential in the depth. The copy keeps that sharing. An object on a
|
|
314
|
+
* cycle is still never copied without a placeholder somewhere under it.
|
|
315
|
+
*/
|
|
316
|
+
function snapshotInput(value, maxDepth = MCP_FAKES_MAX_DEPTH) {
|
|
317
|
+
return snapshotValue(value, {
|
|
318
|
+
maxDepth,
|
|
319
|
+
ancestors: [],
|
|
320
|
+
shapes: /* @__PURE__ */ new Map(),
|
|
321
|
+
copies: /* @__PURE__ */ new Map()
|
|
322
|
+
});
|
|
323
|
+
}
|
|
324
|
+
function snapshotValue(value, state) {
|
|
325
|
+
if (value === null || value === void 0 || typeof value === "string" || typeof value === "number" || typeof value === "boolean") return value;
|
|
326
|
+
if (typeof value !== "object") return new NotPlain(msg`not a JSON value (got ${fixed(typeof value)})`);
|
|
327
|
+
const { ancestors } = state;
|
|
328
|
+
if (ancestors.includes(value)) return new NotPlain(msg`a circular reference`);
|
|
329
|
+
const level = ancestors.length;
|
|
330
|
+
if (level >= state.maxDepth) return new NotPlain(DEPTH_REASON);
|
|
331
|
+
let byLevel = state.copies.get(value);
|
|
332
|
+
if (byLevel?.has(level)) return byLevel.get(level);
|
|
333
|
+
let shape = state.shapes.get(value);
|
|
334
|
+
if (shape === void 0) {
|
|
335
|
+
shape = readShape(value);
|
|
336
|
+
state.shapes.set(value, shape);
|
|
337
|
+
}
|
|
338
|
+
if (shape instanceof NotPlain) return shape;
|
|
339
|
+
ancestors.push(value);
|
|
340
|
+
let copy;
|
|
341
|
+
try {
|
|
342
|
+
copy = copyShape(shape, state);
|
|
343
|
+
} finally {
|
|
344
|
+
ancestors.pop();
|
|
345
|
+
}
|
|
346
|
+
if (!byLevel) {
|
|
347
|
+
byLevel = /* @__PURE__ */ new Map();
|
|
348
|
+
state.copies.set(value, byLevel);
|
|
349
|
+
}
|
|
350
|
+
byLevel.set(level, copy);
|
|
351
|
+
return copy;
|
|
352
|
+
}
|
|
353
|
+
/**
|
|
354
|
+
* Every reflective read of one caller object, and nothing else: the only
|
|
355
|
+
* code a hostile or revoked `Proxy` can make throw. A throw here is the
|
|
356
|
+
* caller's value being unreadable, so it becomes a `NotPlain`.
|
|
357
|
+
*/
|
|
358
|
+
function readShape(value) {
|
|
359
|
+
try {
|
|
360
|
+
const isArray = Array.isArray(value);
|
|
361
|
+
const proto = Object.getPrototypeOf(value);
|
|
362
|
+
if (isArray ? proto !== Array.prototype : proto !== Object.prototype && proto !== null) {
|
|
363
|
+
const tag = Object.prototype.toString.call(value);
|
|
364
|
+
return new NotPlain(msg`not a plain JSON ${fixed(isArray ? "array" : "object")} (got ${tagPart(tag)})`);
|
|
365
|
+
}
|
|
366
|
+
const data = [];
|
|
367
|
+
for (const key of Reflect.ownKeys(value)) {
|
|
368
|
+
const desc = Reflect.getOwnPropertyDescriptor(value, key);
|
|
369
|
+
if (!desc?.enumerable) continue;
|
|
370
|
+
if (typeof key === "symbol") return new NotPlain(msg`an object with a symbol key`);
|
|
371
|
+
data.push([key, hasOwnKey(desc, "value") ? desc.value : ACCESSOR]);
|
|
372
|
+
}
|
|
373
|
+
const lengthDesc = isArray ? Reflect.getOwnPropertyDescriptor(value, "length") : void 0;
|
|
374
|
+
return {
|
|
375
|
+
isArray,
|
|
376
|
+
data,
|
|
377
|
+
length: typeof lengthDesc?.value === "number" ? lengthDesc.value : 0
|
|
378
|
+
};
|
|
379
|
+
} catch (err) {
|
|
380
|
+
return new NotPlain(msg`an unreadable value (reading it threw ${describeThrown(err)})`);
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
/**
|
|
384
|
+
* Most holes a sparse array may have and still be copied element by element.
|
|
385
|
+
* Its `length` costs the caller nothing, so a longer one is not walked.
|
|
386
|
+
*/
|
|
387
|
+
const SPARSE_HOLE_LIMIT = 1e4;
|
|
388
|
+
/**
|
|
389
|
+
* Copy what `readShape` read; recurses through `snapshotValue`. A hole in a
|
|
390
|
+
* sparse array becomes a placeholder in its own place, so the array is still
|
|
391
|
+
* an array and every other element is still checked.
|
|
392
|
+
*/
|
|
393
|
+
function copyShape(shape, state) {
|
|
394
|
+
const { data, length } = shape;
|
|
395
|
+
if (shape.isArray) {
|
|
396
|
+
const own = new Map(data);
|
|
397
|
+
for (const [key] of data) {
|
|
398
|
+
const n = Number(key);
|
|
399
|
+
if (key !== String(n) || !Number.isInteger(n) || n < 0 || n >= length) return new NotPlain(msg`an array with a non-index key ${quote(key)}`);
|
|
400
|
+
}
|
|
401
|
+
const holes = length - own.size;
|
|
402
|
+
if (holes > SPARSE_HOLE_LIMIT) {
|
|
403
|
+
let hole = 0;
|
|
404
|
+
while (own.has(String(hole))) hole += 1;
|
|
405
|
+
return new NotPlain(msg`a sparse array with ${holes} holes (the first at [${hole}])`);
|
|
406
|
+
}
|
|
407
|
+
const out = [];
|
|
408
|
+
for (let i = 0; i < length; i++) {
|
|
409
|
+
const key = String(i);
|
|
410
|
+
out.push(own.has(key) ? snapshotField(own.get(key), state) : new NotPlain(msg`a hole in a sparse array`));
|
|
411
|
+
}
|
|
412
|
+
return out;
|
|
413
|
+
}
|
|
414
|
+
const out = Object.create(null);
|
|
415
|
+
for (const [key, item] of data) Object.defineProperty(out, key, {
|
|
416
|
+
value: snapshotField(item, state),
|
|
417
|
+
enumerable: true,
|
|
418
|
+
writable: true,
|
|
419
|
+
configurable: true
|
|
420
|
+
});
|
|
421
|
+
return out;
|
|
422
|
+
}
|
|
423
|
+
/** Marks an accessor property in `readShape` (its getter is never run). */
|
|
424
|
+
const ACCESSOR = Symbol("accessor");
|
|
425
|
+
function snapshotField(item, state) {
|
|
426
|
+
return item === ACCESSOR ? new NotPlain(msg`an accessor property (getters are not read)`) : snapshotValue(item, state);
|
|
427
|
+
}
|
|
428
|
+
/**
|
|
429
|
+
* A copy of `value`, a snapshot copy, with every container below `levels`
|
|
430
|
+
* levels (the value itself is level 1) replaced by the depth placeholder.
|
|
431
|
+
* Used by `McpFakeStore.add` for a block of an array-form `mcpFakes`, whose
|
|
432
|
+
* array is level 1. It does not change `value`: a snapshot can share one copy
|
|
433
|
+
* between paths at different levels. Each container is copied once per level
|
|
434
|
+
* (`memo`), so shared references stay linear.
|
|
435
|
+
*/
|
|
436
|
+
function capDepth(value, levels, memo = /* @__PURE__ */ new Map()) {
|
|
437
|
+
if (value instanceof NotPlain || typeof value !== "object" || value === null) return value;
|
|
438
|
+
if (levels < 1) return new NotPlain(DEPTH_REASON);
|
|
439
|
+
let byLevel = memo.get(value);
|
|
440
|
+
if (byLevel?.has(levels)) return byLevel.get(levels);
|
|
441
|
+
let out;
|
|
442
|
+
if (Array.isArray(value)) out = value.map((item) => capDepth(item, levels - 1, memo));
|
|
443
|
+
else {
|
|
444
|
+
const src = value;
|
|
445
|
+
const obj = Object.create(null);
|
|
446
|
+
for (const key of ownKeys(src)) Object.defineProperty(obj, key, {
|
|
447
|
+
value: capDepth(src[key], levels - 1, memo),
|
|
448
|
+
enumerable: true,
|
|
449
|
+
writable: true,
|
|
450
|
+
configurable: true
|
|
451
|
+
});
|
|
452
|
+
out = obj;
|
|
453
|
+
}
|
|
454
|
+
if (!byLevel) {
|
|
455
|
+
byLevel = /* @__PURE__ */ new Map();
|
|
456
|
+
memo.set(value, byLevel);
|
|
457
|
+
}
|
|
458
|
+
byLevel.set(levels, out);
|
|
459
|
+
return out;
|
|
460
|
+
}
|
|
461
|
+
/**
|
|
462
|
+
* Freeze `value` and everything under it (plain data only). A frozen object
|
|
463
|
+
* is not walked again: this module freezes children before their parent, and
|
|
464
|
+
* a shared child is then frozen once.
|
|
465
|
+
*/
|
|
466
|
+
function deepFreeze(value) {
|
|
467
|
+
if (typeof value === "object" && value !== null && !Object.isFrozen(value)) {
|
|
468
|
+
const obj = value;
|
|
469
|
+
for (const key of ownKeys(obj)) deepFreeze(obj[key]);
|
|
470
|
+
Object.freeze(value);
|
|
471
|
+
}
|
|
472
|
+
return value;
|
|
473
|
+
}
|
|
474
|
+
function isNonEmptyString(value) {
|
|
475
|
+
return typeof value === "string" && value.length > 0;
|
|
476
|
+
}
|
|
477
|
+
/** A field is present when it is an own key whose value is not `undefined`. */
|
|
478
|
+
function has(raw, key) {
|
|
479
|
+
return hasOwnKey(raw, key) && raw[key] !== void 0;
|
|
480
|
+
}
|
|
481
|
+
/** Own keys whose value is not `undefined` (an `undefined` field is absent). */
|
|
482
|
+
function presentKeys(raw) {
|
|
483
|
+
return ownKeys(raw).filter((key) => raw[key] !== void 0);
|
|
484
|
+
}
|
|
485
|
+
/**
|
|
486
|
+
* The first place under `value` that is not plain JSON (as `JSON.parse` could
|
|
487
|
+
* produce it), or `null`. An object field set to `undefined` is absent, as in
|
|
488
|
+
* `JSON.stringify`. Rejects an `undefined` array item, functions, symbols,
|
|
489
|
+
* bigints, non-finite numbers, sparse-array holes and objects whose prototype
|
|
490
|
+
* is not `Object.prototype` or `null` (`Date`, `Map`, class instances). Its
|
|
491
|
+
* input is always the input snapshot, which holds no cycle (`snapshotInput`
|
|
492
|
+
* puts a placeholder there) and no container deeper than
|
|
493
|
+
* `MCP_FAKES_MAX_DEPTH`, so the recursion is bounded. A container found clean
|
|
494
|
+
* is not checked again (`clean`), so a shared one is walked once.
|
|
495
|
+
*/
|
|
496
|
+
function jsonProblem(value, path, clean = /* @__PURE__ */ new WeakSet()) {
|
|
497
|
+
if (value instanceof NotPlain) return msg`${pathPart(path)} is ${value.reason}`;
|
|
498
|
+
if (value === null || typeof value === "string" || typeof value === "boolean") return null;
|
|
499
|
+
if (typeof value === "number") return Number.isFinite(value) ? null : msg`${pathPart(path)} must be a finite number, got ${shown(value)}`;
|
|
500
|
+
if (typeof value !== "object") return msg`${pathPart(path)} is not a JSON value (got ${fixed(typeof value)})`;
|
|
501
|
+
if (clean.has(value)) return null;
|
|
502
|
+
let problem = null;
|
|
503
|
+
if (Array.isArray(value)) for (let i = 0; i < value.length && !problem; i++) problem = hasOwnKey(value, String(i)) ? jsonProblem(value[i], `${path}[${i}]`, clean) : msg`${pathPart(`${path}[${i}]`)} is a hole in a sparse array`;
|
|
504
|
+
else {
|
|
505
|
+
const proto = Object.getPrototypeOf(value);
|
|
506
|
+
if (proto !== Object.prototype && proto !== null) {
|
|
507
|
+
const tag = Object.prototype.toString.call(value);
|
|
508
|
+
problem = msg`${pathPart(path)} must be a plain JSON object, got ${tagPart(tag)}`;
|
|
509
|
+
} else {
|
|
510
|
+
const obj = value;
|
|
511
|
+
for (const key of presentKeys(obj)) {
|
|
512
|
+
problem = jsonProblem(obj[key], pathKey(path, key), clean);
|
|
513
|
+
if (problem) break;
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
if (!problem) clean.add(value);
|
|
518
|
+
return problem;
|
|
519
|
+
}
|
|
520
|
+
/**
|
|
521
|
+
* Deep copy of a plain JSON value (checked by `jsonProblem`), optionally
|
|
522
|
+
* deep-frozen. Keys are defined, not assigned, so `"__proto__"` stays data;
|
|
523
|
+
* a field set to `undefined` is left out. A container reached through several
|
|
524
|
+
* paths is copied once (`memo`), and the copy shares it the same way.
|
|
525
|
+
*/
|
|
526
|
+
function copyJson(value, freeze = false, memo = /* @__PURE__ */ new Map()) {
|
|
527
|
+
if (typeof value !== "object" || value === null) return value;
|
|
528
|
+
if (memo.has(value)) return memo.get(value);
|
|
529
|
+
let out;
|
|
530
|
+
if (Array.isArray(value)) out = value.map((item) => copyJson(item, freeze, memo));
|
|
531
|
+
else {
|
|
532
|
+
const src = value;
|
|
533
|
+
const obj = {};
|
|
534
|
+
for (const key of presentKeys(src)) Object.defineProperty(obj, key, {
|
|
535
|
+
value: copyJson(src[key], freeze, memo),
|
|
536
|
+
enumerable: true,
|
|
537
|
+
writable: true,
|
|
538
|
+
configurable: true
|
|
539
|
+
});
|
|
540
|
+
out = obj;
|
|
541
|
+
}
|
|
542
|
+
if (freeze) Object.freeze(out);
|
|
543
|
+
memo.set(value, out);
|
|
544
|
+
return out;
|
|
545
|
+
}
|
|
546
|
+
/** Block id: `<source><load>[<block>]`, with no `[<block>]` for a single-object `mcpFakes`. */
|
|
547
|
+
function blockIdOf(source, loadSuffix, blockIndex) {
|
|
548
|
+
return `${source}${loadSuffix}${blockIndex === null ? "" : `[${blockIndex}]`}`;
|
|
549
|
+
}
|
|
550
|
+
function localOf(toolName, index, userId) {
|
|
551
|
+
return isNonEmptyString(userId) ? userId : `${toolName}#${index}`;
|
|
552
|
+
}
|
|
553
|
+
function tierOf(scope) {
|
|
554
|
+
if (scope === "shared") return "shared";
|
|
555
|
+
if (scope.testId !== void 0 && scope.context !== void 0) return "testId+context";
|
|
556
|
+
if (scope.testId !== void 0) return "testId";
|
|
557
|
+
return "context";
|
|
558
|
+
}
|
|
559
|
+
/** Scope match: exact, case-sensitive, no normalization; `"shared"` matches every request. */
|
|
560
|
+
function scopeMatches(scope, identity) {
|
|
561
|
+
if (scope === "shared") return true;
|
|
562
|
+
if (scope.testId !== void 0 && identity.testId !== scope.testId) return false;
|
|
563
|
+
if (scope.context !== void 0 && identity.context !== scope.context) return false;
|
|
564
|
+
return true;
|
|
565
|
+
}
|
|
566
|
+
function badBlock(ctx, letter, detail, entryId = null) {
|
|
567
|
+
return new FixtureLoadError({
|
|
568
|
+
rule: `mcp-fakes/bad-block:${letter}`,
|
|
569
|
+
file: ctx.source,
|
|
570
|
+
blockId: ctx.blockId,
|
|
571
|
+
entryId,
|
|
572
|
+
detail
|
|
573
|
+
});
|
|
574
|
+
}
|
|
575
|
+
/** Every scope error goes to `errors`; returns the scope only when it is clean. */
|
|
576
|
+
function validateScope(raw, ctx, errors) {
|
|
577
|
+
if (raw === void 0) {
|
|
578
|
+
errors.push(badBlock(ctx, "a", msg`block has no scope`));
|
|
579
|
+
return null;
|
|
580
|
+
}
|
|
581
|
+
if (raw === "shared") return "shared";
|
|
582
|
+
if (!isPlainObject(raw)) {
|
|
583
|
+
errors.push(badBlock(ctx, "b", msg`scope must be "shared" or an object, got ${shown(raw)}`));
|
|
584
|
+
return null;
|
|
585
|
+
}
|
|
586
|
+
const before = errors.length;
|
|
587
|
+
for (const key of presentKeys(raw)) if (!SCOPE_KEYS.has(key)) errors.push(badBlock(ctx, "b", msg`scope has unknown key ${shown(key)}`));
|
|
588
|
+
if (!has(raw, "testId") && !has(raw, "context")) errors.push(badBlock(ctx, "b", msg`scope object has neither testId nor context`));
|
|
589
|
+
for (const key of ["testId", "context"]) if (has(raw, key) && !isNonEmptyString(raw[key])) errors.push(badBlock(ctx, "b", msg`scope.${fixed(key)} must be a non-empty string, got ${shown(raw[key])}`));
|
|
590
|
+
if (errors.length > before) return null;
|
|
591
|
+
const testId = raw.testId;
|
|
592
|
+
const context = raw.context;
|
|
593
|
+
if (testId !== void 0 && context !== void 0) return Object.freeze({
|
|
594
|
+
testId,
|
|
595
|
+
context
|
|
596
|
+
});
|
|
597
|
+
if (testId !== void 0) return Object.freeze({ testId });
|
|
598
|
+
return Object.freeze({ context });
|
|
599
|
+
}
|
|
600
|
+
const mustBeString = (v, p) => typeof v === "string" ? null : msg`${pathPart(p)} must be a string, got ${shown(v)}`;
|
|
601
|
+
const mustBeNumber = (v, p) => typeof v === "number" ? null : msg`${pathPart(p)} must be a number, got ${shown(v)}`;
|
|
602
|
+
const mustBeBoolean = (v, p) => typeof v === "boolean" ? null : msg`${pathPart(p)} must be a boolean, got ${shown(v)}`;
|
|
603
|
+
const mustBeObject = (v, p) => isPlainObject(v) ? null : msg`${pathPart(p)} must be a JSON object, got ${shown(v)}`;
|
|
604
|
+
/** Base64 as the SDK's `Base64Schema` accepts it (`atob` does not throw). */
|
|
605
|
+
const mustBeBase64 = (v, p) => {
|
|
606
|
+
if (typeof v === "string") try {
|
|
607
|
+
atob(v);
|
|
608
|
+
return null;
|
|
609
|
+
} catch {}
|
|
610
|
+
return msg`${pathPart(p)} must be a base64 string, got ${shown(v)}`;
|
|
611
|
+
};
|
|
612
|
+
function oneOf(values) {
|
|
613
|
+
const list = values.map((v) => quoteJson(v)).join(", ");
|
|
614
|
+
return (v, p) => typeof v === "string" && values.includes(v) ? null : msg`${pathPart(p)} must be one of ${fixed(list)}, got ${shown(v)}`;
|
|
615
|
+
}
|
|
616
|
+
function arrayOf(item, noun = "an array") {
|
|
617
|
+
return (v, p) => {
|
|
618
|
+
if (!Array.isArray(v)) return msg`${pathPart(p)} must be ${fixed(noun)}, got ${shown(v)}`;
|
|
619
|
+
for (let i = 0; i < v.length; i++) {
|
|
620
|
+
const problem = item(v[i], `${p}[${i}]`);
|
|
621
|
+
if (problem) return problem;
|
|
622
|
+
}
|
|
623
|
+
return null;
|
|
624
|
+
};
|
|
625
|
+
}
|
|
626
|
+
const mustBeStringArray = arrayOf(mustBeString, "an array of strings");
|
|
627
|
+
/**
|
|
628
|
+
* An object with `required` fields and `optional` ones (an `undefined` field
|
|
629
|
+
* is absent). Other keys are not checked.
|
|
630
|
+
*/
|
|
631
|
+
function shape(required, optional = {}) {
|
|
632
|
+
return (v, p) => {
|
|
633
|
+
if (!isPlainObject(v)) return msg`${pathPart(p)} must be a JSON object, got ${shown(v)}`;
|
|
634
|
+
for (const key of ownKeys(required)) {
|
|
635
|
+
const check = required[key];
|
|
636
|
+
const problem = check(has(v, key) ? v[key] : void 0, pathKey(p, key));
|
|
637
|
+
if (problem) return problem;
|
|
638
|
+
}
|
|
639
|
+
for (const key of ownKeys(optional)) {
|
|
640
|
+
const check = optional[key];
|
|
641
|
+
if (!has(v, key)) continue;
|
|
642
|
+
const problem = check(v[key], pathKey(p, key));
|
|
643
|
+
if (problem) return problem;
|
|
644
|
+
}
|
|
645
|
+
return null;
|
|
646
|
+
};
|
|
647
|
+
}
|
|
648
|
+
/**
|
|
649
|
+
* RFC 3339 date-time with seconds and a `Z` or `±hh:mm` offset: the pattern of
|
|
650
|
+
* zod's `z.iso.datetime({ offset: true })`, which the SDK uses for
|
|
651
|
+
* `Annotations.lastModified`.
|
|
652
|
+
*/
|
|
653
|
+
const ISO_DATETIME = /^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|[+-](?:[01]\d|2[0-3]):[0-5]\d)$/;
|
|
654
|
+
const ANNOTATIONS = shape({}, {
|
|
655
|
+
audience: arrayOf(oneOf(["user", "assistant"])),
|
|
656
|
+
priority: (v, p) => typeof v === "number" && v >= 0 && v <= 1 ? null : msg`${pathPart(p)} must be a number from 0 to 1, got ${shown(v)}`,
|
|
657
|
+
lastModified: (v, p) => typeof v === "string" && ISO_DATETIME.test(v) ? null : msg`${pathPart(p)} must be an ISO 8601 date-time with seconds and an offset, got ${shown(v)}`
|
|
658
|
+
});
|
|
659
|
+
const ICON = shape({ src: mustBeString }, {
|
|
660
|
+
mimeType: mustBeString,
|
|
661
|
+
sizes: mustBeStringArray,
|
|
662
|
+
theme: oneOf(["light", "dark"])
|
|
663
|
+
});
|
|
664
|
+
/** MCP `TextResourceContents` / `BlobResourceContents`: exactly one of text / blob. */
|
|
665
|
+
const RESOURCE_CONTENTS = (v, p) => {
|
|
666
|
+
if (!isPlainObject(v)) return msg`${pathPart(p)} must be a JSON object with uri and text or blob, got ${shown(v)}`;
|
|
667
|
+
if (has(v, "text") === has(v, "blob")) return msg`${pathPart(p)} needs exactly one of text / blob`;
|
|
668
|
+
return shape({
|
|
669
|
+
uri: mustBeString,
|
|
670
|
+
...has(v, "text") ? { text: mustBeString } : { blob: mustBeBase64 }
|
|
671
|
+
}, {
|
|
672
|
+
mimeType: mustBeString,
|
|
673
|
+
_meta: mustBeObject
|
|
674
|
+
})(v, p);
|
|
675
|
+
};
|
|
676
|
+
/** Fields every content block may carry. */
|
|
677
|
+
const CONTENT_COMMON = {
|
|
678
|
+
annotations: ANNOTATIONS,
|
|
679
|
+
_meta: mustBeObject
|
|
680
|
+
};
|
|
681
|
+
/** Each MCP content block type, with its fields. */
|
|
682
|
+
const CONTENT_TYPES = new Map([
|
|
683
|
+
["text", shape({ text: mustBeString }, CONTENT_COMMON)],
|
|
684
|
+
["image", shape({
|
|
685
|
+
data: mustBeBase64,
|
|
686
|
+
mimeType: mustBeString
|
|
687
|
+
}, CONTENT_COMMON)],
|
|
688
|
+
["audio", shape({
|
|
689
|
+
data: mustBeBase64,
|
|
690
|
+
mimeType: mustBeString
|
|
691
|
+
}, CONTENT_COMMON)],
|
|
692
|
+
["resource_link", shape({
|
|
693
|
+
uri: mustBeString,
|
|
694
|
+
name: mustBeString
|
|
695
|
+
}, {
|
|
696
|
+
title: mustBeString,
|
|
697
|
+
description: mustBeString,
|
|
698
|
+
mimeType: mustBeString,
|
|
699
|
+
size: mustBeNumber,
|
|
700
|
+
icons: arrayOf(ICON),
|
|
701
|
+
...CONTENT_COMMON
|
|
702
|
+
})],
|
|
703
|
+
["resource", shape({ resource: RESOURCE_CONTENTS }, CONTENT_COMMON)]
|
|
704
|
+
]);
|
|
705
|
+
const CONTENT_TYPE_LIST = [...CONTENT_TYPES.keys()].map((type) => quoteJson(type)).join(", ");
|
|
706
|
+
/** One MCP content block, checked by its `type`. */
|
|
707
|
+
const CONTENT_BLOCK = (v, p) => {
|
|
708
|
+
if (!isPlainObject(v)) return msg`${pathPart(p)} must be a content object, got ${shown(v)}`;
|
|
709
|
+
const check = typeof v.type === "string" ? CONTENT_TYPES.get(v.type) : void 0;
|
|
710
|
+
if (!check) return msg`${pathPart(p)}.type must be one of ${fixed(CONTENT_TYPE_LIST)}, got ${shown(v.type)}`;
|
|
711
|
+
return check(v, p);
|
|
712
|
+
};
|
|
713
|
+
const CONTENT = arrayOf(CONTENT_BLOCK, "an array of content blocks");
|
|
714
|
+
/** The SDK client's `RequestMeta` keys on a result's `_meta`; other keys pass through. */
|
|
715
|
+
const RESULT_META = shape({}, {
|
|
716
|
+
progressToken: (v, p) => typeof v === "string" || Number.isSafeInteger(v) ? null : msg`${pathPart(p)} must be a string or an integer, got ${shown(v)}`,
|
|
717
|
+
"io.modelcontextprotocol/related-task": shape({ taskId: mustBeString })
|
|
718
|
+
});
|
|
719
|
+
/**
|
|
720
|
+
* The keys of a full `CallToolResult`. An object `result` with any of
|
|
721
|
+
* them is that form, and must have a `content` array; any other object is
|
|
722
|
+
* structured data.
|
|
723
|
+
*/
|
|
724
|
+
const FULL_RESULT_KEYS = new Set([
|
|
725
|
+
"content",
|
|
726
|
+
"isError",
|
|
727
|
+
"structuredContent",
|
|
728
|
+
"_meta"
|
|
729
|
+
]);
|
|
730
|
+
const FULL_RESULT = shape({ content: CONTENT }, {
|
|
731
|
+
isError: mustBeBoolean,
|
|
732
|
+
structuredContent: mustBeObject,
|
|
733
|
+
_meta: RESULT_META
|
|
734
|
+
});
|
|
735
|
+
function isFullResult(obj) {
|
|
736
|
+
return [...FULL_RESULT_KEYS].some((key) => has(obj, key));
|
|
737
|
+
}
|
|
738
|
+
/**
|
|
739
|
+
* A valid `result`: a string; a content array; a full `CallToolResult` (`content`
|
|
740
|
+
* array, optional boolean `isError`, optional object `structuredContent` and
|
|
741
|
+
* `_meta`, nothing else); or a JSON object with none of those keys (served as
|
|
742
|
+
* text plus `structuredContent`).
|
|
743
|
+
*/
|
|
744
|
+
function validateResult(result) {
|
|
745
|
+
if (typeof result === "string") return null;
|
|
746
|
+
if (!Array.isArray(result) && !isPlainObject(result)) return msg`result must be a string, a content array or an object, got ${shown(result)}`;
|
|
747
|
+
const json = jsonProblem(result, "result");
|
|
748
|
+
if (json) return json;
|
|
749
|
+
if (Array.isArray(result)) return CONTENT(result, "result");
|
|
750
|
+
if (!isFullResult(result)) return null;
|
|
751
|
+
for (const key of presentKeys(result)) if (!FULL_RESULT_KEYS.has(key)) return msg`result has unknown key ${shown(key)}`;
|
|
752
|
+
return FULL_RESULT(result, "result");
|
|
753
|
+
}
|
|
754
|
+
/**
|
|
755
|
+
* MCP `Tool.inputSchema`: a JSON Schema object with `type: "object"` at the
|
|
756
|
+
* root; `properties`, when present, maps names to schema objects, and
|
|
757
|
+
* `required`, when present, lists names. Any other JSON Schema keyword is
|
|
758
|
+
* allowed.
|
|
759
|
+
*/
|
|
760
|
+
const INPUT_SCHEMA = shape({ type: (v, p) => v === "object" ? null : msg`${pathPart(p)} must be "object", got ${shown(v)}` }, {
|
|
761
|
+
properties: (v, p) => {
|
|
762
|
+
if (!isPlainObject(v)) return mustBeObject(v, p);
|
|
763
|
+
for (const key of presentKeys(v)) {
|
|
764
|
+
const problem = mustBeObject(v[key], pathKey(p, key));
|
|
765
|
+
if (problem) return problem;
|
|
766
|
+
}
|
|
767
|
+
return null;
|
|
768
|
+
},
|
|
769
|
+
required: mustBeStringArray
|
|
770
|
+
});
|
|
771
|
+
function inputSchemaProblem(schema) {
|
|
772
|
+
if (!isPlainObject(schema)) return mustBeObject(schema, "inputSchema");
|
|
773
|
+
return jsonProblem(schema, "inputSchema") ?? INPUT_SCHEMA(schema, "inputSchema");
|
|
774
|
+
}
|
|
775
|
+
/**
|
|
776
|
+
* Validate one call entry of the input snapshot. Every error goes to
|
|
777
|
+
* `errors`; returns its entry id whether or not it is clean, so that the
|
|
778
|
+
* block's entry-id uniqueness check also sees invalid entries, and the frozen call
|
|
779
|
+
* only when it is clean. `toolName` is the tool's name, or a `tools[<i>]`
|
|
780
|
+
* placeholder when that name is invalid.
|
|
781
|
+
*/
|
|
782
|
+
function validateCall(raw, toolName, label, index, ctx, errors) {
|
|
783
|
+
const idRaw = isPlainObject(raw) ? raw.id : void 0;
|
|
784
|
+
const entryId = `${ctx.blockId}:${localOf(toolName, index, idRaw)}`;
|
|
785
|
+
const where = msg`${label} calls[${index}]`;
|
|
786
|
+
const bad = (letter, detail) => {
|
|
787
|
+
errors.push(badBlock(ctx, letter, msg`${where} ${detail}`, entryId));
|
|
788
|
+
};
|
|
789
|
+
if (!isPlainObject(raw)) {
|
|
790
|
+
bad("d", msg`must be an object, got ${shown(raw)}`);
|
|
791
|
+
return {
|
|
792
|
+
entryId,
|
|
793
|
+
call: null
|
|
794
|
+
};
|
|
795
|
+
}
|
|
796
|
+
const before = errors.length;
|
|
797
|
+
for (const key of presentKeys(raw)) if (!CALL_KEYS.has(key)) bad("h", msg`has unknown key ${shown(key)}`);
|
|
798
|
+
if (has(raw, "id") && !isNonEmptyString(raw.id)) bad("d", msg`id must be a non-empty string, got ${shown(raw.id)}`);
|
|
799
|
+
if (has(raw, "anyArgs") && raw.anyArgs !== true) bad("j", msg`anyArgs must be true, got ${shown(raw.anyArgs)}`);
|
|
800
|
+
const hasArgs = has(raw, "args");
|
|
801
|
+
if (hasArgs === has(raw, "anyArgs")) bad("d", msg`needs exactly one of args / anyArgs`);
|
|
802
|
+
if (hasArgs) {
|
|
803
|
+
const problem = isPlainObject(raw.args) ? jsonProblem(raw.args, "args") : msg`args must be a JSON object, got ${shown(raw.args)}`;
|
|
804
|
+
if (problem) bad("d", problem);
|
|
805
|
+
}
|
|
806
|
+
const hasResult = has(raw, "result");
|
|
807
|
+
const hasError = has(raw, "error");
|
|
808
|
+
if (hasResult === hasError) bad("d", msg`needs exactly one of result / error`);
|
|
809
|
+
if (hasError && typeof raw.error !== "string") bad("d", msg`error must be a string, got ${shown(raw.error)}`);
|
|
810
|
+
if (hasResult) {
|
|
811
|
+
const problem = validateResult(raw.result);
|
|
812
|
+
if (problem) bad("d", problem);
|
|
813
|
+
}
|
|
814
|
+
if (errors.length > before) return {
|
|
815
|
+
entryId,
|
|
816
|
+
call: null
|
|
817
|
+
};
|
|
818
|
+
const match = hasArgs ? { args: copyJson(raw.args, true) } : { anyArgs: true };
|
|
819
|
+
const answer = hasResult ? { result: copyJson(raw.result, true) } : { error: raw.error };
|
|
820
|
+
return {
|
|
821
|
+
entryId,
|
|
822
|
+
call: Object.freeze({
|
|
823
|
+
id: entryId,
|
|
824
|
+
index,
|
|
825
|
+
...match,
|
|
826
|
+
...answer
|
|
827
|
+
})
|
|
828
|
+
};
|
|
829
|
+
}
|
|
830
|
+
/**
|
|
831
|
+
* Validate one tool of the input snapshot. Every error goes to `errors`, and
|
|
832
|
+
* the entry ids of all its calls (valid or not; with an invalid tool name,
|
|
833
|
+
* only the user ids) to `entryIds`; returns the frozen tool only when it is
|
|
834
|
+
* clean.
|
|
835
|
+
*/
|
|
836
|
+
function validateTool(raw, index, ctx, errors, entryIds) {
|
|
837
|
+
const where = `tools[${index}]`;
|
|
838
|
+
const place = msg`tools[${index}]`;
|
|
839
|
+
if (!isPlainObject(raw)) {
|
|
840
|
+
errors.push(badBlock(ctx, "g", msg`${place} must be an object, got ${shown(raw)}`));
|
|
841
|
+
return null;
|
|
842
|
+
}
|
|
843
|
+
const before = errors.length;
|
|
844
|
+
for (const key of presentKeys(raw)) if (!TOOL_KEYS.has(key)) errors.push(badBlock(ctx, "h", msg`${place} has unknown key ${shown(key)}`));
|
|
845
|
+
const name = isNonEmptyString(raw.name) ? raw.name : null;
|
|
846
|
+
if (name === null) errors.push(badBlock(ctx, "g", msg`${place} needs a string name, got ${shown(raw.name)}`));
|
|
847
|
+
const tw = name === null ? place : msg`tool ${quote(name)}`;
|
|
848
|
+
if (has(raw, "description") && typeof raw.description !== "string") errors.push(badBlock(ctx, "g", msg`${tw} description must be a string, got ${shown(raw.description)}`));
|
|
849
|
+
if (has(raw, "inputSchema")) {
|
|
850
|
+
const problem = inputSchemaProblem(raw.inputSchema);
|
|
851
|
+
if (problem) errors.push(badBlock(ctx, "g", msg`${tw} ${problem}`));
|
|
852
|
+
}
|
|
853
|
+
const calls = [];
|
|
854
|
+
if (!Array.isArray(raw.calls) || raw.calls.length === 0) errors.push(badBlock(ctx, "g", msg`${tw} calls must be a non-empty array, got ${shown(raw.calls)}`));
|
|
855
|
+
else raw.calls.forEach((rawCall, i) => {
|
|
856
|
+
const { entryId, call } = validateCall(rawCall, name ?? where, tw, i, ctx, errors);
|
|
857
|
+
if (name !== null || isPlainObject(rawCall) && isNonEmptyString(rawCall.id)) entryIds.push(entryId);
|
|
858
|
+
if (call) calls.push(call);
|
|
859
|
+
});
|
|
860
|
+
if (errors.length > before || name === null) return null;
|
|
861
|
+
const tool = {
|
|
862
|
+
name,
|
|
863
|
+
calls: Object.freeze(calls)
|
|
864
|
+
};
|
|
865
|
+
if (typeof raw.description === "string") tool.description = raw.description;
|
|
866
|
+
if (isPlainObject(raw.inputSchema)) tool.inputSchema = copyJson(raw.inputSchema, true);
|
|
867
|
+
return Object.freeze(tool);
|
|
868
|
+
}
|
|
869
|
+
/**
|
|
870
|
+
* Validate one block of the input snapshot. Every check runs and each problem
|
|
871
|
+
* goes to `errors` (no first-stop), with two limits: a block, tool or call
|
|
872
|
+
* entry that is not an object is not checked further, and one `result` or
|
|
873
|
+
* `inputSchema` value reports only its first problem. Entry-id uniqueness
|
|
874
|
+
* inside the block is checked here over every declared entry, valid or not;
|
|
875
|
+
* uniqueness across blocks is the caller's.
|
|
876
|
+
*/
|
|
877
|
+
function validateBlock(raw, source, blockIndex, load, errors) {
|
|
878
|
+
const ctx = {
|
|
879
|
+
source,
|
|
880
|
+
load,
|
|
881
|
+
blockIndex,
|
|
882
|
+
blockId: blockIdOf(source, load, blockIndex)
|
|
883
|
+
};
|
|
884
|
+
if (!isPlainObject(raw)) {
|
|
885
|
+
errors.push(badBlock(ctx, "i", msg`mcpFakes block must be an object, got ${shown(raw)}`));
|
|
886
|
+
return {
|
|
887
|
+
block: null,
|
|
888
|
+
entryIds: [],
|
|
889
|
+
warnings: []
|
|
890
|
+
};
|
|
891
|
+
}
|
|
892
|
+
const before = errors.length;
|
|
893
|
+
for (const key of presentKeys(raw)) if (!BLOCK_KEYS.has(key)) errors.push(badBlock(ctx, "h", msg`block has unknown key ${shown(key)}`));
|
|
894
|
+
const scope = validateScope(raw.scope, ctx, errors);
|
|
895
|
+
let undeclared;
|
|
896
|
+
if (has(raw, "undeclaredTools")) if (raw.undeclaredTools === "allow" || raw.undeclaredTools === "deny") undeclared = raw.undeclaredTools;
|
|
897
|
+
else errors.push(badBlock(ctx, "g", msg`undeclaredTools must be "allow" or "deny", got ${shown(raw.undeclaredTools)}`));
|
|
898
|
+
if (scope === "shared" && undeclared === "deny") errors.push(badBlock(ctx, "c", msg`scope "shared" cannot have undeclaredTools "deny"`));
|
|
899
|
+
let mount = MCP_FAKES_DEFAULT_MOUNT;
|
|
900
|
+
if (has(raw, "mount")) if (typeof raw.mount === "string" && raw.mount.startsWith("/")) mount = raw.mount;
|
|
901
|
+
else errors.push(badBlock(ctx, "g", msg`mount must be a string starting with "/", got ${shown(raw.mount)}`));
|
|
902
|
+
const tools = [];
|
|
903
|
+
const declaredIds = [];
|
|
904
|
+
if (!Array.isArray(raw.tools)) errors.push(badBlock(ctx, "g", msg`tools must be an array, got ${shown(raw.tools)}`));
|
|
905
|
+
else if (raw.tools.length === 0) {
|
|
906
|
+
if (undeclared !== "deny" || scope === "shared") errors.push(badBlock(ctx, "g", msg`tools is empty; an empty tools array (tools: []) is allowed only on a scoped (not "shared") block with undeclaredTools "deny"`));
|
|
907
|
+
} else {
|
|
908
|
+
const names = /* @__PURE__ */ new Set();
|
|
909
|
+
raw.tools.forEach((rawTool, i) => {
|
|
910
|
+
const name = isPlainObject(rawTool) && isNonEmptyString(rawTool.name) ? rawTool.name : null;
|
|
911
|
+
const duplicate = name !== null && names.has(name);
|
|
912
|
+
if (duplicate) errors.push(badBlock(ctx, "e", msg`tool ${quote(name)} is declared twice in one block`));
|
|
913
|
+
else if (name !== null) names.add(name);
|
|
914
|
+
const tool = validateTool(rawTool, i, ctx, errors, duplicate ? [] : declaredIds);
|
|
915
|
+
if (tool && !duplicate) tools.push(tool);
|
|
916
|
+
});
|
|
917
|
+
}
|
|
918
|
+
const seen = /* @__PURE__ */ new Set();
|
|
919
|
+
for (const id of declaredIds) {
|
|
920
|
+
if (seen.has(id)) errors.push(badBlock(ctx, "f", msg`entry id ${quote(id)} is not unique`, id));
|
|
921
|
+
seen.add(id);
|
|
922
|
+
}
|
|
923
|
+
const entryIds = [...seen];
|
|
924
|
+
if (errors.length > before || scope === null) return {
|
|
925
|
+
block: null,
|
|
926
|
+
entryIds,
|
|
927
|
+
warnings: []
|
|
928
|
+
};
|
|
929
|
+
const warnings = [];
|
|
930
|
+
for (const tool of tools) {
|
|
931
|
+
let lastAny = null;
|
|
932
|
+
for (const call of tool.calls) if (call.anyArgs) lastAny = call;
|
|
933
|
+
else if (lastAny) warnings.push({
|
|
934
|
+
file: source,
|
|
935
|
+
blockId: ctx.blockId,
|
|
936
|
+
entryId: call.id,
|
|
937
|
+
message: build(msg`entry ${quote(call.id)} is shadowed until the preceding anyArgs entry ${quote(lastAny.id)} is consumed`)
|
|
938
|
+
});
|
|
939
|
+
}
|
|
940
|
+
return {
|
|
941
|
+
block: Object.freeze({
|
|
942
|
+
source,
|
|
943
|
+
load,
|
|
944
|
+
blockIndex,
|
|
945
|
+
blockId: ctx.blockId,
|
|
946
|
+
scope,
|
|
947
|
+
tier: tierOf(scope),
|
|
948
|
+
mount,
|
|
949
|
+
...undeclared !== void 0 ? { undeclaredTools: undeclared } : {},
|
|
950
|
+
tools: Object.freeze(tools)
|
|
951
|
+
}),
|
|
952
|
+
entryIds,
|
|
953
|
+
warnings
|
|
954
|
+
};
|
|
955
|
+
}
|
|
956
|
+
/**
|
|
957
|
+
* Standalone validation of a raw `mcpFakes` value (one object, or an array of
|
|
958
|
+
* blocks) from one `source`. It takes the plain-data snapshot of `raw`
|
|
959
|
+
* (getters are not run, inherited fields not read) and reports every
|
|
960
|
+
* bad-block case (a)-(j) found as a `FixtureLoadError` naming the source and
|
|
961
|
+
* block; entry-id collisions inside a block are (f). Never throws. Ids use the
|
|
962
|
+
* first-load form (empty `load`). The store does not use this function:
|
|
963
|
+
* `McpFakeStore.add` runs the same per-block checks (`validateBlock`) with its
|
|
964
|
+
* own load suffix, and adds the entry-id check across blocks (against the
|
|
965
|
+
* mount and the earlier blocks of the same input). `blocks` holds only the
|
|
966
|
+
* clean blocks (deeply frozen), `warnings` only their shadowed-entry warnings.
|
|
967
|
+
*/
|
|
968
|
+
function validateMcpFakes(raw, source) {
|
|
969
|
+
const errors = [];
|
|
970
|
+
const warnings = [];
|
|
971
|
+
const blocks = [];
|
|
972
|
+
const input = snapshotInput(raw);
|
|
973
|
+
let items;
|
|
974
|
+
if (Array.isArray(input) && input.length === 0) {
|
|
975
|
+
errors.push(badBlock({
|
|
976
|
+
source,
|
|
977
|
+
blockId: null
|
|
978
|
+
}, "i", msg`mcpFakes is an empty array`));
|
|
979
|
+
return {
|
|
980
|
+
blocks,
|
|
981
|
+
errors,
|
|
982
|
+
warnings
|
|
983
|
+
};
|
|
984
|
+
} else if (Array.isArray(input)) items = input.map((item, i) => ({
|
|
985
|
+
raw: item,
|
|
986
|
+
blockIndex: i
|
|
987
|
+
}));
|
|
988
|
+
else if (isPlainObject(input)) items = [{
|
|
989
|
+
raw: input,
|
|
990
|
+
blockIndex: null
|
|
991
|
+
}];
|
|
992
|
+
else {
|
|
993
|
+
errors.push(badBlock({
|
|
994
|
+
source,
|
|
995
|
+
blockId: null
|
|
996
|
+
}, "i", msg`mcpFakes must be an object or an array, got ${shown(input)}`));
|
|
997
|
+
return {
|
|
998
|
+
blocks,
|
|
999
|
+
errors,
|
|
1000
|
+
warnings
|
|
1001
|
+
};
|
|
1002
|
+
}
|
|
1003
|
+
for (const item of items) {
|
|
1004
|
+
const outcome = validateBlock(item.raw, source, item.blockIndex, "", errors);
|
|
1005
|
+
if (outcome.block) {
|
|
1006
|
+
blocks.push(outcome.block);
|
|
1007
|
+
warnings.push(...outcome.warnings);
|
|
1008
|
+
}
|
|
1009
|
+
}
|
|
1010
|
+
return {
|
|
1011
|
+
blocks,
|
|
1012
|
+
errors,
|
|
1013
|
+
warnings
|
|
1014
|
+
};
|
|
1015
|
+
}
|
|
1016
|
+
function blockEntryIds(block) {
|
|
1017
|
+
return block.tools.flatMap((t) => t.calls.map((c) => c.id));
|
|
1018
|
+
}
|
|
1019
|
+
/**
|
|
1020
|
+
* Turn a call entry's `result` or `error` into a `tools/call` result. The
|
|
1021
|
+
* returned object is a fresh copy the caller may change. A full
|
|
1022
|
+
* `CallToolResult` (validation guarantees any object with one of its keys
|
|
1023
|
+
* has a `content` array) is copied unchanged, with no `isError` added;
|
|
1024
|
+
* every other shape sets `isError`. Throws when the entry has
|
|
1025
|
+
* neither (validation makes that a bad block (d)).
|
|
1026
|
+
*/
|
|
1027
|
+
function toCallToolResult(call) {
|
|
1028
|
+
if (call.error !== void 0) return {
|
|
1029
|
+
content: [{
|
|
1030
|
+
type: "text",
|
|
1031
|
+
text: call.error
|
|
1032
|
+
}],
|
|
1033
|
+
isError: true
|
|
1034
|
+
};
|
|
1035
|
+
const result = call.result;
|
|
1036
|
+
if (result === void 0 || result === null) throw new Error(build(msg`toCallToolResult: call entry has neither result nor error`));
|
|
1037
|
+
if (typeof result === "string") return {
|
|
1038
|
+
content: [{
|
|
1039
|
+
type: "text",
|
|
1040
|
+
text: result
|
|
1041
|
+
}],
|
|
1042
|
+
isError: false
|
|
1043
|
+
};
|
|
1044
|
+
if (Array.isArray(result)) return {
|
|
1045
|
+
content: copyJson(result),
|
|
1046
|
+
isError: false
|
|
1047
|
+
};
|
|
1048
|
+
const obj = copyJson(result);
|
|
1049
|
+
if (Array.isArray(obj.content)) return {
|
|
1050
|
+
...obj,
|
|
1051
|
+
content: obj.content
|
|
1052
|
+
};
|
|
1053
|
+
return {
|
|
1054
|
+
content: [{
|
|
1055
|
+
type: "text",
|
|
1056
|
+
text: JSON.stringify(obj)
|
|
1057
|
+
}],
|
|
1058
|
+
structuredContent: obj,
|
|
1059
|
+
isError: false
|
|
1060
|
+
};
|
|
1061
|
+
}
|
|
1062
|
+
function jsonEqual(a, b) {
|
|
1063
|
+
if (a === b) return true;
|
|
1064
|
+
if (Array.isArray(a) || Array.isArray(b)) {
|
|
1065
|
+
if (!Array.isArray(a) || !Array.isArray(b) || a.length !== b.length) return false;
|
|
1066
|
+
return a.every((v, i) => jsonEqual(v, b[i]));
|
|
1067
|
+
}
|
|
1068
|
+
if (isPlainObject(a) && isPlainObject(b)) {
|
|
1069
|
+
const ak = ownKeys(a);
|
|
1070
|
+
const bk = ownKeys(b);
|
|
1071
|
+
if (ak.length !== bk.length) return false;
|
|
1072
|
+
return ak.every((k) => hasOwnKey(b, k) && jsonEqual(a[k], b[k]));
|
|
1073
|
+
}
|
|
1074
|
+
return false;
|
|
1075
|
+
}
|
|
1076
|
+
/**
|
|
1077
|
+
* Argument equality: key order ignored, array order matters, no coercion,
|
|
1078
|
+
* numbers equal as parsed JSON, absent `arguments` is `{}`, non-object
|
|
1079
|
+
* `arguments` never equals an `args` entry (only an `anyArgs` entry matches
|
|
1080
|
+
* it).
|
|
1081
|
+
*/
|
|
1082
|
+
function mcpArgsEqual(expected, received) {
|
|
1083
|
+
const actual = received === void 0 ? {} : received;
|
|
1084
|
+
if (!isPlainObject(actual)) return false;
|
|
1085
|
+
return jsonEqual(expected, actual);
|
|
1086
|
+
}
|
|
1087
|
+
/**
|
|
1088
|
+
* `path` and one more key: `.key` for a short identifier, else `[<key>]` with
|
|
1089
|
+
* the key JSON-quoted whole. The path is cut once, where a message shows it
|
|
1090
|
+
* (`pathPart`).
|
|
1091
|
+
*/
|
|
1092
|
+
function pathKey(path, key) {
|
|
1093
|
+
return key.length <= MCP_FAKES_ECHO_LIMIT && /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(key) ? `${path}.${key}` : `${path}[${quoteJson(key)}]`;
|
|
1094
|
+
}
|
|
1095
|
+
/**
|
|
1096
|
+
* A JSON path (built by `pathKey`) as a message part: user data, cut as JSON
|
|
1097
|
+
* text, so the cut never splits an escape of a quoted key.
|
|
1098
|
+
*/
|
|
1099
|
+
function pathPart(path) {
|
|
1100
|
+
return jsonText(path);
|
|
1101
|
+
}
|
|
1102
|
+
function diffAt(path, expected, received) {
|
|
1103
|
+
if (Array.isArray(expected)) {
|
|
1104
|
+
if (!Array.isArray(received)) return msg`${pathPart(path)}: expected an array, received ${shown(received)}`;
|
|
1105
|
+
if (expected.length !== received.length) return msg`${pathPart(path)}: expected an array of length ${expected.length}, received length ${received.length}`;
|
|
1106
|
+
for (let i = 0; i < expected.length; i++) {
|
|
1107
|
+
const d = diffAt(`${path}[${i}]`, expected[i], received[i]);
|
|
1108
|
+
if (d) return d;
|
|
1109
|
+
}
|
|
1110
|
+
return null;
|
|
1111
|
+
}
|
|
1112
|
+
if (isPlainObject(expected)) {
|
|
1113
|
+
if (!isPlainObject(received)) return msg`${pathPart(path)}: expected an object, received ${shown(received)}`;
|
|
1114
|
+
for (const key of ownKeys(expected)) {
|
|
1115
|
+
const p = pathKey(path, key);
|
|
1116
|
+
if (!hasOwnKey(received, key)) return msg`${pathPart(p)}: expected ${shown(expected[key])}, received nothing (key missing)`;
|
|
1117
|
+
const d = diffAt(p, expected[key], received[key]);
|
|
1118
|
+
if (d) return d;
|
|
1119
|
+
}
|
|
1120
|
+
for (const key of ownKeys(received)) if (!hasOwnKey(expected, key)) return msg`${pathPart(pathKey(path, key))}: unexpected key, received ${shown(received[key])}`;
|
|
1121
|
+
return null;
|
|
1122
|
+
}
|
|
1123
|
+
if (expected === received) return null;
|
|
1124
|
+
return msg`${pathPart(path)}: expected ${shown(expected)}, received ${shown(received)}`;
|
|
1125
|
+
}
|
|
1126
|
+
/**
|
|
1127
|
+
* A JSON path to the first unequal value, with both values (the mismatch
|
|
1128
|
+
* detail). Returns `""` when `mcpArgsEqual(expected, received)` holds. The
|
|
1129
|
+
* path and each value are cut at `MCP_FAKES_ECHO_LIMIT` chars and the whole
|
|
1130
|
+
* text at 1000, each part once.
|
|
1131
|
+
*/
|
|
1132
|
+
function firstDifference(expected, received) {
|
|
1133
|
+
const diff = diffAt("$", expected, received === void 0 ? {} : received);
|
|
1134
|
+
return diff ? build(diff, DIFF_LIMIT) : "";
|
|
1135
|
+
}
|
|
1136
|
+
/**
|
|
1137
|
+
* Approximate count of differences, used to pick the closest declared entry.
|
|
1138
|
+
* A key or array element present on one side only counts as 1, whatever its
|
|
1139
|
+
* subtree size.
|
|
1140
|
+
*/
|
|
1141
|
+
function diffCount(expected, received) {
|
|
1142
|
+
if (Array.isArray(expected) && Array.isArray(received)) {
|
|
1143
|
+
const n = Math.max(expected.length, received.length);
|
|
1144
|
+
let count = 0;
|
|
1145
|
+
for (let i = 0; i < n; i++) if (i >= expected.length || i >= received.length) count += 1;
|
|
1146
|
+
else count += diffCount(expected[i], received[i]);
|
|
1147
|
+
return count;
|
|
1148
|
+
}
|
|
1149
|
+
if (isPlainObject(expected) && isPlainObject(received)) {
|
|
1150
|
+
let count = 0;
|
|
1151
|
+
for (const key of new Set([...ownKeys(expected), ...ownKeys(received)])) {
|
|
1152
|
+
const inE = hasOwnKey(expected, key);
|
|
1153
|
+
const inR = hasOwnKey(received, key);
|
|
1154
|
+
count += inE && inR ? diffCount(expected[key], received[key]) : 1;
|
|
1155
|
+
}
|
|
1156
|
+
return count;
|
|
1157
|
+
}
|
|
1158
|
+
return jsonEqual(expected, received) ? 0 : 1;
|
|
1159
|
+
}
|
|
1160
|
+
function callMatches(call, received) {
|
|
1161
|
+
return call.anyArgs === true || call.args !== void 0 && mcpArgsEqual(call.args, received);
|
|
1162
|
+
}
|
|
1163
|
+
var McpFakeStore = class {
|
|
1164
|
+
blocks = [];
|
|
1165
|
+
/** Entry id → the block id that owns it, for every loaded entry. */
|
|
1166
|
+
ids = /* @__PURE__ */ new Map();
|
|
1167
|
+
/** Block id → its `source`, for every loaded block. */
|
|
1168
|
+
blockIds = /* @__PURE__ */ new Map();
|
|
1169
|
+
/**
|
|
1170
|
+
* Consumption state: supplied test id → consumed entry ids. Map insertion
|
|
1171
|
+
* order is the FIFO order of the cap.
|
|
1172
|
+
*/
|
|
1173
|
+
consumed = /* @__PURE__ */ new Map();
|
|
1174
|
+
/** Consumed entry ids for requests with no test id. Never evicted. */
|
|
1175
|
+
untagged = /* @__PURE__ */ new Set();
|
|
1176
|
+
/**
|
|
1177
|
+
* Test ids whose state the cap evicted. A claim from one of them, of a tool
|
|
1178
|
+
* an applicable block declares, is `evicted` until the test id is reset; a
|
|
1179
|
+
* mark never ages out, because a forgotten mark would silently replay
|
|
1180
|
+
* entries the test id already consumed. Memory stays bounded per evicted
|
|
1181
|
+
* test id: one id string, against the entry-id set the cap dropped. The set only grows with distinct test ids, and every
|
|
1182
|
+
* reset door clears it (`resetState`, `clear`). A cap of `0` evicts
|
|
1183
|
+
* nothing, so marks made under an earlier cap stay until reset.
|
|
1184
|
+
*/
|
|
1185
|
+
evicted = /* @__PURE__ */ new Set();
|
|
1186
|
+
/**
|
|
1187
|
+
* The last `<n>` of `code#<n>` / `control-api#<n>` used by a run-time
|
|
1188
|
+
* addition (see `nextRuntimeNumber`). Never rewound.
|
|
1189
|
+
*/
|
|
1190
|
+
runtimeAdds = 0;
|
|
1191
|
+
/**
|
|
1192
|
+
* The `<k>` of `@<k>`: loads per source string, skipping a `@<k>` that a
|
|
1193
|
+
* loaded block id already starts (see `add`). Never rewound.
|
|
1194
|
+
*/
|
|
1195
|
+
loadCounts = /* @__PURE__ */ new Map();
|
|
1196
|
+
maxTestIds;
|
|
1197
|
+
constructor(options = {}) {
|
|
1198
|
+
this.maxTestIds = checkCap(options.maxTestIds ?? MCP_FAKES_DEFAULT_MAX_TEST_IDS);
|
|
1199
|
+
}
|
|
1200
|
+
/**
|
|
1201
|
+
* Set the FIFO cap on distinct test ids; an MCP mount can pass the
|
|
1202
|
+
* journal's `fixtureCountsMaxTestIdsCap`. A lower cap evicts the oldest test
|
|
1203
|
+
* ids at once. `0` is unbounded: nothing is evicted, and test ids already
|
|
1204
|
+
* marked evicted stay marked until reset. An invalid cap throws a
|
|
1205
|
+
* `RangeError` and leaves the cap unchanged.
|
|
1206
|
+
*/
|
|
1207
|
+
setMaxTestIds(cap) {
|
|
1208
|
+
this.maxTestIds = checkCap(cap);
|
|
1209
|
+
this.enforceCap();
|
|
1210
|
+
}
|
|
1211
|
+
/**
|
|
1212
|
+
* Take one plain-data snapshot of `sources` and `origin` (getters are not
|
|
1213
|
+
* run, inherited fields not read, holes, symbol keys, proxies that throw and
|
|
1214
|
+
* non-plain objects are errors), validate the whole snapshot, derive entry
|
|
1215
|
+
* ids and their counters, check that every block id and every entry id is
|
|
1216
|
+
* unique against the mount and inside the input (bad block (f), naming the
|
|
1217
|
+
* block that already owns the id and, for a block id, that block's source),
|
|
1218
|
+
* then append. Every block is checked,
|
|
1219
|
+
* valid or not (no first-stop; `validateBlock` says which checks stop
|
|
1220
|
+
* early). On any error, throws one `McpFakesAddError` carrying every error
|
|
1221
|
+
* found, and the warnings of the blocks that passed every check, and adds
|
|
1222
|
+
* nothing (no counter is consumed); it never throws anything else for a bad
|
|
1223
|
+
* input (a throw from a defect in aimock itself is not caught). An empty
|
|
1224
|
+
* input, an invalid `origin` or an invalid source item (not an object, an
|
|
1225
|
+
* empty `source`, a `blockIndex` that is not `null` or a non-negative
|
|
1226
|
+
* integer) is a bad block (i).
|
|
1227
|
+
*
|
|
1228
|
+
* With `kind: "file"`, consecutive sources with the same `source` string
|
|
1229
|
+
* and increasing `blockIndex` form one load (one `@<k>`). A load whose
|
|
1230
|
+
* derived block ids a loaded block already has (a file named `"a@2"`
|
|
1231
|
+
* starts the ids of `@2`; a file named `"a[0]"` has the id of block 0 of
|
|
1232
|
+
* the first load) takes the next free `@<k>`, so a file name never blocks
|
|
1233
|
+
* later loads of another source. A file loaded after the block whose id
|
|
1234
|
+
* its name spells is still the bad block (f). With a run-time origin,
|
|
1235
|
+
* blocks are numbered per `McpFakeAddOrigin`; a block given with a null
|
|
1236
|
+
* `blockIndex` keeps the single-object depth limit.
|
|
1237
|
+
*/
|
|
1238
|
+
add(sources, origin) {
|
|
1239
|
+
const originData = snapshotInput(origin);
|
|
1240
|
+
const kind = isPlainObject(originData) ? originData.kind : void 0;
|
|
1241
|
+
if (kind !== "file" && kind !== "code" && kind !== "control-api") throw new McpFakesAddError([badBlock({
|
|
1242
|
+
source: null,
|
|
1243
|
+
blockId: null
|
|
1244
|
+
}, "i", isPlainObject(originData) ? msg`origin.kind must be "file", "code" or "control-api", got ${shown(kind)}` : msg`origin must be an object { kind }, got ${shown(originData)}`)], []);
|
|
1245
|
+
const runtimeNumber = kind === "file" ? null : this.nextRuntimeNumber(kind);
|
|
1246
|
+
const runtimeSource = runtimeNumber === null ? null : `${kind}#${runtimeNumber}`;
|
|
1247
|
+
const input = snapshotInput(sources, MCP_FAKES_MAX_DEPTH + 2);
|
|
1248
|
+
if (!Array.isArray(input) || input.length === 0) {
|
|
1249
|
+
const detail = Array.isArray(input) ? msg`mcpFakes holds no blocks` : msg`the blocks to add must be an array, got ${shown(input)}`;
|
|
1250
|
+
throw new McpFakesAddError([badBlock({
|
|
1251
|
+
source: runtimeSource,
|
|
1252
|
+
blockId: null
|
|
1253
|
+
}, "i", detail)], []);
|
|
1254
|
+
}
|
|
1255
|
+
const errors = [];
|
|
1256
|
+
const warnings = [];
|
|
1257
|
+
const accepted = [];
|
|
1258
|
+
/** Entry id → owning block id, for this input. */
|
|
1259
|
+
const newIds = /* @__PURE__ */ new Map();
|
|
1260
|
+
/** Block id → source, for this input. */
|
|
1261
|
+
const newBlockIds = /* @__PURE__ */ new Map();
|
|
1262
|
+
const pendingLoads = /* @__PURE__ */ new Map();
|
|
1263
|
+
const items = [];
|
|
1264
|
+
input.forEach((item, position) => {
|
|
1265
|
+
const fail = (detail, file = runtimeSource) => {
|
|
1266
|
+
errors.push(badBlock({
|
|
1267
|
+
source: file,
|
|
1268
|
+
blockId: null
|
|
1269
|
+
}, "i", msg`blocks[${position}] ${detail}`));
|
|
1270
|
+
};
|
|
1271
|
+
if (!isPlainObject(item)) {
|
|
1272
|
+
fail(msg`must be an object { source, blockIndex, raw }, got ${shown(item)}`);
|
|
1273
|
+
return;
|
|
1274
|
+
}
|
|
1275
|
+
const { source, blockIndex } = item;
|
|
1276
|
+
if (runtimeSource === null && !isNonEmptyString(source)) {
|
|
1277
|
+
fail(msg`source must be a non-empty string, got ${shown(source)}`);
|
|
1278
|
+
return;
|
|
1279
|
+
}
|
|
1280
|
+
const sourceText = typeof source === "string" ? source : "";
|
|
1281
|
+
const index = blockIndex === null || typeof blockIndex === "number" && Number.isSafeInteger(blockIndex) && blockIndex >= 0 ? blockIndex : void 0;
|
|
1282
|
+
if (index === void 0) {
|
|
1283
|
+
fail(msg`blockIndex must be null or a non-negative integer, got ${shown(blockIndex)}`, runtimeSource ?? sourceText);
|
|
1284
|
+
return;
|
|
1285
|
+
}
|
|
1286
|
+
items.push({
|
|
1287
|
+
position,
|
|
1288
|
+
source: sourceText,
|
|
1289
|
+
blockIndex: index,
|
|
1290
|
+
raw: item.raw
|
|
1291
|
+
});
|
|
1292
|
+
});
|
|
1293
|
+
const singleObject = input.length === 1 && items.length === 1 && items[0].blockIndex === null;
|
|
1294
|
+
/** Two consecutive file items with one source and a rising block index are one load. */
|
|
1295
|
+
const sameLoad = (prev, item) => prev !== void 0 && prev.source === item.source && prev.blockIndex !== null && item.blockIndex !== null && item.blockIndex > prev.blockIndex;
|
|
1296
|
+
let load = "";
|
|
1297
|
+
for (const [at, item] of items.entries()) {
|
|
1298
|
+
const source = runtimeSource ?? item.source;
|
|
1299
|
+
let blockIndex = item.blockIndex;
|
|
1300
|
+
if (runtimeSource === null) {
|
|
1301
|
+
if (!sameLoad(items[at - 1], item)) {
|
|
1302
|
+
const indices = [];
|
|
1303
|
+
for (let j = at; j < items.length && (j === at || sameLoad(items[j - 1], items[j])); j++) {
|
|
1304
|
+
const index = items[j].blockIndex;
|
|
1305
|
+
if (index !== null) indices.push(index);
|
|
1306
|
+
}
|
|
1307
|
+
let k = (pendingLoads.get(source) ?? this.loadCounts.get(source) ?? 0) + 1;
|
|
1308
|
+
while (k > 1 ? this.blockIdPrefixTaken(blockIdOf(source, `@${k}`, null), newBlockIds) : indices.some((index) => {
|
|
1309
|
+
const derived = blockIdOf(source, "", index);
|
|
1310
|
+
return (this.blockIds.get(derived) ?? newBlockIds.get(derived)) === derived;
|
|
1311
|
+
})) k += 1;
|
|
1312
|
+
pendingLoads.set(source, k);
|
|
1313
|
+
load = k === 1 ? "" : `@${k}`;
|
|
1314
|
+
}
|
|
1315
|
+
} else blockIndex = singleObject ? null : item.position;
|
|
1316
|
+
const outcome = validateBlock(item.blockIndex === null ? item.raw : capDepth(item.raw, MCP_FAKES_MAX_DEPTH - 1), source, blockIndex, load, errors);
|
|
1317
|
+
const blockId = blockIdOf(source, load, blockIndex);
|
|
1318
|
+
const blockOwner = this.blockIds.get(blockId) ?? newBlockIds.get(blockId);
|
|
1319
|
+
let collided = blockOwner !== void 0;
|
|
1320
|
+
if (blockOwner !== void 0) errors.push(badBlock({
|
|
1321
|
+
source,
|
|
1322
|
+
blockId
|
|
1323
|
+
}, "f", blockCollisionDetail(source, blockOwner, this.blockIds.has(blockId))));
|
|
1324
|
+
else newBlockIds.set(blockId, source);
|
|
1325
|
+
for (const id of outcome.entryIds) {
|
|
1326
|
+
const loaded = this.ids.get(id);
|
|
1327
|
+
const earlier = newIds.get(id);
|
|
1328
|
+
if (!newIds.has(id)) newIds.set(id, blockId);
|
|
1329
|
+
if (blockOwner !== void 0) continue;
|
|
1330
|
+
const where = loaded !== void 0 ? msg`is already loaded on this mount, in block ${quote(loaded)}` : earlier !== void 0 ? msg`collides with an entry of block ${quote(earlier)} earlier in the same input` : null;
|
|
1331
|
+
if (where === null) continue;
|
|
1332
|
+
errors.push(badBlock({
|
|
1333
|
+
source,
|
|
1334
|
+
blockId
|
|
1335
|
+
}, "f", msg`entry id ${quote(id)} ${where}`, id));
|
|
1336
|
+
collided = true;
|
|
1337
|
+
}
|
|
1338
|
+
if (outcome.block && !collided) {
|
|
1339
|
+
accepted.push(outcome.block);
|
|
1340
|
+
warnings.push(...outcome.warnings);
|
|
1341
|
+
}
|
|
1342
|
+
}
|
|
1343
|
+
if (errors.length > 0) throw new McpFakesAddError(errors, warnings);
|
|
1344
|
+
for (const [source, k] of pendingLoads) this.loadCounts.set(source, k);
|
|
1345
|
+
if (runtimeNumber !== null) this.runtimeAdds = runtimeNumber;
|
|
1346
|
+
for (const [id, owner] of newIds) this.ids.set(id, owner);
|
|
1347
|
+
for (const [blockId, source] of newBlockIds) this.blockIds.set(blockId, source);
|
|
1348
|
+
this.blocks.push(...accepted);
|
|
1349
|
+
return { warnings };
|
|
1350
|
+
}
|
|
1351
|
+
/**
|
|
1352
|
+
* The `<n>` for the next run-time add of `kind`: one more than the last one
|
|
1353
|
+
* used, skipping any number whose `<kind>#<n>` already starts a loaded block
|
|
1354
|
+
* id (and so every entry id of that block). A file source may be spelled
|
|
1355
|
+
* `code#1`; without the skip, every later run-time add would collide with it
|
|
1356
|
+
* and fail, and a failed add does not advance `<n>`. Blocks loaded after a
|
|
1357
|
+
* run-time add still collide with it as usual (the later block fails).
|
|
1358
|
+
*/
|
|
1359
|
+
nextRuntimeNumber(kind) {
|
|
1360
|
+
let n = this.runtimeAdds + 1;
|
|
1361
|
+
while (this.blockIdPrefixTaken(`${kind}#${n}`)) n += 1;
|
|
1362
|
+
return n;
|
|
1363
|
+
}
|
|
1364
|
+
/**
|
|
1365
|
+
* A loaded block id (or one in `pending`, the blocks earlier in the same
|
|
1366
|
+
* input) starts with `prefix` and does not go on with a digit, so `prefix`
|
|
1367
|
+
* would start a colliding block or entry id (`a@2` is taken by `a@2` and
|
|
1368
|
+
* `a@2[0]`, not by `a@23`).
|
|
1369
|
+
*/
|
|
1370
|
+
blockIdPrefixTaken(prefix, pending) {
|
|
1371
|
+
for (const ids of [this.blockIds, pending ?? /* @__PURE__ */ new Map()]) for (const blockId of ids.keys()) if (blockId.startsWith(prefix) && !/^[0-9]/.test(blockId.slice(prefix.length))) return true;
|
|
1372
|
+
return false;
|
|
1373
|
+
}
|
|
1374
|
+
/** Unload every fake and its state. The id counters (`<n>`, `<k>`) are kept. */
|
|
1375
|
+
clear() {
|
|
1376
|
+
this.blocks = [];
|
|
1377
|
+
this.ids.clear();
|
|
1378
|
+
this.blockIds.clear();
|
|
1379
|
+
this.resetState();
|
|
1380
|
+
}
|
|
1381
|
+
/**
|
|
1382
|
+
* Reset consumption state for one test id, or all. Also clears an
|
|
1383
|
+
* `evicted` mark. `MCP_FAKES_DEFAULT_TEST_ID` resets the untagged state as
|
|
1384
|
+
* well as an explicit test id of that name. That matches
|
|
1385
|
+
* `Journal.clearMatchCounts` (which `LLMock.resetMatchCounts` calls), where
|
|
1386
|
+
* untagged requests and an explicit `__default__` share one key.
|
|
1387
|
+
*/
|
|
1388
|
+
resetState(testId) {
|
|
1389
|
+
if (testId === void 0) {
|
|
1390
|
+
this.consumed.clear();
|
|
1391
|
+
this.untagged.clear();
|
|
1392
|
+
this.evicted.clear();
|
|
1393
|
+
return;
|
|
1394
|
+
}
|
|
1395
|
+
this.consumed.delete(testId);
|
|
1396
|
+
this.evicted.delete(testId);
|
|
1397
|
+
if (testId === MCP_FAKES_DEFAULT_TEST_ID) this.untagged.clear();
|
|
1398
|
+
}
|
|
1399
|
+
/** Every entry id on this mount, in load order. */
|
|
1400
|
+
allEntryIds() {
|
|
1401
|
+
return this.blocks.flatMap(blockEntryIds);
|
|
1402
|
+
}
|
|
1403
|
+
/** The blocks whose scope matches `identity`, in load order. A frozen list of frozen blocks. */
|
|
1404
|
+
applicable(identity) {
|
|
1405
|
+
return Object.freeze(this.blocks.filter((b) => scopeMatches(b.scope, identity)));
|
|
1406
|
+
}
|
|
1407
|
+
/** The candidate entries for `tool` from the most specific tier declaring it. */
|
|
1408
|
+
selectTier(tool, identity) {
|
|
1409
|
+
const blocks = this.applicable(identity);
|
|
1410
|
+
for (const tier of TIER_ORDER) {
|
|
1411
|
+
const entries = [];
|
|
1412
|
+
for (const block of blocks) {
|
|
1413
|
+
if (block.tier !== tier) continue;
|
|
1414
|
+
const t = block.tools.find((x) => x.name === tool);
|
|
1415
|
+
if (t) for (const entry of t.calls) entries.push({
|
|
1416
|
+
block,
|
|
1417
|
+
entry
|
|
1418
|
+
});
|
|
1419
|
+
}
|
|
1420
|
+
if (entries.length > 0) return {
|
|
1421
|
+
tier,
|
|
1422
|
+
entries
|
|
1423
|
+
};
|
|
1424
|
+
}
|
|
1425
|
+
return null;
|
|
1426
|
+
}
|
|
1427
|
+
/**
|
|
1428
|
+
* Find the first unconsumed matching entry of the selected tier (load order,
|
|
1429
|
+
* then `calls` order) and mark it consumed, synchronously (no `await`
|
|
1430
|
+
* between find and mark), so concurrent calls never share an entry. The
|
|
1431
|
+
* result is built and checked to serialize as JSON before the entry is
|
|
1432
|
+
* marked, so a result that cannot be sent throws and leaves the entry
|
|
1433
|
+
* unconsumed. An `anyArgs` entry answers any `arguments`, a non-object
|
|
1434
|
+
* value included; only an `args` entry needs an object.
|
|
1435
|
+
*/
|
|
1436
|
+
claim(tool, args, identity) {
|
|
1437
|
+
const selection = this.selectTier(tool, identity);
|
|
1438
|
+
if (!selection) return Object.freeze({ kind: "none" });
|
|
1439
|
+
const testId = identity.testId;
|
|
1440
|
+
if (testId !== null && this.evicted.has(testId)) return Object.freeze({
|
|
1441
|
+
kind: "evicted",
|
|
1442
|
+
tool,
|
|
1443
|
+
testId,
|
|
1444
|
+
maxTestIds: this.maxTestIds
|
|
1445
|
+
});
|
|
1446
|
+
const used = this.stateOf(testId);
|
|
1447
|
+
const received = args === void 0 ? {} : args;
|
|
1448
|
+
for (const { block, entry } of selection.entries) {
|
|
1449
|
+
if (used?.has(entry.id)) continue;
|
|
1450
|
+
if (!callMatches(entry, received)) continue;
|
|
1451
|
+
let result;
|
|
1452
|
+
try {
|
|
1453
|
+
result = toCallToolResult(entry);
|
|
1454
|
+
} catch (err) {
|
|
1455
|
+
throw new Error(build(msg`MCP fake entry ${quote(entry.id)}: its tools/call result cannot be built: ${describeThrown(err)}`), { cause: err });
|
|
1456
|
+
}
|
|
1457
|
+
try {
|
|
1458
|
+
JSON.stringify(result);
|
|
1459
|
+
} catch (err) {
|
|
1460
|
+
throw new Error(build(msg`MCP fake entry ${quote(entry.id)}: result cannot be serialized as JSON: ${describeThrown(err)}`), { cause: err });
|
|
1461
|
+
}
|
|
1462
|
+
this.stateFor(testId).add(entry.id);
|
|
1463
|
+
return Object.freeze({
|
|
1464
|
+
kind: "answer",
|
|
1465
|
+
tool,
|
|
1466
|
+
entry,
|
|
1467
|
+
block,
|
|
1468
|
+
tier: selection.tier,
|
|
1469
|
+
result
|
|
1470
|
+
});
|
|
1471
|
+
}
|
|
1472
|
+
const matching = selection.entries.filter(({ entry }) => callMatches(entry, received));
|
|
1473
|
+
if (matching.length > 0) return Object.freeze({
|
|
1474
|
+
kind: "exhausted",
|
|
1475
|
+
tool,
|
|
1476
|
+
received,
|
|
1477
|
+
matchingDeclared: matching.length,
|
|
1478
|
+
matchingConsumed: matching.filter(({ entry }) => used?.has(entry.id)).length,
|
|
1479
|
+
matchingIds: Object.freeze(matching.map(({ entry }) => entry.id))
|
|
1480
|
+
});
|
|
1481
|
+
let closest = null;
|
|
1482
|
+
let best = Infinity;
|
|
1483
|
+
for (const { entry } of selection.entries) {
|
|
1484
|
+
if (!entry.args) continue;
|
|
1485
|
+
const score = diffCount(entry.args, received);
|
|
1486
|
+
if (score < best) {
|
|
1487
|
+
best = score;
|
|
1488
|
+
closest = entry;
|
|
1489
|
+
}
|
|
1490
|
+
}
|
|
1491
|
+
return Object.freeze({
|
|
1492
|
+
kind: "mismatch",
|
|
1493
|
+
tool,
|
|
1494
|
+
received,
|
|
1495
|
+
declared: deepFreeze(selection.entries.map(({ entry }) => declaredEntry(entry, used))),
|
|
1496
|
+
firstDifference: closest?.args ? firstDifference(closest.args, received) : ""
|
|
1497
|
+
});
|
|
1498
|
+
}
|
|
1499
|
+
/**
|
|
1500
|
+
* The undeclared-tool policy, first of: `override` (`null` or omitted falls
|
|
1501
|
+
* back to `identity.undeclared`, the request's or session's
|
|
1502
|
+
* `X-AIMock-MCP-Undeclared` / `?undeclared=` value) → `deny` when any
|
|
1503
|
+
* applicable scoped block says `deny` → `allow`.
|
|
1504
|
+
*/
|
|
1505
|
+
policy(identity, override = null) {
|
|
1506
|
+
const effective = override ?? identity.undeclared;
|
|
1507
|
+
if (effective) return effective;
|
|
1508
|
+
for (const block of this.applicable(identity)) if (block.scope !== "shared" && block.undeclaredTools === "deny") return "deny";
|
|
1509
|
+
return "allow";
|
|
1510
|
+
}
|
|
1511
|
+
/** Tool names any applicable block (any tier) declares, in load order. */
|
|
1512
|
+
declaredTools(identity) {
|
|
1513
|
+
const names = /* @__PURE__ */ new Set();
|
|
1514
|
+
for (const block of this.applicable(identity)) for (const t of block.tools) names.add(t.name);
|
|
1515
|
+
return [...names];
|
|
1516
|
+
}
|
|
1517
|
+
/**
|
|
1518
|
+
* Fake tools for `tools/list`, metadata from the most specific
|
|
1519
|
+
* tier declaring each tool; inside that tier, the first block in load order
|
|
1520
|
+
* that declares it. A tool with no `inputSchema` lists `{ type: "object" }`.
|
|
1521
|
+
* Merging with registered tools is the mount's job.
|
|
1522
|
+
*/
|
|
1523
|
+
listTools(identity) {
|
|
1524
|
+
return this.declaredTools(identity).map((name) => {
|
|
1525
|
+
const tool = this.selectTier(name, identity)?.entries[0].block.tools.find((t) => t.name === name);
|
|
1526
|
+
const listing = {
|
|
1527
|
+
name,
|
|
1528
|
+
inputSchema: tool?.inputSchema ? copyJson(tool.inputSchema) : { type: "object" }
|
|
1529
|
+
};
|
|
1530
|
+
if (tool?.description !== void 0) listing.description = tool.description;
|
|
1531
|
+
return listing;
|
|
1532
|
+
});
|
|
1533
|
+
}
|
|
1534
|
+
/**
|
|
1535
|
+
* For `GET /__aimock/mcp/fakes`: applicable blocks with consumed state for
|
|
1536
|
+
* `testId`. For a test id the cap evicted, every block says `evicted: true`.
|
|
1537
|
+
* Deeply frozen.
|
|
1538
|
+
*/
|
|
1539
|
+
snapshot(testId, context) {
|
|
1540
|
+
const used = this.stateOf(testId);
|
|
1541
|
+
const evicted = testId !== null && this.evicted.has(testId);
|
|
1542
|
+
return deepFreeze(this.applicable({
|
|
1543
|
+
testId,
|
|
1544
|
+
context,
|
|
1545
|
+
undeclared: null
|
|
1546
|
+
}).map((block) => ({
|
|
1547
|
+
blockId: block.blockId,
|
|
1548
|
+
source: block.source,
|
|
1549
|
+
mount: block.mount,
|
|
1550
|
+
scope: block.scope,
|
|
1551
|
+
undeclaredTools: block.undeclaredTools ?? "allow",
|
|
1552
|
+
evicted,
|
|
1553
|
+
tools: block.tools.map((t) => ({
|
|
1554
|
+
name: t.name,
|
|
1555
|
+
entries: t.calls.map((c) => declaredEntry(c, used))
|
|
1556
|
+
}))
|
|
1557
|
+
})));
|
|
1558
|
+
}
|
|
1559
|
+
/** The consumed ids for `testId` (`null`: untagged), without creating state. */
|
|
1560
|
+
stateOf(testId) {
|
|
1561
|
+
return testId === null ? this.untagged : this.consumed.get(testId);
|
|
1562
|
+
}
|
|
1563
|
+
stateFor(testId) {
|
|
1564
|
+
if (testId === null) return this.untagged;
|
|
1565
|
+
let set = this.consumed.get(testId);
|
|
1566
|
+
if (!set) {
|
|
1567
|
+
set = /* @__PURE__ */ new Set();
|
|
1568
|
+
this.consumed.set(testId, set);
|
|
1569
|
+
this.enforceCap();
|
|
1570
|
+
}
|
|
1571
|
+
return set;
|
|
1572
|
+
}
|
|
1573
|
+
/**
|
|
1574
|
+
* FIFO cap: evict the oldest test ids until the cap holds, marking each
|
|
1575
|
+
* evicted. A cap of `0` evicts nothing. The marks are not capped (see
|
|
1576
|
+
* `evicted`).
|
|
1577
|
+
*/
|
|
1578
|
+
enforceCap() {
|
|
1579
|
+
if (this.maxTestIds === 0) return;
|
|
1580
|
+
for (const oldest of this.consumed.keys()) {
|
|
1581
|
+
if (this.consumed.size <= this.maxTestIds) break;
|
|
1582
|
+
this.consumed.delete(oldest);
|
|
1583
|
+
this.evicted.add(oldest);
|
|
1584
|
+
}
|
|
1585
|
+
}
|
|
1586
|
+
};
|
|
1587
|
+
/**
|
|
1588
|
+
* The detail of a block-id collision (bad block (f)). The message prefix
|
|
1589
|
+
* already names the block id, so the detail names the two sources.
|
|
1590
|
+
*/
|
|
1591
|
+
function blockCollisionDetail(source, owner, loaded) {
|
|
1592
|
+
return loaded ? msg`block id (this block's source: ${quote(source)}) is already loaded on this mount by source ${quote(owner)}` : msg`block id (this block's source: ${quote(source)}) collides with a block earlier in the same input, from source ${quote(owner)}`;
|
|
1593
|
+
}
|
|
1594
|
+
/** The test-id cap: a non-negative safe integer, `0` meaning unbounded. Anything else throws. */
|
|
1595
|
+
function checkCap(cap) {
|
|
1596
|
+
if (!Number.isSafeInteger(cap) || cap < 0) throw new RangeError(build(msg`MCP fakes maxTestIds must be a non-negative integer (0 = unbounded), got ${shown(cap)}`));
|
|
1597
|
+
return cap;
|
|
1598
|
+
}
|
|
1599
|
+
function declaredEntry(entry, used) {
|
|
1600
|
+
const consumed = used?.has(entry.id) ?? false;
|
|
1601
|
+
return entry.args ? {
|
|
1602
|
+
id: entry.id,
|
|
1603
|
+
args: copyJson(entry.args),
|
|
1604
|
+
consumed
|
|
1605
|
+
} : {
|
|
1606
|
+
id: entry.id,
|
|
1607
|
+
anyArgs: true,
|
|
1608
|
+
consumed
|
|
1609
|
+
};
|
|
1610
|
+
}
|
|
1611
|
+
|
|
1612
|
+
//#endregion
|
|
1613
|
+
export { MCP_FAKES_DEFAULT_MOUNT, MCP_FAKES_DEFAULT_TEST_ID, McpFakeStore, McpFakesAddError, blockIdOf, validateMcpFakes };
|
|
1614
|
+
//# sourceMappingURL=mcp-fakes.js.map
|