MCPMock

Mock MCP (Model Context Protocol) server for testing tool integrations. Implements the Streamable HTTP transport with JSON-RPC dispatch, session management, and full tools/resources/prompts support.

Quick Start

Standalone mode typescript
import { MCPMock } from "@copilotkit/aimock";

const mcp = new MCPMock();

mcp.addTool({ name: "search", description: "Search the web" });
mcp.onToolCall("search", (args) => {
  return `Results for: ${(args as { query: string }).query}`;
});

const url = await mcp.start();
// Point your MCP client at `url`

Mounted Mode

Mount MCPMock onto an LLMock server to share a single port with LLM mocking and other services:

Mount on LLMock typescript
import { LLMock, MCPMock } from "@copilotkit/aimock";

const llm = new LLMock({ port: 5555 });
const mcp = new MCPMock();

mcp.addTool({ name: "calc", description: "Calculator" });
mcp.onToolCall("calc", (args) => "42");

llm.mount("/mcp", mcp);
await llm.start();
// MCP available at http://127.0.0.1:5555/mcp

Subpath Import

MCPMock is also available via a dedicated subpath import for tree-shaking:

Subpath import typescript
import { MCPMock } from "@copilotkit/aimock/mcp";

Tools

Register tools and their handlers:

Tools API typescript
// Register a tool definition
mcp.addTool({
  name: "search",
  description: "Search the web",
  inputSchema: { type: "object", properties: { query: { type: "string" } } },
});

// Register a handler (returns string or MCPContent[])
mcp.onToolCall("search", (args) => {
  const { query } = args as { query: string };
  return `Found 3 results for "${query}"`;
});

// Or return rich content
mcp.onToolCall("rich-tool", () => [
  { type: "text", text: "Hello" },
  { type: "image", data: "base64...", mimeType: "image/png" },
]);

Scenario fakes (fixtures)

A fixture file can hold an mcpFakes key next to its fixtures array. It scripts the MCP tool answers for one test scenario: an ordered list of answers per tool, exact or any-argument matching, and an optional closed world that fails on any tool the scenario does not declare. The LLM turns and the tool answers of a scenario live in one file. Fakes work at the wire, so any MCP client that speaks Streamable HTTP can use them, in any language or agent framework.

tickets/retry.json 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" }
        ]
      }
    ]
  }
}

In the test tickets › retry on timeout, the first create_ticket call with {"title":"Refund"} gets a tool error with the text upstream timeout, which the model sees and can retry. The second call gets TICKET-42. A third call fails with MCP_FAKE_EXHAUSTED.

Minimum version: mcpFakes needs the aimock release that includes MCP fakes. An older aimock does not serve your fakes. It ignores the mcpFakes key without a warning in a file that also has a fixtures array. A file with only mcpFakes loads no fixtures and logs Missing or invalid "fixtures" array. If the run then has no fixtures at all, llmock starts with a warning, or exits under --validate-on-load or --strict.

Format

mcpFakes is one block or an array of blocks. Use an array when an agent talks to several MCP servers (one block per mount) or when one file holds several scopes. A block has these keys:

Key Meaning
scope Required. { "testId": "..." }, { "context": "..." }, both, or the string "shared".
mount Optional. The MCP mount path, starting with /. Default /mcp.
undeclaredTools Optional. "allow" (default) or "deny". See Closed world.
tools Required array. Each tool has a name (exact, case-sensitive), an optional description and inputSchema (used for tools/list), and a non-empty calls array of answers. Only a deny block with a test id or context scope may have an empty tools array.

Each item of calls has these keys:

Key Meaning
args A JSON object. The call matches only when its arguments are equal to it.
anyArgs true. The call matches whatever its arguments are.
result The success answer: a string, a content array or an object (see below).
error A string. The answer is a tool execution error, { content: [{ type: "text", text }], isError: true }. The model sees it and can retry.
id Optional name for the entry, used in errors, the journal and inspection.

Each entry has exactly one of args or anyArgs, and exactly one of result or error. A result becomes a tools/call result like this:

