Project overview · local teaching tool

Anthropic MCP Lab

An MCP server you can watch — every request in, every response out, over either transport, with the protocol visible on the wire.

Status
Complete5 of 5 phases
Transports
2stdio, HTTP
Primitives
3tools, resources, prompts
Port
3100beside the chat lab

What it is

The premise

A Model Context Protocol server with a live inspector beside it. The inspector shows every MCP message in both directions — the actual envelope, not a summary — along with which transport carried it and how long the handler took.

It is the companion to the Anthropic Chat Lab, and the two are built to run at the same time. The chat lab shows you the Anthropic API boundary. This shows you the MCP boundary — a second, independent protocol with its own handshake, its own envelope and its own idea of what a tool is.

Run both and the full picture is on screen at once: Claude asks the chat lab for a tool, the chat lab asks this server, this server answers, and the answer travels back. Two logs, two protocols, one request.

The distinction the lab exists to make

This server never calls Anthropic and holds no API key. An MCP server does not talk to a model — it exposes capabilities and waits to be called. The model sits on the far side of the client. "Where does the key live?" is the question that tells you which component talks to the model, and in an MCP setup the answer is never the MCP server.

Tech stack

Node · Express · vanilla TypeScript · no frontend framework

Runtime dependencies

PackageVersionRole
@modelcontextprotocol/server2.4.0McpServer, registerTool, and the stdio and HTTP serving entry points
@modelcontextprotocol/client2.4.0The client half, used by the two try- scripts to call this server over both transports
@modelcontextprotocol/node2.1.2toNodeHandler — adapts the web-standard MCP handler onto Express
express5.3.0Hosts the MCP endpoint, the inspector and its event stream
zod4.6.7Tool input schemas. MCP takes Zod objects directly
dotenv18.0.10Loads an optional .env at startup — port only, no secrets

Build and dev toolchain

PackageVersionRole
typescript7.0.2Two configs — one per half of the app
tsx4.23.15Runs TypeScript directly, restarts on save. The normal way to use the app
@types/node26.6.6Server-side type definitions
@types/express5.0.6Route and middleware types

Platform

Node.js20 or newer required; developed on 24.4.1
Module system"type": "module" — ESM throughout, which means .js extensions on imports of .ts files
FrontendHand-written TypeScript compiled to ES modules. No React, no bundler, no CSS framework
ModelsNone. This process never calls one

The package split that catches people out

MCP's TypeScript SDK moved from a single @modelcontextprotocol/sdk package to separate /server and /client packages at 2.x. Most tutorials still show the 1.x shape, and several of its APIs no longer exist. The notes in this project's files record the differences where they bite; the short version is that HTTP serving is now createMcpHandler() returning a web-standard handler, not a transport object you wire up by hand.

Why no framework

The page is the teaching surface. A framework would put its own abstraction between the reader and the JSON on the wire, which is the one thing the app exists to show. That reasoning carries over from the chat lab unchanged — and matters more here, because the MCP envelope is the subject rather than the plumbing.

What's in it

Built and working, except where noted

Three MCP primitives

PrimitiveWhat it isWhy it is in the lab
ToolsFunctions a model can callThe familiar one — and the one that looks most like what the chat lab already does
ResourcesReadable data, addressed by URIHas no Anthropic-API equivalent. Closer to a file read than a tool call
PromptsParameterised templates the server offersThe server suggesting how it should be used, rather than waiting to be told

Four tools

ToolWhat it teaches
echoThe round trip, with nothing else in the way
roll_diceNon-determinism — proof the server really ran, rather than the model guessing
inspect_requestReturns the MCP envelope it was called with. The protocol looking at itself
slow_taskProgress notifications: a server speaking before it has an answer

Three resources, two prompts

AddressWhat it shows
lab://server-infoGenerated at read time, not stored — read it twice and the uptime has moved
lab://notes/protocolBacked by a file. The client names an address, never a path, so there is no traversal to defend against
lab://dice/{sides}A template. The parameter arrives as an unvalidated string, which is where the handler earns its keep

The two prompts cover the primitive's whole range: try_the_tools returns a single message, and explain_exchange returns three — user, assistant, user — where the middle turn is a worked example the server wrote. That fabricated turn is why prompts return messages rather than text, and it is the same mechanism as prompt injection, differing only in who is doing it.

The inspector

A two-pane page at localhost:3100: the traffic log on the left, what the server exposes on the right. Every exchange appears as it happens — the request arriving, each handler running and returning with its timing, the response going back.

Rows carry a transport tag, which the chat lab's activity log has no equivalent of. With a stdio client and an HTTP client both connected, filtering by transport leaves one conversation alone on screen. The filters run in the browser and never reach the server, so unticking a level hides rows rather than stopping them being recorded — tick it again and the history is still there.

