Skip to main content
Glama
README.md
# mcpstub

**A fake MCP server generated from the real contract.**

`mcpstub` turns an `mcpdoctor` report or a small JSON fixture into a deterministic stdio server. Test MCP clients, gateways, and agent harnesses without live credentials, flaky APIs, or hand-written JSON-RPC plumbing.

![mcpstub turns a contract into a deterministic modern or legacy test server](demo.svg)

```console
$ mcpstub from mcpdoctor.json --out fixtures/docs.json
mcpstub: wrote fixtures/docs.json (14 tools)

$ mcpstub check fixtures/docs.json
✓ valid mcpstub fixture: fixtures/docs.json

$ mcpstub serve fixtures/docs.json
# speaks MCP on stdin/stdout until the client disconnects
```

## The useful loop

Inspect a real server once, review the contract, then test against the local stub:

```bash
# 1. Inventory the server. mcpdoctor never calls its tools.
npx --yes github:jovial-liu/mcpdoctor#v1 inspect \
  --format json --out mcpdoctor.json -- \
  npx -y @modelcontextprotocol/server-filesystem /tmp

# 2. Generate a safe starting fixture from the advertised contract.
npx --yes github:jovial-liu/mcpstub#v1 from mcpdoctor.json --out fixtures/files.json

# 3. Point an MCP client at the deterministic fixture server.
npx --yes github:jovial-liu/mcpstub#v1 serve fixtures/files.json
```

The generated fixture preserves tool names, descriptions, schemas, annotations, resources, templates, prompts, and server identity. It generates placeholder results—never captured production tool output.

## Hand-authored scenarios

Fixtures are ordinary reviewable JSON. Add argument-specific cases directly to a tool:

```json
{
  "schema": "mcpstub/v1",
  "server": { "name": "weather-fixture", "version": "1.0.0" },
  "tools": [
    {
      "name": "get_weather",
      "description": "Get a fixture forecast",
      "inputSchema": {
        "type": "object",
        "properties": { "city": { "type": "string" } },
        "required": ["city"]
      },
      "cases": [
        {
          "when": { "city": "Paris" },
          "result": { "content": [{ "type": "text", "text": "Paris: 21°C, clear" }] }
        },
        {
          "when": { "city": "Atlantis" },
          "error": { "code": -32004, "message": "Fixture city not found" }
        }
      ],
      "result": { "content": [{ "type": "text", "text": "Fixture forecast unavailable" }] }
    }
  ]
}
```

`when` performs recursive subset matching on argument objects. Object cases can ignore unrelated arguments; arrays match by length and position. The first matching case wins, then `result` is the fallback. Without either, `mcpstub` returns a deterministic generated result containing the tool name and arguments.

Try the maintained example:

```bash
npx --yes github:jovial-liu/mcpstub#v1 check examples/weather.json
npx --yes github:jovial-liu/mcpstub#v1 serve examples/weather.json
```

## What it implements

- modern `2026-07-28` `server/discover`, result discriminators, and cache metadata;
- legacy `initialize` negotiation, defaulting to `2025-11-25`;
- `tools/list` and deterministic `tools/call` results or JSON-RPC errors;
- resource and resource-template listing plus `resources/read`;
- prompt listing plus `prompts/get`;
- `ping`, unknown-method errors, and parse errors;
- optional JSONL recording of calls made **to the stub**.

Both protocol eras are available from the same fixture process. A modern client starts with `server/discover`; a legacy client starts with `initialize`.

## Optional call assertions

Record what the system under test asked the stub to do:

```bash
mcpstub serve fixture.json --log calls.jsonl
```

The log contains timestamps, tool names, and arguments. It is opt-in because arguments may contain secrets or private data. Keep call logs out of source control unless reviewed and sanitized.

## CLI

```text
mcpstub from <mcpdoctor.json> [--out mcpstub.json]
mcpstub check <fixture.json> [--json]
mcpstub serve <fixture.json> [--log calls.jsonl]
mcpstub --version
```

The fixture format is versioned by [`schema/fixture.schema.json`](schema/fixture.schema.json). `check` also rejects duplicate names and malformed cases with stable finding codes.

## Safety and limits

`mcpstub` does not start a real MCP server, call a real tool, invoke a shell, load environment credentials, or make network requests. It reads only the fixture you name and writes only an explicit output or log path.

It is a protocol fixture, not a security sandbox or complete server emulator:

- stdio is supported; Streamable HTTP and authorization are not;
- multi-round-trip requests, subscriptions, sampling, elicitation, and extensions are not simulated;
- advertised JSON Schemas are not used to validate call arguments;
- generated fixtures contain contracts and placeholder responses, not behavioral recordings;
- a malicious fixture can still return malicious text to the client under test, so review third-party fixtures as code-like test data.

For recorded live sessions, use a cassette-style recorder. `mcpstub` is intentionally contract-driven: small fixtures, explicit cases, deterministic results.

## Development

```bash
git clone https://github.com/jovial-liu/mcpstub.git
cd mcpstub
npm run check
```

Node.js 20+ and zero runtime dependencies. See [CONTRIBUTING.md](CONTRIBUTING.md) and [SECURITY.md](SECURITY.md).

## License

MIT