A single answer with an object result. In the test weather › seattle, the model first asks for get_weather. The call with {"city":"Seattle"} gets structuredContent {"tempF":60,"conditions":"rain"} and the same JSON as a text block. After the tool result, the model answers It is 60F and raining in Seattle.

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" } }
        ]
      }
    ]
  }
}

An entry without an id gets one from its file, tool and position: for example, if the first entry above had no id, its id would be tickets/retry.json:create_ticket#0. A file in a fixtures directory is named by its path relative to that directory. An entry with an id uses it in place of the tool and position, as in tickets/retry.json:first-try. Entry ids are unique on each mount.

The part before the colon is the block id. In the array form, it ends with the block’s position, as in multi/travel.json[0]:search_flights#0. When the same file is loaded again, its blocks are added again, with @2, @3 and so on after the file name (weather/seattle.json@2:get_weather#0). Blocks added from code get the block id code#<n>, and blocks added through the control API get control-api#<n>. Each mount numbers these calls from 1.

An array of blocks, one per MCP server. aimock mounts an MCPMock at /mcp/flights and one at /mcp/hotels. In the test travel › book trip, search_flights on /mcp/flights answers UA 100 departs 09:00 for any arguments, and book_hotel on /mcp/hotels answers HOTEL-7 for {"city":"Paris"}. Each mount serves only its own block: a book_hotel call on /mcp/flights gets Unknown tool: book_hotel.

multi/travel.json json
{
  "mcpFakes": [
    {
      "scope": { "testId": "travel › book trip" },
      "mount": "/mcp/flights",
      "tools": [
        {
          "name": "search_flights",
          "calls": [{ "anyArgs": true, "result": "UA 100 departs 09:00" }]
        }
      ]
    },
    {
      "scope": { "testId": "travel › book trip" },
      "mount": "/mcp/hotels",
      "tools": [
        { "name": "book_hotel", "calls": [{ "args": { "city": "Paris" }, "result": "HOTEL-7" }] }
      ]
    }
  ]
}

Why scope is required

All fixture files load into one server. Without a scope, a fake written for one test would answer every other test. So a block with no scope fails the load. Use "shared" only for answers that every test may get.

How a request picks blocks

Each MCP request resolves a test id and a context, each on its own:

On initialize, the test id, context and undeclared override that the request sends are stored on the session, so later requests in that session need not send them again. A value sent on a later request wins over the session value for that field only.

A testId scope matches only a request whose test id is exactly equal to it (case-sensitive, no normalization). A context scope works the same way. A scope with both needs both. "shared" matches every request. A request with no test id and no context sees only shared blocks.

When more than one block declares a tool, the most specific level wins: test id and context, then test id only, then context only, then shared. Only the entries of that level are used for that tool, so a scenario that declares get_weather replaces a shared get_weather, and two scenarios never see each other’s entries.

A field sent twice on one path (two X-Test-Id headers, or ?testId= twice) is rejected with HTTP 400 and code MCP_DUPLICATE_IDENTITY. When the header and the query both carry a value, the header is used. The body of such a 400 is { "error": "<message>", "code": "MCP_DUPLICATE_IDENTITY" }, for example Duplicate ?testId= query parameter: 2 values sent, expected one. A bad undeclared override (see Closed world) gets the same shape with MCP_INVALID_UNDECLARED.

Encode test ids on the MCP client

Test ids often contain characters such as ›, which fetch and Node’s http.request refuse to put in a header. The MCP SDK transports use fetch. So send encodeURIComponent(id) in X-Test-Id and X-AIMock-Context, or in the ?testId= and ?context= query parameters, on the MCP client. aimock decodes both headers on MCP requests. A value that does not decode is used as sent, and the request is not rejected. An id that contains a literal %XX sequence must be sent as %25XX.

Encode on the MCP client only. LLM routes compare these headers as sent. If a shared request interceptor encodes them for LLM calls too, LLM context fixtures that contain a space stop matching.

Argument matching