The surface panel shows the same JSON Schema a client receives from tools/list, produced by the same conversion, so it cannot flatter the server by describing something other than what is advertised.

Two transports, one protocol

The same server is served two ways. stdio is how Claude Desktop launches a local server — a child process talking over its own standard input and output. Streamable HTTP is how a remote server works, and is what the chat lab will connect to.

Both work now. npm run try-stdio and npm run try-http run the same function over each — one shared session.ts, called with two different clients, so any difference in output is a difference in the transport rather than in the script. Run both and diff them: after normalising the dice rolls and the uptime, the output is identical line for line.

Out of scope, deliberately

Sampling and elicitation — where the server calls back into the client — are not planned. They need client-side support that would complicate the chat lab integration substantially. Their absence is a decision, not an oversight.

Using both labs together

The configuration the two projects are built for

Each lab is useful alone. Together they show the whole path a tool call travels, with both boundaries on screen at once.

Start them side by side

WhereCommandOpens
anthropic-mcp-labnpm run devlocalhost:3100 — the inspector
anthropic-chat-labnpm run devlocalhost:3000 — the chat window

In the chat lab, open Settings → Tools, scroll to Connect an MCP server, and connect to http://localhost:3100/mcp. Its four tools appear in the Tools tab badged mcp, ticked and sent exactly like the local ones.

What to watch

Ask the chat lab to roll some dice, with both activity logs visible. The same exchange appears twice, from two sides:

In the chat labIn this inspector
A request to Anthropic carrying the tool definitionsnothing — this server is not involved yet
stop_reason: "tool_use" asking for roll_dicenothing — the model has only asked
A tool row tagged mcpPOST /mcp arriving, the handler running, the response leaving
A second request carrying the tool_resultnothing — this server's part is over
The thing worth noticing

Claude is never told where a tool runs. It receives one tools array and answers with one tool_use block; the chat lab's own tools, Anthropic's web_search, and these MCP tools are indistinguishable in the request. Which machine executed the function is the chat lab's problem alone — and watching both logs is what makes that concrete rather than a claim.

What the integration cost

The chat lab's tool loop was not changed to accommodate MCP. A remote tool is adapted into the same shape a local one has and goes in the same list. Three things made that possible, and they are the interesting part of the diff: the tool schema type was widened to name only the two operations the app actually performs on a schema; the JSON Schema from the remote server is carried verbatim rather than converted to Zod; and a remote tool whose name is already taken is dropped rather than allowed to shadow a local one.

There is deliberately no JSON-Schema-to-Zod converter. JSON Schema is the larger language, and a converter that handles most of it fails silently — as a tool that mysteriously rejects valid arguments.

How it is being built

5 phases · phase 1 complete

Built in phases from a written plan, the same way the chat lab was. Each phase ends somewhere runnable: phase 2 finishes with a server the MCP Inspector can list tools from, before any of the UI exists.

  1. Skeleton — configs, scripts, conventions done
  2. The MCP server: tools, resources, prompts, stdio done
  3. HTTP serving and the inspector UI done
  4. Chat lab integration done
  5. Documentation and Pages done

What phase 1 put in place

The two TypeScript configs, the three CLI scripts, the wire contract in shared/types.ts, and the conventions the rest of the build is held to. npm run audit-comments went in first, before any source file, so the documentation rule is enforced from the beginning rather than retrofitted at the end.

Gotchas already recorded

Conventions held through the build

What the code enforces on itself

Every file explains itself

Each file opens with a comment saying what it does and why it exists; npm run audit-comments checks none were missed, and fails the check if a header is missing, does not name its file, or is short enough to be a label rather than an explanation.

This project's copy of that script fixes two things about the chat lab's. It audits shared/, which the original never looked at — leaving the wire contract as the one file exempt from the rule. And it drops a stray fragment from a regex that quietly ate two characters from any comment line beginning * ex…. Both fixes are documented in the script's own header, because an unexplained difference between two near-identical files is worse than either version.

Dependency order as a rule

Tools follow three levels: types.ts imports nothing, definitions/ imports types, registry.ts imports the definitions. The chat lab learned this the hard way — an earlier version put the interface in the registry, creating an import cycle that worked with one tool and threw Cannot access 'currentTime' before initialization with two. Same layout here, for the same reason.

Security by construction

Current state

Verified 13 November 2026

What this is for

All five phases are complete and every figure above is measured. The server runs over both transports, the inspector shows each exchange live, and the chat lab borrows these tools over MCP. What it is not is production software — it has no authentication, binds to localhost, and executes tools for anyone who can reach it. It is built to be read and watched, not deployed.