Repository navigation
feat: scenario-scoped MCP tool fakes in fixtures - #490
Merged
Merged
Conversation
Adds the mcpFakes fixture types, the fake store (claim, match, echo, error results), message and echo text helpers, FixtureLoadError rules for MCP fakes, request identity helpers, and the journal cap getter. Adds the MCP SDK dev dependencies and the real-client test harness.
MCPMock and the MCP handler answer tools/call from the fake store, with structured MCP_FAKE_* errors, per-test-id scoping and journal records. Adds the auto-mount used by the server and LLMock.
Adds loaders that return fixtures together with service fixtures and MCP fake sources. The recorder keeps top-level keys it does not own, such as mcpFakes, when it merges into an existing file.
The server owns the mount list, auto-mounts /mcp for loaded fakes, and adds the /__aimock MCP fakes control routes and reset doors.
…ugins LLMock and the CLI load mcpFakes blocks and auto-mount them. validate-cli checks mcpFakes. The vitest, jest and pytest plugins load fake blocks and report load errors.
The package entry and the ./mcp subpath export the new loaders, FixtureLoadError, McpFakesAddError, the error codes and the public types.
Adds the MCP fakes guide, control API reference, fixture and integration notes, and a test that runs the documented samples against a real server.
commit: |
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AKuRWApa4KRBia1X1F3anz
Document the MCPMock fake methods, the loaders and their return shape, the exports, entry and block ids, the identity 400 body, the failure metric, the test plugins and the pytest plugin, aimock --config loading, and aimock validate with mcpFakes. Add mcpFakeBlocks to the validate --json samples, and list the missing items in the changelog.
jpr5
marked this pull request as ready for review
October 5, 2026 15:20
jpr5
added a commit
that referenced
this pull request
Oct 7, 2026
… comparison (#494) **Current integrated candidate:** `72ec91bf171d7138b711d18913120d55f6fccd61`, parents `46b523f08bfacd9d6708850b2e6a2efac8b017a6` and main `f3ff1d22bcd5ef593a4ca8f9ba1b06d457878dad` after #479. Independent integration review: NO FINDINGS. Fresh full quality is CLEAN and bounded local completion PASS: 9,561 tests passed, one unchanged optional external-spec skip, 293 files (63.40s). Preparation build, format, lint, three-project types, full tests, final build and exports passed. The protected AC-power interval had no Sleep/Wake/DarkWake events; tree and HEAD remained clean and unchanged. Push and body publication/exact readback are complete. All 13 exact-head CI checks succeeded; GitHub reports MERGEABLE and draft=false. Ready for authorized merge in the prescribed order after this final status publication. Prior statuses and execution SHAs below are historical; reuse unchanged proof only. No merge is claimed yet. No new versions or release are requested. Only adjacent #494/#479 Changed changelog entries conflicted; both were retained and conflict markers removed. Homepage HTML remains byte-identical, so accepted visual proof is reused; src/scripts/version/workflow blobs equal new main. **Current-main integration:** `0b795bcb77585626ffbb7a06be88f66e87901593`, parents `ffbc2c4901a59e03e9140715306c75ad93023426` and main `0002210a42fad8fc68585c26f58fc4a53bfc1677`. New-head push, revised body publication/readback and CI are pending. Earlier candidate identities, quality and publication statements below remain historical. Authorization status is superseded by the current gated instruction above. Versions/workflows remain unchanged; no new versions or release are requested. PR #494 remains draft. Independent integration review reports NO FINDINGS and full integrated local quality is CLEAN at this exact head; new-head push and CI remain pending. **Current documentation successor:** `ffbc2c4901a59e03e9140715306c75ad93023426`, from `3f298d6536f9d3f12d5d6f89fce5387e96175d1b`. Only `CHANGELOG.md` changed; those blobs exactly match independently reviewed concern `cfe96ab2641451a854ff87f4b0b0271fc670c26c` (NO FINDINGS). Runtime, tests, versions and workflows remain byte-identical, so prior quality/review evidence is reused without a new full-suite claim. New-head push, body publication/readback and CI are PENDING. Earlier publication/CI statements below are historical predecessor snapshots. Merge only when the final candidate is ready and CI is green, after required landing-order/latest-main checks; no new versions, merge or release are claimed. PR #494 remains draft. **Preliminary publication:** branch head `3f298d6536f9d3f12d5d6f89fce5387e96175d1b` was pushed successfully by normal fast-forward and verified with `ls-remote`. This revised PR-body publication/readback is pending. CI has just been triggered; no passing result is asserted. Final latest-main integration in the approved landing order, affected validation and remaining shipping gates are still required. Merge and release have not occurred. PR #494 remains a draft. This adds MockServer and WireMock columns to the homepage comparison, two scenario-scoped MCP-fakes rows, and an Agent tool mocking comparison with Mastra and MockServer. It retains the original responsive-table changes: contained horizontal scrolling below 1140px and the desktop sticky header. Only `docs/index.html` changes. The reviewed corrections make WireMock's partial support explicit, label MCP fakes as **preview-only**, provide a tested commit-pinned install, and keep that warning and command visible after either Preview link is clicked. **Draft status:** bounded documentation review is clean at reviewed candidate `ee0653b14b05514268ecd2bec1c2b0bafd9c5e14`. This description is prepared before shipping; the remote PR was still draft at `d0398c03e6134ad8abff2817c816c172fdcd74ab` when read. Refresh after #488 lands, affected-proof revalidation, final shipping quality, final-candidate commit grouping, completion checks, final-candidate publication, exact-head CI, and merge remain pending. No fresh CI success or readiness to merge is claimed. **Current candidate:** `3f298d6536f9d3f12d5d6f89fce5387e96175d1b` after metadata-only message repair; commitlint passes. Its tree `2a8b814131e09001b0f1425957db9da67d0285e9` equals reviewed/tested `ee0653b14b05514268ecd2bec1c2b0bafd9c5e14` exactly. Every mapped commit retains its tree, author and author date; original contributor commits are unchanged. Prior proof, review and quality results are reused by that identity, not claimed as new executions. ## Comparison and release wording - WireMock's Chaos testing cell now says **HTTP faults and delays**, linked to its [primary fault-simulation documentation](https://wiremock.org/docs/simulating-faults/#bad-responses). This describes its fixed/random delays and HTTP faults without implying parity with every aimock chaos control. - The two new aimock cells say **Preview**, the agent-tools header says **aimock MCP fakes (preview)**, and the note explicitly says these features are absent from npm release `1.43.0`. - The documented install is `npm install https://pkg.pr.new/@copilotkit/aimock@d0398c0`. The preview and npm release both report version `1.43.0`; the resolved distribution URL and integrity distinguish them. - The preview paragraph uses only `scroll-margin-top: calc(var(--nav-h) + 3rem)` to clear the existing fixed navigation and reveal translation. The command wraps on mobile. No runtime implementation, package version, or release is changed. ## Comparison source record The following preserves the original source mapping checked on 2026-10-05, with the WireMock correction rechecked on 2026-10-06. Unchanged source reviews are reused; this does not claim a new exhaustive runtime certification of competitors. Each competitor cell is backed by a documented source, checked on 2026-10-05. The aimock MCP-fakes cells describe the explicitly qualified preview feature merged in #490, not availability in the default npm release. The ✗ cells for MSW, VidaiMock, mock-llm, piyook/llm-mock and mokksy/ai-mocks in the two new rows follow from their existing ✗ in "MCP tool mocking". <details> <summary>Main table: MockServer and WireMock cells → source</summary> MockServer pages are under `https://www.mock-server.com/mock_server/`. WireMock pages are under `https://wiremock.org/docs/`. | Row | MockServer | Source | WireMock | Source | |---|---|---|---|---| | Cross-process interception | ✓ | https://www.mock-server.com/ | ✓ | standalone/java-jar/, standalone/docker/, solutions/kubernetes/ | | Chat Completions SSE | ✓ | llm_response_mocking.html | Not documented | solutions/ai/ | | Responses API SSE | ✓ | llm_response_mocking.html | Not documented | solutions/ai/ | | Claude Messages API | ✓ | llm_response_mocking.html | Not documented | solutions/ai/ | | Gemini streaming | ✓ | llm_response_mocking.html | Not documented | solutions/ai/ | | Multi-provider support | ✓ | llm_response_mocking.html | Not documented | solutions/ai/ | | OpenRouter router / fallback simulation | Partial (OpenAI-compatible endpoint only; no fallback routing) | llm_response_mocking.html | Not documented | solutions/ai/ | | WebSocket APIs | Partial (Java client only) | llm_response_mocking.html | Partial (4.x beta, generic WebSocket only) | websockets/ | | Embeddings API | ✓ | llm_response_mocking.html | Not documented | solutions/ai/ | | Image generation | ✗ | llm_response_mocking.html ("does not ... generate image/audio responses") | Not documented | solutions/ai/ | | Image editing | ✗ | llm_response_mocking.html | Not documented | solutions/ai/ | | Text-to-Speech | ✗ | llm_response_mocking.html | Not documented | solutions/ai/ | | Audio transcription | Not documented | all 93 pages in https://www.mock-server.com/sitemap.xml | Not documented | solutions/ai/ | | Audio translation | Not documented | sitemap scan, as above | Not documented | solutions/ai/ | | Non-speech audio | ✗ | llm_response_mocking.html | Not documented | solutions/ai/ | | Video generation | Not documented | sitemap scan, as above | Not documented | solutions/ai/ | | AG-UI event mocking | Not documented | sitemap scan, as above | Not documented | all 76 pages in https://wiremock.org/sitemap-0.xml | | MCP tool mocking | ✓ | ai_protocol_mocking.html | Not documented | solutions/ai/ | | Scenario-scoped MCP tool fakes (new) | Not documented | ai_protocol_mocking.html | Not documented | solutions/ai/ | | Fail on undeclared MCP tool (new) | Not documented | ai_protocol_mocking.html | Not documented | solutions/ai/ | | A2A agent mocking | ✓ | ai_protocol_mocking.html | Not documented | solutions/ai/ | | Vector DB mocking | Not documented | sitemap scan, as above | Not documented | WireMock sitemap scan, as above | | Search & rerank | Partial (rerank only; no web search) | llm_response_mocking.html | Not documented | WireMock sitemap scan, as above | | Record & replay | ✓ | ai_traffic_inspection.html, configuration_properties.html | ✓ HTTP | record-playback/ | | Drift detection | Partial (HTTP stubs; LLM cassette drift named, not described) | drift_detection.html | ✗ OSS | wiremock-cloud/ (OSS vs Cloud table) | | Sequential / stateful responses | ✓ | llm_response_mocking.html | ✓ HTTP | stateful-behaviour/ | | Request journal | ✓ | debugging_issues.html | ✓ | verifying/ | | Error injection | ✓ | https://www.mock-server.com/ | ✓ HTTP | simulating-faults/ | | Chaos testing | ✓ | https://www.mock-server.com/ | HTTP faults and delays | https://wiremock.org/docs/simulating-faults/#bad-responses | | Streaming physics | ✓ | llm_response_mocking.html | Partial (chunked dribble delay only) | simulating-faults/ | | Structured output / JSON mode | ✓ | creating_expectations.html | Not documented | solutions/ai/ | | Programmatic API | ✓ 8 languages | https://www.mock-server.com/ | ✓ Java/JVM | java-usage/ | | Fixture files | ✓ JSON | initializing_expectations.html | ✓ JSON | standalone/java-jar/ | | GitHub Action | ✓ | https://github.com/marketplace/actions/setup-mockserver | Not documented | WireMock sitemap scan, as above | | Vitest / Jest plugins | Partial (framework-agnostic helper, hooks wired by hand) | mockserver_clients.html | Not documented | WireMock sitemap scan, as above | | Docker + Helm | ✓ | https://www.mock-server.com/ | ✓ (Helm experimental) | standalone/docker/, solutions/kubernetes/ | | Prometheus metrics | ✓ | https://www.mock-server.com/ | Partial (community extension only) | external-resources/ | | Streaming usage chunks | Partial (realtime WebSocket only) | llm_response_mocking.html | Not documented | solutions/ai/ | | Rate limiting headers | Partial (generic 429 rate-limit clause only) | llm_response_mocking.html, configuration_properties.html | Not documented | solutions/ai/ | | Dependencies | JVM, or JVM-less binary | https://www.mock-server.com/ | JVM | standalone/java-jar/ | </details> <details> <summary>Agent tool mocking table: Mastra and MockServer cells → source</summary> Mastra sources: docs https://mastra.ai/docs/evals/experiments and blog https://mastra.ai/blog/introducing-experiment-tool-mocks. MockServer sources are under `https://www.mock-server.com/mock_server/`. | Row | Mastra | Source | MockServer | Source | |---|---|---|---|---| | Works with any agent framework | ✗ (agent targets only) | docs | ✓ (over MCP HTTP) | ai_protocol_mocking.html | | MCP tools | Not documented | docs, blog | ✓ | ai_protocol_mocking.html | | In-process (non-network) tools | ✓ | docs | n/a (a network mock cannot see in-process calls) | — | | Ordered answers per tool | ✓ | docs | Not documented | ai_protocol_mocking.html | | Exact-argument matching | ✓ | docs | Not documented | ai_protocol_mocking.html | | Any-argument matching | ✓ | docs | ✓ (one answer for every call) | ai_protocol_mocking.html | | Fail on undeclared tool | ✓ | docs | Not documented | ai_protocol_mocking.html | | Scripted tool errors | Not documented | docs | ✓ | ai_protocol_mocking.html | | Per-test state and reset | ✓ | docs | Not documented | ai_protocol_mocking.html | | LLM responses mocked in the same scenario | Not documented | docs, blog | Partial (same server; tool mocks not tied to the LLM scenario) | llm_response_mocking.html, ai_protocol_mocking.html | | Create fakes from recorded runs | Partial (how a mock is made from a trace is not described) | docs | ✓ | ai_traffic_inspection.html, ai_mcp_tools.html | | Report unused mocks | ✓ | docs | Not documented | ai_protocol_mocking.html | | Workflows | Planned | blog | n/a | — | </details> ## Local proof and exact revisions | Correction | Local RED / pre-edit revision | Proven fix / accepted integration | |---|---|---| | WireMock cell | `06702702d245676e81fc0183f6626246a8c3bd6f` | Executor `88f13ed6973d29b7cd46f3e700a5fd25e40d5897`; accepted `31191e099c85727dde092e2060bfb297c5f682aa` (same tree) | | Preview qualification | `31191e099c85727dde092e2060bfb297c5f682aa` | Executor `b328ca9b5d2047fd919349b8cdc26749b3bfd10d`; accepted `82e7f11a943abbcb6b808194cee8fee95db0bfa0` | | Preview anchor visibility | `82e7f11a943abbcb6b808194cee8fee95db0bfa0` | Executor `97acc75c21edff093ec4583f328886d524a483ee`; candidate `ee0653b14b05514268ecd2bec1c2b0bafd9c5e14` | These are historical proof identities, not a claim that the eventual pushed/reorganized commits keep the same SHAs. Final candidate and anchor-fix executor have identical HTML blob `b19bfa8ae0663ed06c9fd9ab786cc8235de9f628`. Browser probes record pre-commit HEAD plus the actual working patch; a GREEN line retaining its RED HEAD is not a test of unchanged content. <details> <summary>WireMock: same rendered-cell assertion fails before and passes after at three sizes</summary> Recorded commands: `node probe.cjs red` (exit 1) before the cell edit; `node probe.cjs green` (exit 0) afterward. The original assertion script was retained as `probe-original.cjs`. Both runs locate the actual WireMock table header and Chaos testing row and check text, source URL, and absence of the No label. ```jsonl {"phase":"red","url":"http://127.0.0.1:59889/","head":"06702702d245676e81fc0183f6626246a8c3bd6f","diff":""} {"viewport":"375x667","column":"WireMock","row":"Chaos testing","actual":{"text":"✗","href":null,"no":1},"assertion":"FAIL","screenshot":"red-375x667.png"} {"viewport":"430x932","column":"WireMock","row":"Chaos testing","actual":{"text":"✗","href":null,"no":1},"assertion":"FAIL","screenshot":"red-430x932.png"} {"viewport":"1440x900","column":"WireMock","row":"Chaos testing","actual":{"text":"✗","href":null,"no":1},"assertion":"FAIL","screenshot":"red-1440x900.png"} RED: rendered WireMock cell falsely says No ``` GREEN raw output: ```jsonl {"phase":"green","url":"http://127.0.0.1:59979/","head":"06702702d245676e81fc0183f6626246a8c3bd6f","diff":"diff --git a/docs/index.html b/docs/index.html\nindex 9005c9cb..4889aa83 100644\n--- a/docs/index.html\n+++ b/docs/index.html\n@@ -3363,7 +3363,11 @@\n <td><span class=\"no\" role=\"img\" aria-label=\"No\">✗</span></td>\n <td><span class=\"no\" role=\"img\" aria-label=\"No\">✗</span></td>\n <td><span class=\"yes\" role=\"img\" aria-label=\"Yes\">✓</span></td>\n- <td><span class=\"no\" role=\"img\" aria-label=\"No\">✗</span></td>\n+ <td>\n+ <a href=\"https://wiremock.org/docs/simulating-faults/#bad-responses\"\n+ >HTTP faults and delays</a\n+ >\n+ </td>\n </tr>\n <tr>\n <th scope=\"row\">Streaming physics</th>\n"} {"viewport":"375x667","column":"WireMock","row":"Chaos testing","actual":{"text":"HTTP faults and delays","href":"https://wiremock.org/docs/simulating-faults/#bad-responses","no":0},"assertion":"PASS","screenshot":"green-375x667.png"} {"viewport":"430x932","column":"WireMock","row":"Chaos testing","actual":{"text":"HTTP faults and delays","href":"https://wiremock.org/docs/simulating-faults/#bad-responses","no":0},"assertion":"PASS","screenshot":"green-430x932.png"} {"viewport":"1440x900","column":"WireMock","row":"Chaos testing","actual":{"text":"HTTP faults and delays","href":"https://wiremock.org/docs/simulating-faults/#bad-responses","no":0},"assertion":"PASS","screenshot":"green-1440x900.png"} GREEN: qualified linked cell at all three viewports ``` The early screenshots caught existing reveal/scroll animation before it settled and are **not** the final visual evidence. Supplemental `green-visible` and committed captures used instant scrolling and waited for opacity transitions without changing the DOM assertions. Those images were opened and inspected: the qualified link is legible and wraps within the Chaos testing cell at 375×667, 430×932, and 1440×900. Mobile captures scroll the existing table to WireMock while retaining the row label. </details> <details> <summary>MCP readiness: default npm failure, identical real HTTP smoke succeeds on the pinned preview</summary> Recorded distribution commands (machine-local output directories abbreviated): ```sh npm view @copilotkit/aimock version dist-tags --json npm install --prefix published --ignore-scripts --no-audit --no-fund @copilotkit/aimock npm install --prefix preview --ignore-scripts --no-audit --no-fund https://pkg.pr.new/@copilotkit/aimock@d0398c0 # Within each respective directory, with identical fixture, loader, and smoke: node smoke.mjs ``` The registry then reported `{"version":"1.43.0","dist-tags":{"latest":"1.43.0"}}`. For reproducing that baseline later, pin `@copilotkit/aimock@1.43.0` instead of relying on a moving dist-tag. Registry output: ```json { "version": "1.43.0", "dist-tags": { "latest": "1.43.0" } } ``` `node smoke.mjs` in published directory exits1: ```text Published package: 1.43.0 file:///Users/jpr5/.local/share/copilotkit/cr/aimock-pr-resolution-20261006/pr494/494-mcp-readiness/published/programmatic/load-fakes.mjs:11 billing.loadFakes({ ^ TypeError: billing.loadFakes is not a function at startMock (file:///Users/jpr5/.local/share/copilotkit/cr/aimock-pr-resolution-20261006/pr494/494-mcp-readiness/published/programmatic/load-fakes.mjs:11:11) at file:///Users/jpr5/.local/share/copilotkit/cr/aimock-pr-resolution-20261006/pr494/494-mcp-readiness/published/smoke.mjs:4:19 Node.js v26.8.1 ``` ## Preview actual HTTP GREEN The same script in preview directory exits0: ```text Published package: 1.43.0 Started http://127.0.0.1:63963 {"mount":"/mcp","status":200,"data":{"jsonrpc":"2.0","id":1,"result":{"content":[{"type":"text","text":"{\"tempF\":60,\"conditions\":\"rain\"}"}],"structuredContent":{"tempF":60,"conditions":"rain"},"isError":false}}} {"mount":"/billing","status":200,"data":{"jsonrpc":"2.0","id":1,"result":{"content":[{"type":"text","text":"refunded 10"}],"isError":false}}} PASS real HTTP published package advertised docs samples ``` Smoke script (MCP initialize + initialized notification before real tools/call): ```js import assert from 'node:assert/strict'; import { startMock } from './programmatic/load-fakes.mjs'; console.log('Published package:', JSON.parse((await import('node:fs')).readFileSync('./node_modules/@copilotkit/aimock/package.json')).version); const llm = await startMock(); try { const url = llm.url; console.log('Started', url); for (const [mount, testId, name, args, expected] of [ ['/mcp', 'weather › seattle', 'get_weather', {city:'Seattle'}, '{"tempF":60,"conditions":"rain"}'], ['/billing', 'billing › refund', 'refund', {amount:10}, 'refunded 10']]) { const init = await fetch(url+mount, {method:'POST', headers:{'content-type':'application/json','X-Test-Id':encodeURIComponent(testId)}, body:JSON.stringify({jsonrpc:'2.0',id:0,method:'initialize',params:{protocolVersion:'2025-03-26',capabilities:{},clientInfo:{name:'readiness-smoke',version:'1'}}})}); assert.equal(init.status,200); const session=init.headers.get('mcp-session-id'); assert.ok(session); const ready=await fetch(url+mount,{method:'POST',headers:{'content-type':'application/json','mcp-session-id':session},body:JSON.stringify({jsonrpc:'2.0',method:'notifications/initialized'})}); assert.equal(ready.status,202); const response = await fetch(url+mount, {method:'POST', headers:{'mcp-session-id':session,'content-type':'application/json', 'X-Test-Id':encodeURIComponent(testId)}, body:JSON.stringify({jsonrpc:'2.0',id:1,method:'tools/call',params:{name,arguments:args}})}); const data = await response.json(); console.log(JSON.stringify({mount,status:response.status,data})); assert.equal(response.status,200); assert.equal(data.result.content[0].text,expected); } console.log('PASS real HTTP published package advertised docs samples'); } finally { await llm.stop(); } ``` The final v3 smoke initializes actual MCP sessions, sends `notifications/initialized`, then calls tools over HTTP with the returned session IDs and scenario IDs. Earlier harness attempts missing initialization were discarded as product evidence. Default npm still fails: GREEN demonstrates preview support and truthful documentation, not a repaired default release. Two successful documented calls are covered; undeclared-tool errors and every other MCP option were not freshly exercised. A later install into a new directory retrieved the same preview and the unchanged smoke again returned HTTP 200 for weather and refund. Its lockfile resolves: ```text https://pkg.pr.new/@copilotkit/aimock@d0398c0 sha512-/P8JxS1ClezOTllLqt6uikN9sgw/alsXBi/jyHQJgSfA/8zE907hwa/Jep2aQanEzqGTrr0g+pFn+vTs0iY0WQ== ``` The generic smoke label “Published package: 1.43.0” does not identify the distribution. The npm output supports successful installation and the lockfile identifies the artifact; no independent cache-miss proof or guarantee of future preview-host availability is claimed. A web client's 403 was not an npm-install failure. To reproduce the exact smoke above without access to local evidence files, save these two unchanged linked-doc samples alongside `smoke.mjs` in **each** installation directory. Create the `programmatic` and `weather` subdirectories first. They were extracted from the branch's [MCP example page](https://github.com/CopilotKit/aimock/blob/d0398c03e6134ad8abff2817c816c172fdcd74ab/docs/mcp-mock/index.html). `programmatic/load-fakes.mjs`: ```js import { LLMock, MCPMock } from "@copilotkit/aimock"; export async function startMock() { const llm = new LLMock({ port: 0 }); // A fixture file: its LLM fixtures and its mcpFakes load together. llm.loadFixtureFile("weather/seattle.json"); // Fakes from code, on a mount you create. const billing = new MCPMock(); billing.loadFakes({ scope: { testId: "billing › refund" }, tools: [{ name: "refund", calls: [{ args: { amount: 10 }, result: "refunded 10" }] }], }); llm.mount("/billing", billing); await llm.start(); return llm; } ``` `weather/seattle.json`: ```json { "fixtures": [ { "match": { "userMessage": "weather in Seattle", "hasToolResult": false }, "response": { "toolCalls": [{ "name": "get_weather", "arguments": { "city": "Seattle" } }] } }, { "match": { "userMessage": "weather in Seattle", "hasToolResult": true }, "response": { "content": "It is 60F and raining in Seattle." } } ], "mcpFakes": { "scope": { "testId": "weather › seattle" }, "tools": [ { "name": "get_weather", "calls": [ { "args": { "city": "Seattle" }, "result": { "tempF": 60, "conditions": "rain" } } ] } ] } } ``` </details> <details> <summary>Rendered preview qualification: real homepage RED/GREEN at all three sizes</summary> Recorded commands: `node visual.cjs red` before the qualification edit and `node visual.cjs final` afterward. The assertions stayed the same; supplementary screenshot positioning centered the new note. The final mobile image also verified the bounded command-wrapping correction. ```text visual-red.log {"viewport":"375x667","actual":{"text":"aimock MCP fakes","href":"/mcp-mock","preview":0,"labels":["Built-in ✓","Built-in ✓"],"command":[]},"assertion":"FAIL","screenshot":"red-375x667.png"} {"viewport":"430x932","actual":{"text":"aimock MCP fakes","href":"/mcp-mock","preview":0,"labels":["Built-in ✓","Built-in ✓"],"command":[]},"assertion":"FAIL","screenshot":"red-430x932.png"} {"viewport":"1440x900","actual":{"text":"aimock MCP fakes","href":"/mcp-mock","preview":0,"labels":["Built-in ✓","Built-in ✓"],"command":[]},"assertion":"FAIL","screenshot":"red-1440x900.png"} FAIL: missing preview qualification for unpublished MCP fakes visual-final.log {"viewport":"375x667","actual":{"text":"aimock MCP fakes (preview)","href":"/mcp-mock","preview":1,"labels":["Preview ✓","Preview ✓"],"command":["npm install https://pkg.pr.new/@copilotkit/aimock@d0398c0"]},"assertion":"PASS","screenshot":"final-375x667.png"} {"viewport":"430x932","actual":{"text":"aimock MCP fakes (preview)","href":"/mcp-mock","preview":1,"labels":["Preview ✓","Preview ✓"],"command":["npm install https://pkg.pr.new/@copilotkit/aimock@d0398c0"]},"assertion":"PASS","screenshot":"final-430x932.png"} {"viewport":"1440x900","actual":{"text":"aimock MCP fakes (preview)","href":"/mcp-mock","preview":1,"labels":["Preview ✓","Preview ✓"],"command":["npm install https://pkg.pr.new/@copilotkit/aimock@d0398c0"]},"assertion":"PASS","screenshot":"final-1440x900.png"} PASS: preview-qualified MCP fakes at all three viewports ``` Final-note images were opened at all three required sizes and the complete preview warning and wrapping install command were inspected. The separate click-navigation failure discovered afterward was fixed and verified below. </details> <details> <summary>Preview navigation: both links fail before, then expose the full warning and command at all three sizes</summary> Recorded commands: `node probe.cjs red` (exit 1) before the paragraph-only offset; `node probe.cjs green` (exit 0) after. Both run actual clicks for both Preview links at 375×667, 430×932, and 1440×900. The same click/destination assertions were used. ```text red.log {"viewport":"375x667","linkIndex":0,"actual":{"text":"MCP fakes require a preview package; they are not included in npm release 1.43.0. To try\n the MCP fakes examples, install the verified\n preview:\n npm install https://pkg.pr.new/@copilotkit/aimock@d0398c0.","top":-30.015625,"bottom":126.546875,"navHeight":105,"margin":"0px"},"assertion":"FAIL","screenshot":"red-clicked0-375x667.png"} {"viewport":"375x667","linkIndex":1,"actual":{"text":"MCP fakes require a preview package; they are not included in npm release 1.43.0. To try\n the MCP fakes examples, install the verified\n preview:\n npm install https://pkg.pr.new/@copilotkit/aimock@d0398c0.","top":-0.015625,"bottom":156.546875,"navHeight":105,"margin":"0px"},"assertion":"FAIL","screenshot":"red-clicked1-375x667.png"} {"viewport":"430x932","linkIndex":0,"actual":{"text":"MCP fakes require a preview package; they are not included in npm release 1.43.0. To try\n the MCP fakes examples, install the verified\n preview:\n npm install https://pkg.pr.new/@copilotkit/aimock@d0398c0.","top":-30.015625,"bottom":127.546875,"navHeight":105,"margin":"0px"},"assertion":"FAIL","screenshot":"red-clicked0-430x932.png"} {"viewport":"430x932","linkIndex":1,"actual":{"text":"MCP fakes require a preview package; they are not included in npm release 1.43.0. To try\n the MCP fakes examples, install the verified\n preview:\n npm install https://pkg.pr.new/@copilotkit/aimock@d0398c0.","top":-0.015625,"bottom":157.546875,"navHeight":105,"margin":"0px"},"assertion":"FAIL","screenshot":"red-clicked1-430x932.png"} {"viewport":"1440x900","linkIndex":0,"actual":{"text":"MCP fakes require a preview package; they are not included in npm release 1.43.0. To try\n the MCP fakes examples, install the verified\n preview:\n npm install https://pkg.pr.new/@copilotkit/aimock@d0398c0.","top":-29.703125,"bottom":23.484375,"navHeight":70,"margin":"0px"},"assertion":"FAIL","screenshot":"red-clicked0-1440x900.png"} {"viewport":"1440x900","linkIndex":1,"actual":{"text":"MCP fakes require a preview package; they are not included in npm release 1.43.0. To try\n the MCP fakes examples, install the verified\n preview:\n npm install https://pkg.pr.new/@copilotkit/aimock@d0398c0.","top":0.296875,"bottom":53.484375,"navHeight":70,"margin":"0px"},"assertion":"FAIL","screenshot":"red-clicked1-1440x900.png"} FAIL: Preview click hides warning/command behind fixed navigation green.log {"viewport":"375x667","linkIndex":0,"actual":{"text":"MCP fakes require a preview package; they are not included in npm release 1.43.0. To try\n the MCP fakes examples, install the verified\n preview:\n npm install https://pkg.pr.new/@copilotkit/aimock@d0398c0.","top":122.984375,"bottom":279.546875,"navHeight":105,"margin":"153px"},"assertion":"PASS","screenshot":"green-clicked0-375x667.png"} {"viewport":"375x667","linkIndex":1,"actual":{"text":"MCP fakes require a preview package; they are not included in npm release 1.43.0. To try\n the MCP fakes examples, install the verified\n preview:\n npm install https://pkg.pr.new/@copilotkit/aimock@d0398c0.","top":152.984375,"bottom":309.546875,"navHeight":105,"margin":"153px"},"assertion":"PASS","screenshot":"green-clicked1-375x667.png"} {"viewport":"430x932","linkIndex":0,"actual":{"text":"MCP fakes require a preview package; they are not included in npm release 1.43.0. To try\n the MCP fakes examples, install the verified\n preview:\n npm install https://pkg.pr.new/@copilotkit/aimock@d0398c0.","top":122.984375,"bottom":280.546875,"navHeight":105,"margin":"153px"},"assertion":"PASS","screenshot":"green-clicked0-430x932.png"} {"viewport":"430x932","linkIndex":1,"actual":{"text":"MCP fakes require a preview package; they are not included in npm release 1.43.0. To try\n the MCP fakes examples, install the verified\n preview:\n npm install https://pkg.pr.new/@copilotkit/aimock@d0398c0.","top":152.984375,"bottom":310.546875,"navHeight":105,"margin":"153px"},"assertion":"PASS","screenshot":"green-clicked1-430x932.png"} {"viewport":"1440x900","linkIndex":0,"actual":{"text":"MCP fakes require a preview package; they are not included in npm release 1.43.0. To try\n the MCP fakes examples, install the verified\n preview:\n npm install https://pkg.pr.new/@copilotkit/aimock@d0398c0.","top":88.296875,"bottom":141.484375,"navHeight":70,"margin":"118px"},"assertion":"PASS","screenshot":"green-clicked0-1440x900.png"} {"viewport":"1440x900","linkIndex":1,"actual":{"text":"MCP fakes require a preview package; they are not included in npm release 1.43.0. To try\n the MCP fakes examples, install the verified\n preview:\n npm install https://pkg.pr.new/@copilotkit/aimock@d0398c0.","top":118.296875,"bottom":171.484375,"navHeight":70,"margin":"118px"},"assertion":"PASS","screenshot":"green-clicked1-1440x900.png"} PASS: both Preview links expose complete warning and command at all three viewports ``` `final-visual.cjs final` changes only the source-cell horizontal positioning from centered to nearest so source row names remain visible. Both independent final reviewers inspected all 12 final screenshots: both source rows and both clicked destinations, each at all three sizes. They verified the release warning, examples link, and **entire** install command visibly below navigation. Red destination pixels corroborate the original occlusion. Geometry is supplementary, not the visual verdict. Evidence hashes and exact HTML blob equality were checked before reusing the captures. </details> <details> <summary>Portable browser repro scripts: source, commands, and visual inspection procedure</summary> These copies make the recorded probes reproducible without machine-local scripts. Only environment setup is adapted: `require('playwright')`, Playwright's installed Chromium, and `DOCS_REPO` instead of the original absolute worktree path. They were not freshly executed while drafting this description; recorded results above belong to the original scripts/revisions. In a separate proof directory, install Playwright and Chromium, then save the scripts below under their shown names: ```sh npm install --no-save playwright npx playwright install chromium # Point at the actual checked-out before or after revision; use separate worktrees. DOCS_REPO=/absolute/path/to/aimock-before node wiremock.cjs red DOCS_REPO=/absolute/path/to/aimock-after node wiremock.cjs green DOCS_REPO=/absolute/path/to/aimock-before node qualification.cjs red DOCS_REPO=/absolute/path/to/aimock-after node qualification.cjs final DOCS_REPO=/absolute/path/to/aimock-before node anchor.cjs red DOCS_REPO=/absolute/path/to/aimock-after node anchor.cjs green ``` Use each correction's before/after identities in the revision table; local fix SHAs become externally retrievable only when published. The original PR head also contains the pre-correction WireMock and preview-qualification failures. The anchor RED requires the intermediate qualified-page revision. To reproduce the anchor failure on a final copy if that intermediate history is unavailable after regrouping, remove **only** the target paragraph's added `scroll-margin-top` declaration; label that run a reconstructed before-state, not an original historical execution. Open generated PNGs and inspect actual pixels at all sizes. The original WireMock script deliberately preserves its original timing for RED/GREEN parity; for settled visual captures use instant `scrollIntoView` and wait for the existing reveal transition, leaving assertions unchanged. For final navigation source-row framing, change only `inline:'center'` to `inline:'nearest'`; keep the same clicks and destination checks. Do not treat bounding boxes alone as visual success. `wiremock.cjs`: ```js const { chromium } = require('playwright'); const http = require('node:http'); const fs = require('node:fs'); const path = require('node:path'); const { execFileSync } = require('node:child_process'); const repo = process.env.DOCS_REPO; if (!repo) throw new Error('DOCS_REPO must name the checked-out aimock worktree'); const out = __dirname; const phase = process.argv[2]; (async () => { const server = http.createServer((req,res) => { const file = path.resolve(repo,'docs','.' + decodeURI(req.url.split('?')[0] === '/' ? '/index.html' : req.url.split('?')[0])); if (!file.startsWith(path.join(repo,'docs') + '/')) { res.writeHead(403).end(); return; } try { res.setHeader('Content-Type',file.endsWith('.html')?'text/html':file.endsWith('.css')?'text/css':file.endsWith('.js')?'text/javascript':'application/octet-stream'); res.end(fs.readFileSync(file)); } catch { res.writeHead(404).end(); } }); await new Promise(resolve => server.listen(0,'127.0.0.1',resolve)); const url = `http://127.0.0.1:${server.address().port}/`; const browser = await chromium.launch({headless:true}); let passed = true; try { console.log(JSON.stringify({phase,url,head:execFileSync('git',['rev-parse','HEAD'],{cwd:repo,encoding:'utf8'}).trim(),diff:execFileSync('git',['diff','--','docs/index.html'],{cwd:repo,encoding:'utf8'})})); for (const [width,height] of [[375,667],[430,932],[1440,900]]) { const page = await browser.newPage({viewport:{width,height}}); await page.goto(url); await page.evaluate(() => document.fonts.ready); const table = page.locator('table').filter({has:page.getByRole('columnheader',{name:'WireMock',exact:true})}); const headers = await table.locator('thead th').allTextContents(); const index = headers.findIndex(text => text.trim() === 'WireMock'); if (index < 0) throw new Error('WireMock header missing'); const row = table.getByRole('row').filter({has:page.getByRole('rowheader',{name:'Chaos testing',exact:true})}); const cell = row.locator('td').nth(index-1); await cell.evaluate(el=>el.scrollIntoView({block:'center',inline:'center'})); const actual = {text:(await cell.innerText()).trim(),href:await cell.locator('a').count() ? await cell.locator('a').getAttribute('href'):null,no:await cell.locator('[aria-label="No"]').count()}; const ok = actual.text === 'HTTP faults and delays' && actual.href === 'https://wiremock.org/docs/simulating-faults/#bad-responses' && actual.no === 0; passed &&= ok; await page.screenshot({path:path.join(out,`${phase}-${width}x${height}.png`)}); console.log(JSON.stringify({viewport:`${width}x${height}`,column:headers[index].trim(),row:'Chaos testing',actual,assertion:ok?'PASS':'FAIL',screenshot:`${phase}-${width}x${height}.png`})); await page.close(); } } finally { await browser.close(); await new Promise(resolve=>server.close(resolve)); } console.log(passed?'GREEN: qualified linked cell at all three viewports':'RED: rendered WireMock cell falsely says No'); process.exitCode = passed?0:1; })().catch(e=>{ console.error(e); process.exit(2); }); ``` `qualification.cjs`: ```js const { chromium } = require('playwright'); const http = require('node:http'); const fs = require('node:fs'); const path = require('node:path'); const { execFileSync } = require('node:child_process'); const repo = process.env.DOCS_REPO; if (!repo) throw new Error('DOCS_REPO must name the checked-out aimock worktree'); const out = __dirname; const phase = process.argv[2]; (async () => { const server = http.createServer((req,res) => { const file = path.resolve(repo,'docs','.' + decodeURI(req.url.split('?')[0] === '/' ? '/index.html' : req.url.split('?')[0])); if (!file.startsWith(path.join(repo,'docs') + '/')) { res.writeHead(403).end(); return; } try { res.setHeader('Content-Type',file.endsWith('.html')?'text/html':file.endsWith('.css')?'text/css':file.endsWith('.js')?'text/javascript':'application/octet-stream'); res.end(fs.readFileSync(file)); } catch { res.writeHead(404).end(); } }); await new Promise(resolve => server.listen(0,'127.0.0.1',resolve)); const url = `http://127.0.0.1:${server.address().port}/`; const browser = await chromium.launch({headless:true}); let passed = true; try { console.log(JSON.stringify({phase,url,head:execFileSync('git',['rev-parse','HEAD'],{cwd:repo,encoding:'utf8'}).trim(),diff:execFileSync('git',['diff','--','docs/index.html'],{cwd:repo,encoding:'utf8'})})); for (const [width,height] of [[375,667],[430,932],[1440,900]]) { const page = await browser.newPage({viewport:{width,height}}); await page.goto(url); await page.evaluate(() => document.fonts.ready); const table = page.locator('.agent-tools-table'); await table.evaluate(el=>el.scrollIntoView({block:'center',behavior:'instant'})); await page.waitForTimeout(1500); const link = table.getByRole('link',{name:/aimock MCP fakes/}); const actual = {text:await link.innerText(),href:await link.getAttribute('href')}; actual.preview = await page.locator('#mcp-fakes-preview').count(); actual.labels = await page.locator('.comparison-table tr').filter({has:page.getByRole('rowheader',{name:/^(Scenario-scoped MCP tool fakes|Fail on undeclared MCP tool)$/})}).locator('.col-aimock').allTextContents(); actual.command = await page.locator('#mcp-fakes-preview code').allTextContents(); const ok = actual.href === '/mcp-mock' && actual.text.includes('preview') && actual.preview === 1 && actual.labels.length === 2 && actual.labels.every(t=>t.includes('Preview')) && actual.command.includes('npm install https://pkg.pr.new/@copilotkit/aimock@d0398c0'); passed &&= ok; await page.screenshot({path:path.join(out,`${phase}-${width}x${height}.png`)}); console.log(JSON.stringify({viewport:`${width}x${height}`,actual,assertion:ok?'PASS':'FAIL',screenshot:`${phase}-${width}x${height}.png`})); if (actual.preview) { await page.locator('#mcp-fakes-preview').evaluate(el=>el.scrollIntoView({block:'center',behavior:'instant'})); await page.waitForTimeout(1000); await page.screenshot({path:path.join(out,`${phase}-note-${width}x${height}.png`)}); } await page.close(); } } finally { await browser.close(); await new Promise(resolve=>server.close(resolve)); } console.log(passed?'PASS: preview-qualified MCP fakes at all three viewports':'FAIL: missing preview qualification for unpublished MCP fakes'); process.exitCode = passed?0:1; })().catch(e=>{ console.error(e); process.exit(2); }); ``` `anchor.cjs`: ```js const { chromium } = require('playwright'); const http = require('node:http'); const fs = require('node:fs'); const path = require('node:path'); const { execFileSync } = require('node:child_process'); const repo = process.env.DOCS_REPO; if (!repo) throw new Error('DOCS_REPO must name the checked-out aimock worktree'); const out = __dirname; const phase = process.argv[2]; (async () => { const server = http.createServer((req,res) => { const file = path.resolve(repo,'docs','.' + decodeURI(req.url.split('?')[0] === '/' ? '/index.html' : req.url.split('?')[0])); if (!file.startsWith(path.join(repo,'docs') + '/')) { res.writeHead(403).end(); return; } try { res.setHeader('Content-Type',file.endsWith('.html')?'text/html':file.endsWith('.css')?'text/css':file.endsWith('.js')?'text/javascript':'application/octet-stream'); res.end(fs.readFileSync(file)); } catch { res.writeHead(404).end(); } }); await new Promise(resolve => server.listen(0,'127.0.0.1',resolve)); const url = `http://127.0.0.1:${server.address().port}/`; const browser = await chromium.launch({headless:true}); let passed = true; try { console.log(JSON.stringify({phase,url,head:execFileSync('git',['rev-parse','HEAD'],{cwd:repo,encoding:'utf8'}).trim(),diff:execFileSync('git',['diff','--','docs/index.html'],{cwd:repo,encoding:'utf8'})})); for (const [width,height] of [[375,667],[430,932],[1440,900]]) { const page = await browser.newPage({viewport:{width,height}}); await page.goto(url); await page.evaluate(() => document.fonts.ready); const links = page.locator('a[href="#mcp-fakes-preview"]'); for (let index=0;index<2;index++) { const link=links.nth(index); await link.evaluate(el=>el.scrollIntoView({block:'center',inline:'center',behavior:'instant'})); await page.waitForTimeout(1200); await page.screenshot({path:path.join(out,`${phase}-row${index}-${width}x${height}.png`)}); await link.click(); await page.waitForTimeout(1600); const actual = await page.locator('#mcp-fakes-preview').evaluate(el=>({text:el.textContent.trim(),top:el.getBoundingClientRect().top,bottom:el.getBoundingClientRect().bottom,navHeight:parseFloat(getComputedStyle(document.documentElement).getPropertyValue('--nav-h')),margin:getComputedStyle(el).scrollMarginTop})); const ok = actual.top >= actual.navHeight && actual.bottom <= height; passed &&= ok; await page.screenshot({path:path.join(out,`${phase}-clicked${index}-${width}x${height}.png`)}); console.log(JSON.stringify({viewport:`${width}x${height}`,linkIndex:index,actual,assertion:ok?'PASS':'FAIL',screenshot:`${phase}-clicked${index}-${width}x${height}.png`})); } await page.close(); } } finally { await browser.close(); await new Promise(resolve=>server.close(resolve)); } console.log(passed?'PASS: both Preview links expose complete warning and command at all three viewports':'FAIL: Preview click hides warning/command behind fixed navigation'); process.exitCode = passed?0:1; })().catch(e=>{ console.error(e); process.exit(2); }); ``` </details> ## Checks and review boundaries Recorded targeted commands: ```sh pnpm exec prettier --check docs/index.html pnpm exec vitest run src/__tests__/competitive-matrix-homepage.test.ts pnpm typecheck git diff --check ``` The fix evidence records **40 homepage tests passed**, formatting passed, and diff-check passed. The final candidate pre-audit records all three TypeScript configurations passing. Both final independent documentation roles returned **NO FINDINGS** at `ee0653b14b05514268ecd2bec1c2b0bafd9c5e14`, with zero deferred findings in the bounded subject. They inspected provenance-verified raw execution evidence and pixels; they did not rerun installs or browser probes during confirmation. Unchanged earlier factual proof was reused. Coverage is Chromium at 375×667, 430×932, and 1440×900. Safari/Firefox, arbitrary zoom/viewports, keyboard or assistive technology, reduced motion, no JavaScript, and direct initial fragment loading were not freshly tested. No deployed routing audit or exhaustive competitor/MCP certification is claimed. The original author reported additional responsive widths; this correction pass does not recertify those historical captures. ## Remaining work and limits - Reintegrate current main after #488, recheck affected proof and source identity, and complete applicable final shipping quality, final-candidate commit grouping, completion checks, final-candidate publication/description update, exact-head CI, and merge. This draft is not evidence those steps have happened. - The desktop table has limited width headroom; another column requires another layout check. Between 769 and 1139px, its horizontal scrollbar remains at the bottom of the tall table. - The weekly competitor updater does not yet track MockServer/WireMock columns. Pre-existing migration-page provider-count behavior remains outside this PR. - Merging documentation does **not** release MCP fakes. Default npm `1.43.0` remains unsupported for these examples; the explicit tested preview path is the supported claim of this page. No release is requested by this PR. ## Preparatory local quality — CLEAN Candidate `ee0653b14b05514268ecd2bec1c2b0bafd9c5e14`: Passing format, lint and all-three-project typecheck evidence was reused on the unchanged candidate; test, build and exports were rerun serially. `pnpm test`, `pnpm build`, and `pnpm test:exports` exited 0. Full suite: **9,521 passed, 1 skipped** (9,522 total), 291 files passed. The unchanged optional spec-heading check skipped because `AIMOCK_MCP_FAKES_SPEC` was absent. Existing built CLI was exercised; no test was newly skipped or disabled. Builds retain three nonfatal tsdown `define` warnings. Earlier failed runs remain preserved; this serial recovery changed no source, dependency, configuration or timeout. This is exact-candidate preparatory evidence. Final shipping requires the approved landing order, latest-main integration, applicable affected-proof and quality revalidation, final-candidate commit grouping/authorship, completion checks, final-candidate publication/body update and exact-head CI. Merge remains pending; no release or CI success is claimed. ### Documentation follow-up proof The same actual-file check ran before edits (exit 1) and on the committed worker (exit 0). This is missing/present documentation proof, not a runtime defect. The GREEN output includes the actual Unreleased entry. Recorded command: ```sh python3 /Users/jpr5/.local/share/copilotkit/cr/aimock-pr-resolution-20261006/pr494/docs-followup/doc-proof.py ``` RED: ```text HEAD 3f298d6536f9d3f12d5d6f89fce5387e96175d1b Unreleased/Changed subject coverage: ["comparisons", "WireMock", "MCP", "preview"] matching entries: 0 FAIL: actual Unreleased section omits this reviewed PR subject Documentation check FAIL ``` GREEN (worker execution, not relabelled as integration execution): ```text HEAD cfe96ab2641451a854ff87f4b0b0271fc670c26c Unreleased/Changed subject coverage: ["comparisons", "WireMock", "MCP", "preview"] matching entries: 1 PASS: - Expand the landing-page comparisons, qualify WireMock fault support, and label MCP scenario fakes as preview-only with a pinned preview installation (#494). Documentation check PASS ``` Save this original probe outside the checkout and run from its root for reproduction without local artifact access: ```python from pathlib import Path import subprocess,re,sys,json p=Path.cwd() print('HEAD',subprocess.check_output(['git','rev-parse','HEAD'],text=True).strip()) s=(p/'CHANGELOG.md').read_text() u=re.split(r'\n## ',s)[1] category='Changed' section=re.search(r'(?ms)^### '+re.escape(category)+r'\n(.*?)(?=^### |\Z)',u) bullets=[] if section is None else re.findall(r'(?m)^- .*',section.group(1)) required=['comparisons', 'WireMock', 'MCP', 'preview'] matches=[b for b in bullets if all(t in b for t in required)] print('Unreleased/'+category+' subject coverage:',json.dumps(required),'matching entries:',len(matches)) fail=not matches if fail:print('FAIL: actual Unreleased section omits this reviewed PR subject') else:print('PASS:',matches[0]) if 494==451: text=(p/'README.md').read_text() checks=['/__aimock/openapi.json','/__aimock/routes','{ count, routes }','buildOpenApiDocument','CATALOG_ROUTES','ROUTE_DEFINITIONS'] missing=[t for t in checks if t not in text] print('README catalog guidance missing:',json.dumps(missing)) fail=fail or bool(missing) print('FAIL: public catalog usage is undocumented' if missing else 'PASS: README documents real catalog endpoints, envelope and exports') print('Documentation check', 'FAIL' if fail else 'PASS') sys.exit(1 if fail else 0) ``` Markdown formatting, diff checks and commitlint passed for the documentation concern. No new runtime tests or version/workflow changes were made. ### Clean integration with current main Merged `0002210a42fad8fc68585c26f58fc4a53bfc1677` into published `ffbc2c4901a59e03e9140715306c75ad93023426` without conflict or manual edits. The file historically named `merge-red.log` records a successful merge, not RED; no failure is invented. Command: `git merge --no-commit --no-ff 0002210a42fad8fc68585c26f58fc4a53bfc1677`. Actual output: ```text Auto-merging CHANGELOG.md Auto-merging docs/index.html Automatic merge went well; stopped before committing as requested ``` Only `CHANGELOG.md` and `docs/index.html` differ from integrated main. Runtime source and the competitive-matrix script equal main. Incoming HTML changes set the mock-llm and piyook/llm-mock **Responses API SSE** cells to No; reviewed WireMock HTTP faults/delays, preview labels, pinned install and navigation offset remain intact. Fresh direct validation: ```sh pnpm exec vitest run src/__tests__/competitive-matrix pnpm exec prettier --check docs/index.html CHANGELOG.md ``` ```text Test Files 9 passed (9) Tests 241 passed (241) Checking formatting... All matched files use Prettier code style! ``` Actual locally served merged-page browser checks passed both Preview links and incoming Responses API SSE cells at 375×667, 430×932 and 1440×900. Screenshots were opened and visually inspected, including the warning/command below the fixed navigation and the incoming No cells. The initial probe targeted the wrong Chat Completions row; it was corrected without editing the page and is not credited as product RED. Existing pinned-artifact evidence remains valid. Independent integration review now reports NO FINDINGS at `0b795bcb77585626ffbb7a06be88f66e87901593`, after inspecting all 21 screenshots. Fresh full integrated local quality is CLEAN: format, lint, source/test/scripts typecheck, full tests, build and package exports passed; 9,558 tests passed and one unchanged optional external-spec check skipped across 292 files. CLI tests used fresh dist; the existing three nonfatal `define` warnings remain disclosed. New-head push and CI remain pending; administrator merging remains subject to the current readiness/CI gates. The integration reviewer verified staged-merge screenshot provenance: capture logs retain the first-parent HEAD because images preceded the merge commit. The final HTML contains exactly the two incoming No cells plus unchanged accepted preview/WireMock surfaces. The 21 images are not represented as having logged the final commit SHA; no fresh cross-browser or accessibility claim is made. ### Integration after #492 landed Actual command: `git merge --no-commit --no-ff c35471dbd262ed7eb2248c74382d531f9461870e`. Successful output (not RED): ```text Auto-merging CHANGELOG.md Automatic merge went well; stopped before committing as requested ``` The complete homepage HTML remains blob `45008fac769cf99bdc63b8966ea8b05c876bbe3b`, identical to accepted parent `0b795bcb77585626ffbb7a06be88f66e87901593`. The independent reviewer reuses the 21 previously inspected screenshots and accepted Preview navigation, WireMock and Responses API SSE evidence; no rerender or new screenshot execution is claimed. Incoming runtime/OpenRouter documentation equals main, and only CHANGELOG.md plus docs/index.html differ from main. The #494 entry remains under **Changed**; #492/#488/#480 entries remain under **Fixed**. No entry was lost or duplicated. Independent integration review: **NO FINDINGS**. The earlier full local quality attempt FAILED: `pnpm test` exited 1 with 17 failed / 9,541 passed / 1 skipped tests across 17 failed and 275 passed files, plus 4 unhandled errors. That failure and its diagnosis remain preserved; no source fix is claimed. A subsequently authorized recovery on the identical candidate completed successfully, as recorded below. The failed run is not relabelled as passing. Push, publication/readback, exact-head CI and merge remain pending. Version, package and workflow files remain unchanged. For the already-landed #492 merge, actual Release run 37567151181 succeeded with npm publish skipped and Python build/publish steps skipped; no new version was published by that run. This predecessor result does not claim a future release-check outcome for this PR. No merge or new release is asserted here. ### Exact-head local quality recovery On unchanged `46b523f08bfacd9d6708850b2e6a2efac8b017a6`, one authorized standard recovery run passed preparation build, format, lint, all three TypeScript projects, full tests, final build and exports. **9,558 tests passed, one unchanged optional external-spec check skipped; 292 files passed.** Local completion is PASS. There were no source/configuration/timeout changes or further attempts within that recovery; earlier failure artifacts remain intact. Temporary process-scoped `caffeinate -is` protected the 84.24-second gate interval on AC power. OS power logs contain no Sleep, Wake or DarkWake transitions during that interval; the assertion process exited afterward, without persistent power-setting changes. Full tests took 63.51 seconds and the earlier 17 failures did not recur. Existing `define` build warnings remain disclosed. Accepted exact-head integration review and unchanged visual proof remain applicable. This establishes recovered local quality only. Root verified successful push of `46b523f08bfacd9d6708850b2e6a2efac8b017a6`; revised body publication/readback and exact-head CI remain pending; no merge, release or new version is claimed. ### Current merge conflict evidence ## Real local RED ```json { "command": "git merge --no-commit --no-ff f3ff1d22bcd5ef593a4ca8f9ba1b06d457878dad", "exit_code": 1, "stdout": "Auto-merging CHANGELOG.md\nCONFLICT (content): Merge conflict in CHANGELOG.md\nAutomatic merge failed; fix conflicts and then commit the result.\n", "stderr": "Recorded preimage for 'CHANGELOG.md'\n" } ``` Only CHANGELOG.md conflicted, at adjacent #494/#479 Changed entries. Before editing the actual conflict was inspected. Resolution retained both entire bullet lines and removed only marker lines, including diff3 base marker. No other manual edit. ## Same-merge GREEN ```sh git add -- CHANGELOG.md pnpm exec prettier --check CHANGELOG.md git diff --cached --check git commit -m 'Merge main after decimal parser cleanup into PR 494' ``` Before commit checks: ```json { "checks": [ { "command": "git diff --name-only --diff-filter=U", "exit": 0, "stdout": "", "stderr": "" }, { "command": "git diff --cached --exit-code 46b523f08bfacd9d6708850b2e6a2efac8b017a6 -- docs/index.html", "exit": 0, "stdout": "", "stderr": "" }, { "command": "git diff --cached --exit-code f3ff1d22bcd5ef593a4ca8f9ba1b06d457878dad -- src scripts package.json pnpm-lock.yaml .github", "exit": 0, "stdout": "", "stderr": "" } ], "changelog": "Both #479 and #494 Changed entries retained; #492/#488/#480 also retained; no markers" } ``` The resulting merge is clean; both Changed entries and #492/#488/#480 Fixed entries remain. No new visual execution or runtime fix is claimed. Independent integration review reports NO FINDINGS; fresh full quality is CLEAN and bounded local completion PASS at this new head. Push is complete; body publication/readback and exact-head CI remain pending.
jpr5
added a commit
that referenced
this pull request
Oct 21, 2026
Refreshes the docs overview "What's New" cards for the last month of work, and adds three verified rows to the comparison matrix (homepage and the four scanned migration pages). ## What's New (`docs/docs/index.html`) The cards were last updated in May. The 5 old cards are replaced with 8 cards (two full rows of the 4-column grid). They cover CHANGELOG 1.43.0 (2026-09-22) through `[Unreleased]` (1.46.0, #562): | Card | Covers | Links to | | --- | --- | --- | | Model Catalogs & Token Counts | Native Anthropic/Gemini model list and detail routes. The Anthropic list on shared `/v1/models` is opt-in (`modelCatalog`). Anthropic `count_tokens` and Gemini `countTokens` estimates (#542, #551, #568) | `/claude-messages#model-catalog` | | Native Auth Errors | Opt-in provider-native 401 bodies (`authErrors`) and the opt-in required-header policy (`requiredHeaders`) (#542, #566) | `/claude-messages#authentication-errors` | | Model Misbehavior | Opt-in misbehavior faults (#508, #518) | `/model-misbehavior` | | MCP Record & Replay | MCP recording/replay, scenario-scoped tool fakes, per-test fake reports (#490, #509, #517) | `/mcp-mock#recorded-fakes` | | Stream Realism | Anthropic streams stop and journal on client disconnect (#557). Opt-in `nonStreamingTiming` for OpenAI non-streaming replies (#545, #561, #567) | `/streaming-physics#non-streaming` | | Responses API Tools | Opt-in (`responsesTools: "extended"`) namespaced and custom tool calls, plus message phases (#511, #513, #531) | `/responses-api#namespaced-and-custom-tool-calls` | | OpenAI Platform APIs | Files, Batches, Fine-tuning jobs, Vector Stores (#445, #446, #447, #497) | `/vector-stores` | | GPT-Live Sessions | GPT-Live mock, record and replay (#468) | `/websocket#openai-live` | The Model Catalogs & Token Counts, Native Auth Errors and Stream Realism cards carry a "New in 1.46.0" label, and the Responses API Tools card marks its message phases "(new in 1.46.0)". Every opt-in is called out on its card: `requiredHeaders`, `authErrors`, `modelCatalog` (the Anthropic shared list), `nonStreamingTiming`, misbehavior and extended Responses tools. The version banner is not touched. ## Comparison matrix ### New rows Homepage (`docs/index.html`), inserted before "Dependencies": | Row | aimock | MSW | VidaiMock | mock-llm | piyook/llm-mock | mokksy/ai-mocks | MockServer | WireMock | | --- | --- | --- | --- | --- | --- | --- | --- | --- | | Native model catalogs (Anthropic, Gemini) | Built-in ✓ | ✗ | OpenAI format only | OpenAI format only | ✗ | ✗ | ✗ | ND | | Token-count endpoints | Built-in ✓ | ✗ | Gemini only (fixed count) | ✗ | ✗ | ✗ | ✗ | ND | | Native 401 auth errors | Opt-in ✓ | manual | ✓ | manual | partial | manual | partial | manual | The migration pages for VidaiMock, mock-llm, piyook and Mokksy get the same three rows, in each page's own style: VidaiMock uses text cells, and the other three use the ✓/✗ glyphs with text for partial or manual values. No new legend marks are needed. The MSW, Python-mocks and openai-responses pages have different columns, so they are unchanged. Every aimock ✓ cell on the three new rows carries a muted "1.46.0+" label, on the homepage and on the four migration pages. The label uses a `.version-note` class in the muted style of the existing "Requires 1.45.0+" table note. ### Rows considered and not added - Already on the matrix: record & replay, drift detection, MCP / A2A / AG-UI, streaming physics, chaos testing, model misbehavior, provider count. - Client-disconnect handling: dropped. Most competitors' source and docs say nothing about it, so the row would be mostly ND. A server that drops a closed stream is normal framework behavior, not something a competitor's docs state. - Recorded timing replay: dropped. I could not verify the MockServer and WireMock cells from their docs, and Streaming physics already covers timing. ### Per-cell evidence (checked 2026-10-20) Competitor sources are the repos listed in `scripts/update-competitive-matrix.ts`, at the default-branch HEAD on that date: VidaiMock `e61b8ba`, mock-llm `aeb04d6`, piyook/llm-mock `9e7a3ce`, mokksy/ai-mocks `fc70777`, MockServer `c892f58`. The homepage table has an HTML comment on each new row with a short evidence note. **Native model catalogs** - aimock: Gemini `GET /v1beta/models` list and detail are native by default. Anthropic `/v1/models/{id}` detail is native by default. The Anthropic list on shared `/v1/models` needs `modelCatalog: "native"`. See `/claude-messages#model-catalog` and `/gemini#model-catalog`. - MSW: has no LLM endpoints. It is marked ✗, the same as the Embeddings and Image rows. - VidaiMock: `src/server.rs` registers `GET /v1/models`, `/v1beta/models` and `/v1beta/openai/models` on `models_handler`. (Its Anthropic `GET /v1/models/{model_action}` route is never registered, because the Gemini `POST /v1/models/{model_action}` route claims the path first.) In `src/handlers.rs`, `models_handler` always returns the OpenAI list shape: `{"object":"list","data":[{"id","object":"model","created","owned_by":"vidai"}]}`. So its Gemini catalog route is not in native format, and there is no Anthropic catalog. - mock-llm: the README's default config ships one OpenAI-format `/v1/models` rule (`{"object":"list","data":[{"id":"gpt-5.2",...}]}`). There is no Anthropic or Gemini catalog. - piyook/llm-mock: the README's "Available endpoints" lists only chat completions, `/v1/messages` and `/v1/embeddings`. `src/api/` has only `completions` and `embeddings`. - mokksy/ai-mocks: `MockAnthropic` exposes only `messages()` (`POST /v1/messages`), and `MockGemini` exposes only `generateContent` and `generateContentStream`. The repo has no models route (code search for `v1/models` finds nothing). - MockServer: the `httpLlmResponse` supported-provider path table (mock-server.com `llm_response_mocking.html`) has no models route. `docs/code/llm-mocking.md` names `GET /v1/models` only as proxied traffic that gets no telemetry. - WireMock: has no LLM-specific docs (`wiremock.org/docs/solutions/ai/` is about using AI to write stubs). It is ND, the same as WireMock's other LLM-endpoint cells. **Token-count endpoints** - aimock: Anthropic `POST /v1/messages/count_tokens` and Gemini `:countTokens` estimate tokens from the translated request. See `/claude-messages#token-counts` and `/gemini#token-counts`. - VidaiMock: `config/providers/gemini_count_tokens.yaml` matches `^/v1(beta)?/models/[^:]+:countTokens$`. Its template `config/templates/gemini/count_tokens.json.j2` always returns `"totalTokens": 9`. There is no Anthropic `count_tokens` provider config. - mock-llm and mokksy/ai-mocks: GitHub code search for `count_tokens` (mock-llm) and `countTokens` (mokksy) finds nothing. Neither README or API has a count route. - piyook/llm-mock: `src/api/` has only `completions` and `embeddings`, and the endpoint list has no count route. - MockServer: `docs/code/llm-mocking.md` names Anthropic `/v1/messages/count_tokens` only as unaccounted proxied traffic. There is no `httpLlmResponse` for it. - MSW ✗ and WireMock ND, for the same reasons as the catalog row. **Native 401 auth errors** - aimock: opt-in `policy.authErrors: "native"` returns Anthropic `authentication_error` and Gemini `UNAUTHENTICATED` bodies. See `/claude-messages#authentication-errors` and `/gemini#authentication-errors`. - VidaiMock ✓: the README lists `X-Mock-Status` and `?chaos_status=` overrides that return "real provider error envelopes". `config/templates/anthropic/error.json.j2` maps 401 to `authentication_error`. `config/templates/gemini/error.json.j2` maps 401 to `UNAUTHENTICATED`. - piyook/llm-mock partial: chaos `status` accepts any status from 400 to 599. `src/utilities/chaos.ts` types every status except 429/529 (Claude) or 429/503 (Gemini) as `api_error` or `INTERNAL`. So a 401 is not typed `authentication_error` or `UNAUTHENTICATED`. The README also says "LLMock has no authentication". - mock-llm manual: the README example is a hand-written rule that returns an OpenAI `invalid_api_key` 401. - mokksy/ai-mocks manual: `AbstractBuildingStep.respondsError` lets you set any status and body. - MockServer partial: the LLM chaos `errorStatus` accepts 401. With no `errorKind`, `LlmErrorBodies.kindForStatus` (`mockserver-core/src/main/java/org/mockserver/llm/LlmErrorBodies.java`) maps every status except 429 and 529 to `SERVER_ERROR`. So a 401 gets the provider envelope typed `api_error` or `INTERNAL`, not `authentication_error` or `UNAUTHENTICATED`. This is the same behavior as piyook. - MSW and WireMock manual: both are generic HTTP stubs with any status and body. ### Weekly scanner compatibility No `FEATURE_RULES` entry or migration variant names the new rows, so the Monday scan never reads or writes them. The `GET /v1/models` rule stays row-less. Its keyword (`/v1/models`) would match any OpenAI models endpoint, which is not what the catalog row compares. A new test, `hand-reviewed homepage rows with no scan rule` in `competitive-matrix-homepage.test.ts`, checks four things: - every new label is on the homepage; - no rule or migration variant names a new label; - when every competitor matches every rule, the homepage keeps every new-row cell; - when every rule matches, each scanned migration page keeps its new rows. I mutation-tested it: adding `"Token-count endpoints"` as a migration variant fails 4 of its tests. `docs-matrix-legend.test.ts`: the homepage ND count goes from 45 to 47, because of the two WireMock ND cells. Dry run (`npx tsx scripts/update-competitive-matrix.ts --dry-run`, with a GitHub token), before and after the change: ``` before: Parsed 41 capability rows, 8 competitor columns. after: Parsed 44 capability rows, 8 competitor columns. both: No homepage matrix changes detected. 1 migration page row(s) to check by hand: mock-llm / Azure OpenAI (combined-row) [pre-existing] 12 row-less detection(s) to check by hand [identical list] ``` The only difference is the row count. No change is proposed to the new rows or anywhere else. ## Checks - `prettier --check .`, `eslint .` and `tsdown` build: pass. - The docs and competitive-matrix tests (`docs-*`, `competitive-matrix*`, `mcp-fakes-doc-samples`): 13 files, 530 tests, all pass. - Full `vitest run`: 17610 passed. One test failed: a timing assertion in `chaos-fal-single-roll.test.ts`. That file only measures chaos latency, and this PR does not change it. It passes when run alone (4/4). - Local Playwright CLI screenshots, at 375x667, 430x932 and 1440x900: - What's New, on this branch and on main. - The homepage matrix, on this branch and on main. - The four edited migration pages. - On phones, the homepage matrix scrolls sideways inside its wrapper, as it does on main. The new rows add no width. - What's New now fills two full grid rows. On main, the fifth card sat alone in a second row. - The "On this page" panel overlaps the quick-start card at 1440 wide. Main does the same.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What & why
Fixture files can now declare MCP tool fakes for a scenario, in an
mcpFakesblock next to the LLM turns of the same test. Each block names its scenario (a test id or a context). Inside a block you can:undeclaredTools: "deny"), so a call to any undeclared tool is an error;denyreturns a JSON-RPC error with a stable code inerror.data.aimock.code. The failure is also written to the journal, a metric and the server log.An agent test has two inputs that change from run to run: what the LLM says, and what the tools return. aimock already pins the LLM side. For MCP tools, aimock could only give one fixed answer per tool for the whole server process. A test could not say "in this scenario,
get_weather(Seattle)returns rain the first time and a timeout the second time, and any other call is a bug." So teams either ran real MCP servers in tests (side effects, drift, flaky runs) or wrote code handlers that cannot be shared as data and do not reset per test.This works at the wire, over Streamable HTTP, so it is the same for LangGraph, CrewAI, ADK, PydanticAI, Mastra or a plain SDK client, in any language. In-process tool mocks, such as Mastra's Experiment Tool Mocks, only cover agents of one framework. Because the fakes live in fixture files, a later recording mode can write real MCP tool results into the same file as the LLM turns, so one file holds the whole scenario.
How to use
tickets/retry.json(fromsrc/__tests__/fixtures/mcp-fakes/tickets/retry.json):{ "fixtures": [ { "match": { "userMessage": "open a refund ticket", "hasToolResult": false }, "response": { "toolCalls": [{ "name": "create_ticket", "arguments": { "title": "Refund" } }] } } ], "mcpFakes": { "scope": { "testId": "tickets › retry on timeout" }, "tools": [ { "name": "create_ticket", "calls": [ { "id": "first-try", "args": { "title": "Refund" }, "error": "upstream timeout" }, { "id": "retry", "args": { "title": "Refund" }, "result": "TICKET-42" } ] } ] } }Start aimock with
--fixtureson that directory. aimock mounts an MCP mock at/mcpif there is none. Point the agent's MCP client at/mcp?testId=tickets%20%E2%80%BA%20retry%20on%20timeout(or sendX-Test-Id). The firstcreate_ticketcall getsisError: true"upstream timeout", the second getsTICKET-42, and a third fails withMCP_FAKE_EXHAUSTED.What changed
mcpFakestypes, the validator, the fake store (scope, ordering, matching, consumption, eviction), the error codes and the request identity helpers (header, query, session).tools/callgoes to fakes first, with the error bodies for mismatch, exhausted, not declared, evicted and invalid override.tools/listincludes faked tools. Journal entries record the body, test id, context and fake outcome. There is a failure metric.GETon the mount answers 405.mcpFakesfrom fixture files. NewloadFixtureFileWithServices/loadFixturesFromDirWithServices(insrc/fixture-loader-services.ts). The old free loaders throw on a file that holdsmcpFakesinstead of dropping it silently./mcp, mount-conflict checks.POST /__aimock/fixturesacceptsmcpFakes. NewGET /__aimock/mcp/fakes. Reset andDELETE /__aimock/fixturesalso unload fakes.LLMockloads fakes before and after start. The CLI supports fakes-only starts,--watchand remote URLs, and reports every bad block.aimock validatechecks blocks and cross-file id collisions. The vitest and jest plugins load fakes and failbeforeAllon a bad block. The pytest plugin'sload_fixturespostsmcpFakesand detects a server that is too old. The MCP SDK v1 and v2 clients are pinned as dev dependencies for the real-client tests.McpFakesAddError,MCP_FAKE_ERROR_CODESand the fake types, from the root and the./mcpsubpath.User-visible changes
GETon an MCP mount answers 405 withAllow: POST, DELETEinstead of falling through to the LLM routes. This includes a standaloneMCPMock.tools/calljournal entries carry the requestbody(wasnull), andresponse.mcpFake = { id, outcome }when a fake answered or failed.testIdandcontext.GET /__aimock/journal?testId=lists an MCP request under the test id bound atinitialize, or under the decoded header value, instead of"__default__"or the encoded string.X-Test-IdandX-AIMock-Context. Answers on a mount without fakes do not change.MCP_DUPLICATE_IDENTITY. Nothing changes for clients that never do that.X-AIMock-MCP-Undeclaredheader or?undeclared=query parameter (allowordeny) overrides the undeclared-tool policy for a request, or for the session when sent atinitialize. Withdeny, anytools/callto a tool that no fake declares fails withMCP_FAKE_NOT_DECLARED, even on a mount with no fakes loaded, soonToolCallhandlers do not run.MCP_INVALID_UNDECLAREDon every MCP request, also on a mount without fakes.POST /__aimock/fixtures:fixturesis optional whenmcpFakesis sent, and the response addsmcpFakesAdded. Existing callers still get{"added": n}.POST /__aimock/reset,DELETE /__aimock/fixtures,LLMock.reset()andLLMock.clearFixtures()also clear fakes.undeclaredTools: "deny", an undeclared tool fails even when anonToolCallhandler or a configresultexists for it.aimock validateaccept fakes-only files.mcpFakes).tools/callwhosenameis not a string (for example123) getsInvalid tool name: expected a string, got numberinstead ofUnknown tool: 123. A missing or empty name still getsMissing tool name.body, instead of one entry per HTTP request. Journal andgetRequests()counts go up for batch callers.load_fixturesstill raisesrequests.HTTPErroron a 400 with a JSON body, but the message is nowaimock rejected fixtures from <path>: <error>, plus one line perdetailsitem, instead of the defaultrequeststext.aimock validate --jsonalways includesrun.mcpFakeBlocks, which is0when no file hasmcpFakes.mcpFakesblock, a mount conflict, or a--watchreload whosemcpFakeschanged fails the load instead of being skipped. On--watchthe previous fixtures stay loaded. No effect withoutmcpFakes.mcpFakes: the free functionsloadFixtureFileandloadFixturesFromDirthrow on a file with a top-levelmcpFakeskey. Use the...WithServicesloaders or theLLMockmethods.unreadableonFixturesWithServices, the return type of the services loaders: the sources of files that could not be read or were not valid JSON.mcpFakessilently. The MCPMock docs warn about this.MCPMockcode API (addTool,onToolCall,addResource,addPrompt) does not change.Proof
Red-green, same command on the base and on this branch:
RED (base
origin/mainplus the test only):The base mounts nothing at
/mcpand does not load themcpFakesblock.GREEN (this branch):
The env-var gate is removed on this branch, so plain
pnpm testruns this test as well (287 files, 9349 passed at that point). Typecheck, lint, format check and build exit 0. Mutation checks on the CLI paths (watch guard, URL source label, fakes-only start, print every error) each turn one test red.Value check with the built CLI (
node dist/cli.js --fixtures <dir>), no real LLM calls:createReactAgentusing@langchain/mcp-adaptersMultiServerMCPClient(http transport) called the fakedget_weather, got{"tempF":60,"conditions":"rain"}, and printedFINAL: It is 60F and raining in Seattle.(exit 0). The journal entry hasresponse.mcpFake = {"id":"weather/seattle.json:get_weather#0","outcome":"answered"}.MCP_FAKE_MISMATCHinerror.data.aimock.code, withfirstDifference: "$.city: expected \"Portland\", received \"Seattle\"". The journal recordedoutcome: "mismatch"and the server log printed anMCP-FAKE: mismatchline.-31010), any-args and closed-world (not declared,-32602) scenarios returned error bodies that match the documented shapes field for field.Note from the value check: LangGraph's default
ToolNode(handleToolErrors: true) turns the MCP error into a tool message, so a default agent can still print the expected answer. The journal outcome and the server log catch that case.Known limitations
LLMockafter start, the start-time hand-off to existing MCP mocks, and control-API adds that span several mounts. This is documented on the MCPMock page.Follow-ups (backlog)
/healthbody shape after auto-mount.docs/integrate-mafshows the wrong command (@copilotkit/aimock --fixturesas npx arguments).Release
Release surfaces are deferred to the release commit. There is no version bump in this PR. The changelog entries are under Unreleased.
🤖 Generated with Claude Code
https://claude.ai/code/session_01AKuRWApa4KRBia1X1F3anz