Value Rule
Objects Same keys, each value equal. Key order does not matter. An extra or missing key is a mismatch.
Arrays Same length, equal element by element. Order matters.
Strings, booleans, null Strict equality. No type conversion: "1" is not 1, and null is not a missing key.
Numbers Equal as parsed JSON numbers (1 equals 1.0).
No arguments Treated as {}.
Arguments that are not an object Match only an anyArgs entry.

These rules follow the argument rules that Mastra documents for its tool mocks: object key order is ignored, array order matters, and values are not converted to other types. Fixtures written for one carry over to the other.

For each call, aimock takes the first entry that is not used yet and whose arguments match, and marks it used. Entries for the same tool and arguments are used in the order you declare them. Calls with different arguments each find their own entry, whatever the call order, because models make parallel tool calls in an order a test cannot control. Put args entries before anyArgs entries for the same tool; an args entry after an anyArgs entry gets a validation warning, because it cannot match until the anyArgs entry is used.

Any arguments, scoped by context. Every request that sends X-AIMock-Context: docs-search uses this block, whatever its test id. The first search_docs call gets Doc A: refunds take 5 days and the second gets Doc B: contact support, whatever the query text. A third call fails with MCP_FAKE_EXHAUSTED. A request without that context does not see the block, so with no registered search_docs tool it gets Unknown tool: search_docs.

docs/search.json json
{
  "mcpFakes": {
    "scope": { "context": "docs-search" },
    "tools": [
      {
        "name": "search_docs",
        "calls": [
          { "anyArgs": true, "result": [{ "type": "text", "text": "Doc A: refunds take 5 days" }] },
          { "anyArgs": true, "result": [{ "type": "text", "text": "Doc B: contact support" }] }
        ]
      }
    ]
  }
}

Precedence

For a tools/call of a tool:

  1. If the request’s blocks declare fakes for the tool, a fake answers. A mismatch or an exhausted tool returns an error; the call never falls through to a code handler.
  2. Otherwise, under deny, the call fails with MCP_FAKE_NOT_DECLARED, even if an onToolCall handler or a config result exists for the tool.
  3. Otherwise the tool is answered as it is without fakes.

tools/list returns the registered tools plus the tools named in the request’s blocks. A fake-only tool is listed with its description and inputSchema, or with { "type": "object" } as its schema. When a tool is both registered and faked, the registered definition is listed. Fakes never remove a registered tool from the list.

Errors

A fake failure means the test scenario is wrong, not that the tool failed. So aimock answers it with a JSON-RPC protocol error (over HTTP 200), which MCP client SDKs raise as an exception. If it were a tool result with isError: true, the model could read it, recover, and let the test pass by accident. Use an entry’s error key for a tool failure that the model is meant to see.

error.data.aimock.code JSON-RPC code When
MCP_FAKE_MISMATCH -32602 The tool is faked, but no entry matches the arguments.
MCP_FAKE_EXHAUSTED -31010 Entries match the arguments, but all of them are used.
MCP_FAKE_NOT_DECLARED -32602 The scenario denies undeclared tools and no block declares this tool. The message starts with Unknown tool:, as for an unknown tool without fakes.
MCP_FAKE_EVICTED -31011 The state of this test id was dropped by the per-mount test-id cap. See Parallel tests.
MCP_FAKE_INTERNAL_ERROR -32603 An aimock defect: a matching entry’s answer could not be built. The message starts with aimock MCP fake internal error:. Please report it.

Key your test code on error.data.aimock.code; the JSON-RPC code is shared with other errors. Every data.aimock has code, tool, testId, context (each null when not supplied) and mount. A mismatch adds received, declared (each entry’s id, args or anyArgs, and consumed) and firstDifference, the path to the first value that differs from the closest declared entry. An exhausted error adds received, matchingDeclared, matchingConsumed and matchingIds. A not-declared error adds declaredTools. An evicted error adds cap. An internal error adds error, the text of the error that aimock caught. The message names the tool, the arguments received and what was declared.

The bodies below are what aimock sends. id is the JSON-RPC id of the tools/call request, which the MCP client picks. A get_weather call with {"city":"seattle"} in the test weather › seattle (the weather/seattle.json example above):

