Amplifier · tool-mcp module

Three silent failures. Fixed.

The MCP client now works on the SDK you actually have installed, speaks the protocol you think it does, and stops losing tools mid-list.

Merged · PR #11 Commit 22f3d14 2026-07-29
The pattern

None of them told you.

Three separate defects, three different layers of the client. The thing they had in common: not one of them produced a warning, an error, or a log line that pointed at the real problem.

Wrong SDK

A connect that raised no useful error

The real cause was buried two exception-group levels deep, or didn't error at all.

Wrong protocol

A handshake with no warning it was outdated

The client silently ran a year-old negotiation path with nothing to say so.

Wrong tool list

A truncation with no error at all

Tools past page one simply weren't there. No exception, no log.

Every one of these shipped and worked — right up until it silently didn't.
Act 1 — the SDK moved

mcp 2.0.0 shipped the same day as the new spec. Our pin had no ceiling.

pyproject.toml declared mcp>=1.0.0 — unbounded. SDK 2.0.0 renamed its model fields camelCase → snake_case (Tool.inputSchemaTool.input_schema). A fresh install pulled 2.0.0. Every MCP server: dead on connect.

# what the user saw RuntimeError: MCP server connection failed: unhandled errors in a TaskGroup (1 sub-exception) # the real cause, two ExceptionGroup levels down AttributeError: 'Tool' object has no attribute 'inputSchema'
The quiet sibling

No error at all

resource.mimeType if hasattr(resource, "mimeType") else Nonehasattr evaluated False on the renamed 2.x field and silently returned None. Real data, dropped, zero error.

The fix

Reads either spelling, fails loud on the rest

Bridges both field names. Floor corrected to mcp>=1.24 (1.23 and earlier fail at import for an unrelated symbol rename). Truly unknown SDK shapes now raise a self-describing error.

Act 2 — the wrong protocol

session.initialize() drives the SDK down its legacy path

Found by tapping the stdio pipe and reading raw JSON-RPC — not by reading documentation.

Before

initialize()

Handshake-era negotiation. Protocol 2025-11-25. No _meta field on any request — a concept that protocol doesn't have.

After

negotiate_auto()

Stateless negotiation via server/discover. No handshake. Full _meta on every request. Protocol 2026-07-28.

Falls back to the legacy path automatically when the installed SDK is 1.x — and logs it at WARNING, not silently.

Act 2 — the evidence

Same client code. Same tool call. Two different wires.

Before · mcp 1.29.0
real stdio capture, trimmed
C→S {"method":"initialize","params": {"protocolVersion":"2025-11-25", ...}} S→C {"result":{"protocolVersion": "2025-11-25", ...}} C→S {"method":"notifications/initialized"} C→S {"method":"tools/list","id":1} ^ no _meta anywhere in the exchange
After · mcp 2.0.0
real stdio capture, trimmed
C→S {"method":"server/discover","params": {"_meta":{ "io.modelcontextprotocol/protocolVersion": "2026-07-28", "io.modelcontextprotocol/clientCapabilities":{} }}} S→C {"result":{"resultType":"complete", "supportedVersions":["2026-07-28"], ...}} C→S {"method":"tools/list","params": {"_meta":{ ...same 3 keys... }}} ^ _meta on every request, no handshake
Act 3 — the tool list was shrinking

nextCursor was ignored. Multi-page tool lists were silently cut short.

A live bug in the protocol the module was already running — independent of the 2026-07-28 rewrite. Any MCP server returning more than one page of tools lost everything past page one. No error, no warning, no signal at all.

# verified against a real multi-page stdio server cursor chain followed: None → '' → 'c2' → None # the empty-string cursor is the trap: # spec says it is a valid cursor and MUST NOT be # read as end-of-results — a naive falsy check gets this wrong

The fix follows the full cursor chain, including that empty-string case, before treating discovery as complete.

Act 4 — caught by review, not by tests

The pagination fix reintroduced the pagination bug.

Before peer review, the PR went through independent cold review — two reviewers with zero prior context, plus a six-lens review council. They converged on two real blockers, both in the newly-added code.

Blocker 1

default=None

pagination.py read the cursor with a default. An unrecognized page shape was treated as terminal — silently returning a partial list. Twelve lines below the docstring that forbade exactly this pattern.

Blocker 2

bare except Exception

discovery.py swallowed the deliberate RuntimeError safety nets the pagination fix had just added. A misbehaving server returned [] with zero signal.

We wrote the rule. Then broke it. An outside reader caught it — not the tests. Both fixed, with regression tests confirmed to fail against the pre-fix code.
Act 5 — proof, not assertion

Every claim above was executed.

CheckHow
Both wire formatsRaw stdio JSON-RPC capture on mcp 1.29.0 and 2.0.0
PaginationReal multi-page stdio server, full cursor chain confirmed
HTTP headersStreamable HTTP capture behind a logging proxy — all required headers present on every request
As a real moduleMounted via normal bundle + mcp.json in a Digital Twin — not just unit tests. Tools appeared in the live tool list; an agent called one end-to-end, negotiating 2026-07-28 against SDK 2.0.0
RegressionTests: 57 → 99, zero skips, green on both mcp 1.29.0 and 2.0.0
What to do

Update the module. Watch the logs once.

Update

Pull the latest amplifier-module-tool-mcp and connect any MCP server as usual.

Check the log line once

A WARNING about legacy negotiation means your installed mcp SDK is still 1.x. The module still works — automatic fallback — but you're not on the current protocol.

No warning printed → you're negotiating 2026-07-28.

Honest scope

This is not full conformance. The PR says so.

Still unimplemented

x-mcp-header mirroring (a client MUST)
subscriptions/listenlistChanged is never observed
MRTR / elicitation
Cache hints (ttlMs / cacheScope)

Deliberately excluded

Roots — deprecated in 2026-07-28
Sampling — deprecated in 2026-07-28
HTTP+SSE transport — deprecated, Streamable HTTP only

Sources & links

Everything here traces back to something you can read or run.

Data as of 2026-07-29. Sourced from the original evidence-based gap analysis (2026-07-28 spec vs. module HEAD b32b7ce), the current per-obligation conformance table in docs/GAP_ANALYSIS.md, real stdio and Streamable HTTP wire captures, and a Digital Twin Universe run mounting the module as a live Amplifier tool.

More Amplifier Stories