cd /news/developer-tools/pin-your-mcp-server-contracts-the-wa… · home topics developer-tools article
[ARTICLE · art-67814] src=dev.to ↗ pub= topic=developer-tools verified=true sentiment=· neutral

Pin your MCP server contracts the way you pin your dependencies

A developer built mcpward, a CI gate that pins the contract of MCP (Model Context Protocol) servers the way npm lockfiles pin dependencies. The tool snapshots tool descriptions, schemas, and annotations into a lockfile, then fails the build on breaking changes like description rewrites, required parameter additions, or readOnlyHint flips. Invariant Labs previously identified tool poisoning and MCP rug pulls with their mcp-scan tool.

read5 min views2 publishedJul 21, 2026

You pin your npm dependencies. You have a lockfile. You review the diff when it changes.

Now consider the MCP servers your agent depends on. What pins those?

tools/list

hands back names, descriptions, and JSON schemas, and your agent trusts all of it. The description isn't documentation — it's the instruction the model reads to decide what a tool does and when to call it. There's no version pin, no integrity check, no diff to review. The server changes, and your agent's behaviour changes with it.

1. A description is rewritten. Same tool name, same schema, different text. The model now behaves differently and nothing registers a change. This is the one that matters most, because it requires no schema change at all — which is exactly why schema-only diffing misses it.

2. A required parameter appears. Your existing calls start failing with invalid params, and you find out from production traffic.

3. readOnlyHint flips from true to false. A tool you allow-listed as safe to call freely can now mutate state.

4. A tool disappears. Best case, a clean error. Worst case, your agent improvises with something else.

Invariant Labs did the foundational work here. They named tool poisoning and MCP rug pulls, and their mcp-scan has detected description changes via tool hashing since April 2025. If you want to audit the MCP servers installed on your own machine, that's the tool — it scans Claude, Cursor, and Windsurf configs, detects cross-origin escalation (tool shadowing), and offers a proxy mode with live guardrails. None of which I do.

I needed something adjacent: a CI gate. Not "is my laptop safe," but "did this dependency's contract change since my last release." And it had to run against internal servers without sending tool descriptions to anyone's API.

So I built mcpward.

npx mcpward baseline   # snapshot the server's contract into a lockfile
npx mcpward diff       # in CI: fail the build if it drifted

The baseline captures every tool's name, description hash, input and output schemas, and annotations. Then, when the server ships an update:

DRIFT (5 failed)
  ✗ Tool "echo" description changed (possible rug-pull)
  ✗ Tool "compute" inputSchema added required property "multiplier"
  ✗ Tool "read_data" readOnlyHint changed from true to false (tool may now mutate state)
  ✗ Tool "removed_tool" was removed

Summary: 2 passed | 5 failed

Exit code 1

. The build fails. Four contract changes, none of which would have surfaced at runtime until something broke.

Not every change should fail a build. Adding an optional parameter is fine. Adding a required one is not. The classification:

Change Class Fails by default
Tool removed breaking yes
Description changed rug-pull yes
Required field added, type narrowed, enum tightened breaking yes
Optional field added, type widened, constraint loosened non-breaking no
readOnlyHint true→false, destructiveHint false→true
annotation change yes
Tool added info no

Getting that line right is the hard part of the whole tool. It's a pure function with an exhaustive fixture-backed test suite behind it, and it's the part I'd most like to be told I've got wrong.

Since it's already speaking the protocol as a real client:

Protocol compliance — handshake, version negotiation, capability consistency, JSON-RPC correctness.

The two-layer error contract. This one is underrated. MCP distinguishes protocol errors (a JSON-RPC error

object) from tool errors (a successful result carrying isError: true

). A tool that fails its job — file not found, upstream 500 — should return the second, not the first. Servers get this backwards routinely, and it changes how every client must handle the failure. Nothing else checks it.

Tool-poisoning heuristics — injection-like phrasing in descriptions, hidden and zero-width unicode, schemas soliciting API keys or passwords, readOnlyHint

that contradicts an obviously destructive tool. Output is SARIF, so findings land in the GitHub Security tab.

Behavioral suites — declarative YAML cases with JSONPath assertions:

suites:
  - tool: read_file
    cases:
      - name: missing file is a tool error, not a protocol error
        args: { path: "/does-not-exist" }
        expect: { tool_is_error: true }
      - name: invalid params are a protocol error
        args: {}
        expect: { protocol_error_code: -32602 }

Latency budgets — p50/p95 per tool against a configurable threshold.

A test harness nobody can trust is worse than none, so the testing approach is deliberately paranoid.

Every check is developed against controlled fixture servers whose correctness is defined up front: a fully compliant one, a deliberately malformed one, a pair differing by exactly one change of each classification class, a slow one, and a poisoned one. Real third-party servers can't serve as ground truth, because you don't control whether they're correct.

And every check has a negative test proving it can go red. A check that only ever passes is a false-confidence generator, not a feature. The clean fixture also has to stay 100% clean through every release — a security check that cries wolf trains people to ignore it, which is worse than not shipping it.

npx mcpward init
npx mcpward run

Black-box, so it works against servers you didn't write, over stdio or Streamable HTTP, in any implementation language. MIT licensed. Runs entirely on your machine: no account, no API calls, no telemetry.

https://github.com/TsvetanG2/mcpward

If you've been bitten by description drift in practice — or if you think I've drawn the breaking/non-breaking line in the wrong place — I'd like to hear it.

── more in #developer-tools 4 stories · sorted by recency
── more on @invariant labs 3 stories trending now
sponsored brought to you by zahid.host 4,200+ EU-deployed projects
reading about agents? ship yours in a single git push.

Run your AI side-project on zahid.host

EU-based hosting, git-push deploys, automatic HTTPS, no cold starts. Free tier with a custom domain — perfect for shipping the agent you just read about.

$git push zahid main
Live at https://your-agent.zahid.host
Get free account → Pricing
from €0/mo · no card required
LIVE [news/pin-your-mcp-server-…] indexed:0 read:5min 2026-07-21 ·