Mismatch json
{
  "jsonrpc": "2.0",
  "id": 1,
  "error": {
    "code": -32602,
    "message": "aimock MCP fake mismatch: tool \"get_weather\" was called with arguments that match no declared fake (testId \"weather › seattle\"). Received {\"city\":\"seattle\"}. Declared: weather/seattle.json:get_weather#0 {\"city\":\"Seattle\"}.",
    "data": {
      "aimock": {
        "code": "MCP_FAKE_MISMATCH",
        "tool": "get_weather",
        "testId": "weather › seattle",
        "context": null,
        "mount": "/mcp",
        "received": {
          "city": "seattle"
        },
        "declared": [
          {
            "id": "weather/seattle.json:get_weather#0",
            "args": {
              "city": "Seattle"
            },
            "consumed": false
          }
        ],
        "firstDifference": "$.city: expected \"Seattle\", received \"seattle\""
      }
    }
  }
}

The third create_ticket call in the test tickets › retry on timeout (the tickets/retry.json example above):

Exhausted json
{
  "jsonrpc": "2.0",
  "id": 3,
  "error": {
    "code": -31010,
    "message": "aimock MCP fake exhausted: tool \"create_ticket\" with {\"title\":\"Refund\"} was called again, but all 2 matching fakes are already used (testId \"tickets › retry on timeout\").",
    "data": {
      "aimock": {
        "code": "MCP_FAKE_EXHAUSTED",
        "tool": "create_ticket",
        "testId": "tickets › retry on timeout",
        "context": null,
        "mount": "/mcp",
        "received": {
          "title": "Refund"
        },
        "matchingDeclared": 2,
        "matchingConsumed": 2,
        "matchingIds": ["tickets/retry.json:first-try", "tickets/retry.json:retry"]
      }
    }
  }
}

A delete_account call in the test account › read only (the account/read-only.json example in Closed world):

Not declared json
{
  "jsonrpc": "2.0",
  "id": 2,
  "error": {
    "code": -32602,
    "message": "Unknown tool: delete_account (aimock MCP fake not declared: this scenario denies undeclared tools; testId \"account › read only\").",
    "data": {
      "aimock": {
        "code": "MCP_FAKE_NOT_DECLARED",
        "tool": "delete_account",
        "testId": "account › read only",
        "context": null,
        "mount": "/mcp",
        "declaredTools": ["get_account"]
      }
    }
  }
}

MCP 2025-03-26, the version aimock advertises, lists invalid arguments as a protocol error. From MCP 2025-11-25, input validation errors are tool execution errors. aimock still answers a mismatch with a protocol error, on purpose, for the reason above.

Agents sometimes catch tool exceptions. So every fake failure is also logged at error level (a line starting with MCP-FAKE:), stored in the journal with response.mcpFake.outcome set to mismatch, exhausted, not_declared or evicted, and counted in the aimock_mcp_fake_failures_total{code} metric. A fake answer is stored with outcome answered and the entry id. MCP_FAKE_INTERNAL_ERROR is logged and counted the same way, but its journal entry has no response.mcpFake; the message is in response.error. To check that a test had no fake failures, read GET /__aimock/journal?testId=<id>&service=mcp and fail on any entry that has a response.mcpFake.outcome other than answered, or that has response.error. Entries for other methods, and calls that no fake answered, have no response.mcpFake. A standalone MCPMock (not mounted on an LLMock server) has no journal and no metrics, and logs only after setLogger() (see API for fakes), so the JSON-RPC error may be the only trace.

Closed world (undeclaredTools: "deny")

With "undeclaredTools": "deny" in a scoped block, a call to a tool that no block of the request declares fails with MCP_FAKE_NOT_DECLARED. Tools that any of the request’s blocks declare, shared ones included, are still answered by their fakes. If any scoped block of the request says deny, the request is under deny.

In the test account › read only, get_account with {"id":"u1"} answers {"plan":"pro"}. A call to any other tool, such as delete_account, fails with MCP_FAKE_NOT_DECLARED.

account/read-only.json json
{
  "mcpFakes": {
    "scope": { "testId": "account › read only" },
    "undeclaredTools": "deny",
    "tools": [
      {
        "name": "get_account",
        "calls": [{ "args": { "id": "u1" }, "result": "{\"plan\":\"pro\"}" }]
      }
    ]
  }
}

