Skip to main content
Glama
roanbrasil

contract-first-mcp

by roanbrasil
README.md
# contract-first-mcp

A reference MCP server that treats tool schemas as **data contracts** — versioned,
tested for backward compatibility in CI, and hash-pinned against rug pulls.

Built for the MCP `2026-07-28` era: stateless core, explicit handles instead of
protocol sessions, and a formal deprecation discipline borrowed from
event-driven-architecture schema governance.

## Why

MCP servers today evolve their tools the way early Kafka producers evolved their
events: silently. A tool renames a field, a client's prompt cache goes stale, an
agent starts failing mid-plan — and there is no registry, no compat check, no
changelog. This repo shows the minimum viable governance layer:

| Discipline | Where |
|---|---|
| Contract-as-repository: one JSON Schema file per tool version | `contracts/<tool>/vN.json` |
| Additive-only evolution, enforced — including the tightenings that remove nothing and still break clients | `registry.check_backward_compatible`, `tests/` |
| Rug-pull detection: canonical SHA-256 of every contract, pinned | `pinned_tools.lock.json`, `list_contracts` tool |
| Stateless state: HMAC-signed handles the model threads between tools | `create_workspace_handle` / `verify_workspace_handle` |
| Fail-fast boot: server refuses to start if a version chain breaks compatibility | `server.main()` |
| Human-readable evolution history | `TOOL_CHANGELOG.md` |

## Run

```bash
pip install "mcp>=2.0" jsonschema
python -m contract_first_mcp.server            # stdio
```

Claude Desktop / any MCP client config:

```json
{ "mcpServers": { "contract-first": {
    "command": "python", "args": ["-m", "contract_first_mcp.server"],
    "env": { "PYTHONPATH": "src", "HANDLE_KEY": "<inject-from-secret-manager>" } } } }
```

## What "additive-only" actually means here

The obvious rules — no removals, no type changes, no new required fields, no
enum removals — miss a whole family of breaking changes, because they look for
things that disappear. Lowering `maximum` from 12 to 6 removes nothing, renames
nothing, and rejects a payload that worked yesterday. So the checker also fails
a version that:

* raises a lower bound or lowers an upper bound (`minimum`, `maxLength`,
  `maxItems`, and the rest), **including introducing a bound where there was
  none** — unbounded is the loosest bound there is;
* adds or changes `pattern` or `multipleOf`;
* introduces an `enum` on a field that was previously open;
* closes `additionalProperties`;
* narrows a type (widening `string` to `["string", "number"]` stays legal);
* does any of the above inside a nested object or an array's items.

The asymmetry is the whole rule: dropping a constraint is always safe, adding
one never is. Each case above has a test that first proves the payload really
stops validating, then demands the checker catch it — because a compatibility
checker nobody adversarially tested is a comment, not a gate.

## Evolving a tool

1. Copy `contracts/<tool>/vN.json` to `v(N+1).json` and make **additive** changes only
   (new optional fields, new enum values).
2. `pytest` — the compat chain test tells you immediately if the change is breaking.
3. Regenerate the lockfile:
   ```bash
   python -c "import sys; sys.path.insert(0,'src'); from contract_first_mcp import registry; \
   import json,pathlib; pathlib.Path('pinned_tools.lock.json').write_text(\
   json.dumps(registry.build_lock(registry.load_contracts()), indent=2)+'\n')"
   ```
4. Record the change in `TOOL_CHANGELOG.md`.
5. Breaking change needed? Don't mutate — publish a **new tool name**
   (`convert_units_v2` as a separate tool), deprecate the old one in its
   description, and remove it no earlier than the spec's own bar: 12 months.

## Security posture

See [SECURITY.md](SECURITY.md). Short version: every input is validated against
its schema before touching logic, handles are signed and verifiable, contract
hashes are auditable by the client, and nothing in a tool result should ever be
treated as instructions.

## License

MIT

TDQS

A3.8/5.0

Scored across 4 tools

Disambiguation5/5

Each tool has a clearly distinct role: contract introspection, unit conversion, handle creation, and handle verification. Even the two handle tools are complementary rather than overlapping, with create vs verify being unambiguous.

Naming Consistency5/5

All tool names follow a consistent verb_noun snake_case pattern: list_contracts, convert_units, create_workspace_handle, verify_workspace_handle. This makes the set predictable and easy for an agent to navigate.

Tool Count5/5

Four tools is a well-scoped size for a focused server: one meta/introspection tool, one utility tool, and a small handle lifecycle pair. No tool feels redundant or out of place.

Completeness4/5

The main gap is that workspace handles can be created and verified but not explicitly revoked or expired. For the stated contract-first scope, this is a minor workaround rather than a critical missing capability.

Maintenance

ActivityMaintained
ResponsivenessNo issues