Parallel tests and consumption state

Which entries are used is tracked per mount and per test id. Requests that send only a context, or nothing, share one default state. Tests that run in parallel must each send their own test id, or they use up each other’s entries.

Each mount keeps the state of at most as many test ids as the fixture match-count cap (--fixture-counts-max, default 500; 0 means no cap). When the cap drops the oldest test id, every later fake call with that test id fails with MCP_FAKE_EVICTED until a reset, so used entries are never served again.

Reset

Call Effect on fakes
POST /__aimock/reset, DELETE /__aimock/fixtures, LLMock.reset(), LLMock.clearFixtures() Unload all fakes, with the LLM fixtures.
LLMock.resetMatchCounts(testId?) Keep the fakes; mark the entries of that test id (or of every test id) as unused. The Vitest and Jest plugins call it before each test.
MCPMock.reset() Unload this mount’s fakes, with its tools, resources, prompts and sessions.
POST /__aimock/reset/journal No effect on fakes.
--watch reload Fakes are not reloaded. A reload whose mcpFakes changed is rejected with an error on stderr, and the previous fixtures stay loaded. Restart the server to load changed fakes.

Session bindings survive every reset except MCPMock.reset().

Loading fakes

Both ways in code. loadFixtureFile() takes a file path, relative to the working directory. In this example, /mcp (mounted by aimock for the file’s block) answers get_weather in the test weather › seattle, and /billing answers refund with {"amount":10} in the test billing › refund with refunded 10.

programmatic/load-fakes.mjs javascript
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;
}

Fakes fail loud. A block that aimock cannot honor fails the whole load, in every mode, strict or not: the load call throws a FixtureLoadError, start() rejects, the CLI prints the error and exits non-zero, and the control API answers 400 and adds nothing. This covers a missing or malformed scope, a "shared" block with deny, a malformed entry, the same tool twice in one block, an entry id that is already used on the mount, an unknown key (for example a misspelled undeclaredTool), and a mount path held by a mount that is not an MCPMock. The error’s rule field (for example mcp-fakes/bad-block:a or mcp-fakes/mount-conflict) names the check, and its message names the file and block. aimock validate reports the same errors per file, except mount conflicts, which depend on the running server.

When one input has more than one problem, the error is a McpFakesAddError, a subclass of FixtureLoadError. Its errors array holds one FixtureLoadError per problem, and its warnings array holds the shadowed-entry warnings of the same input. Each FixtureLoadError has rule, file, blockId, entryId and message, and toJSON() returns those fields with name.

A load that succeeds can still warn. An args entry after an anyArgs entry for the same tool is a shadowed-entry warning: the CLI and LLMock log it, and mcp.loadFakes() returns it as { warnings: [{ file, blockId, entryId, message }] }. If you call llm.mount(path, mcp) after start at a path where aimock already mounted an MCPMock for fakes, the auto-mounted mock keeps those requests, and aimock logs a warning that the new mount is shadowed.

API for fakes

These MCPMock methods work on the fakes of one mount:

Method Effect
loadFakes(blocks) Adds one mcpFakes value (a block or an array of blocks), in the fixture file form. Adds all or nothing. Returns { warnings }; throws a McpFakesAddError on a bad block.
addMcpFakes(sources, origin) The lower-level add that the loaders use. sources is an array of { source, blockIndex, raw }, as the …WithServices loaders return them, and origin is { kind: "file" }, { kind: "code" } or { kind: "control-api" }. With "file", the entry ids use each source; with the other two, they use code#<n> or control-api#<n>.
fakesSnapshot(testId?, context?) The blocks on this mount that apply to that test id and context, with each entry’s id and consumed state for the test id: the blocks of one mount in GET /__aimock/mcp/fakes. Omit an argument, or pass null, for “not sent”. The result is frozen.
resetScenarioState(testId?) Marks the entries of that test id (or of every test id) as unused. The fakes stay loaded. LLMock.resetMatchCounts() calls it on every mount.
clearMcpFakes() Unloads every fake on this mount, with its consumption state. Tools, resources, prompts and sessions stay.
setLogger(logger) Sets the logger for MCP-FAKE: lines and for warnings about a test id or context that does not decode. LLMock calls it when it mounts the mock, so you need it only for a mock that you use without LLMock.

loadFixtureFileWithServices(path) and loadFixturesFromDirWithServices(dir) read files the way LLMock does and return { fixtures, mcpFakes, mcpFakeWarnings, unreadable }:

A bad block makes these loaders throw, after they have checked every file. The main entry @copilotkit/aimock exports the two loaders, FixtureLoadError, McpFakesAddError, MCP_FAKE_ERROR_CODES and the fake types (McpFakeBlock, McpFakeTool, McpFakeCall, McpFakeResult, McpFakeScope, McpFakeBlockSnapshot, McpFakeErrorCode and others). The @copilotkit/aimock/mcp subpath exports MCPMock, McpFakesAddError, MCP_FAKE_ERROR_CODES and the same types, but not the loaders. MCP_FAKE_ERROR_CODES maps each code to its JSON-RPC code: MCP_FAKE_NOT_DECLARED and MCP_FAKE_MISMATCH to -32602, MCP_FAKE_EXHAUSTED to -31010 and MCP_FAKE_EVICTED to -31011. MCP_FAKE_INTERNAL_ERROR is not in the map.

Inspecting fakes

GET /__aimock/mcp/fakes?testId=<id>&context=<ctx>&mount=<path> lists, per mount, the blocks that apply to that test id and context, with each entry’s id and whether it is used. Use it at the end of a test to check that every declared fake was used. See the Control API.

What aimock cannot fake: in-process tools

aimock sees only what crosses the network. A tool that runs inside your agent process and never makes an MCP call cannot be faked here. Only an in-process mock, such as Mastra’s tool mocks for Mastra agents, can fake those. MCP over stdio is not supported either; MCPMock serves HTTP only.

Known limitations

Resources

Register static resources that clients can read:

Resources API typescript
mcp.addResource(
  { uri: "file:///readme.md", name: "README", mimeType: "text/markdown" },
  { text: "# My Project\nWelcome!" },
);

Prompts

Register prompt templates with optional handlers:

Prompts API typescript
mcp.addPrompt(
  { name: "summarize", arguments: [{ name: "text", required: true }] },
  (args) => ({
    messages: [
      { role: "user", content: { type: "text", text: `Summarize: ${(args as { text: string }).text}` } },
    ],
  }),
);

Config File

MCPMock can be configured via the aimock JSON config file:

aimock.json json
{
  "mcp": {
    "path": "/mcp",
    "tools": [
      { "name": "search", "description": "Search", "result": "Found it" }
    ],
    "resources": [
      { "uri": "file:///data.json", "name": "Data", "text": "{\"key\": \"value\"}" }
    ]
  }
}

A config result is one fixed answer per tool for the whole server process. To script answers per test, with ordered answers and argument checks, use scenario fakes in fixture files.

Session Management

MCPMock implements full session management per the MCP Streamable HTTP spec. Each initialize request creates a new session, and the session ID is returned via the Mcp-Session-Id header. All subsequent requests must include this header.

Method Description
initialize Creates session, returns capabilities and session ID
tools/list Lists the registered tools, plus the tools named in the request’s fake blocks
tools/call Calls a tool by name with arguments
resources/list Lists all registered resources
resources/read Reads a resource by URI
prompts/list Lists all registered prompts
prompts/get Gets a prompt by name with arguments
ping Returns empty object (health check)
DELETE / Destroys a session
GET / Answers 405 with Allow: POST, DELETE; the mock offers no server-sent event stream

Inspection

Inspection API typescript
mcp.health();       // { status: "ok", tools: 2, resources: 1, prompts: 0, sessions: 1 }
mcp.getSessions();  // Map of active sessions
mcp.getRequests();  // Journal entries (when mounted with shared journal)
mcp.reset();        // Clears all tools, resources, prompts, sessions, and MCP fakes