MCP Lab: Give Claude a Tool of Your Own
The Model Context Protocol lets Claude Code call tools that live outside it: a database, a ticket system, or, here, a 68-line Node script that reports which bookshelf features are passing. This lab extends the server from Chapter 12 with a second tool, tests it over the wire without Claude, registers it with claude mcp add, and then watches Claude use it, and be refused it, in real sessions.
haiku model on a copy of the bookshelf project; the server needs Node.js (v24 here). Chapter 12 wrote the first version of this server.A Plug Between Claude and Everything Else
Anthropic describes the Model Context Protocol as an open standard for AI-tool integrations, and suggests connecting a server when you catch yourself copying data into the chat from another tool (Source 1). Claude Code is the client. A server advertises a list of tools, each with a name, a description and an input schema, and Claude calls them with JSON.
Where the server runs, and how Claude reaches it, is the transport:
| Transport | Status in the docs | Use |
|---|---|---|
http | Recommended | Remote servers |
stdio | Supported | A local process Claude starts, as in this lab |
sse | Deprecated | Remote servers that only offer SSE |
ws | Supported | Configured only through .mcp.json or add-json |
The Server, and Testing It Without Claude
The whole server is one file with no SDK: it reads a line from stdin, parses JSON-RPC, and answers on stdout. Version 0.2.0 adds list_features, which takes an optional status and, on bad input, returns a tool-level error instead of crashing.
Three design decisions worth copying:
- Read-only. Both tools only read a file. A first server should not be able to change anything.
- A schema that says what is valid.
statusis an enum ofpassingorfailing, and the server checks it again anyway, because a client's schema is advice. - Two kinds of error. A request the protocol cannot understand (an unknown tool) is a JSON-RPC
error. A request that is valid but wrong (a bad status) is a normal result withisError: true, so Claude can read the message and correct itself.
Test it before Claude ever sees it. This script pipes five messages into the server, the same ones a client sends: initialise, list tools, a good call, a call with a bad argument and a call to a tool that does not exist. No model is involved.
Row 1 is the handshake, row 2 lists both tools, row 3 returns the two failing features, row 4 is the tool-level error (isError: true) and row 5 is the protocol-level error (code -32602). Debugging a server this way is faster and free; only move to Claude when this passes.
claude mcp add, Scopes and .mcp.json
Register a local server with claude mcp add. Everything before the -- belongs to Claude's own options; everything after is the server command, passed through unchanged (Source 1):
The --scope flag decides where the entry is stored and who gets it:
| Scope | Loads in | Shared? | Stored in |
|---|---|---|---|
local (default) | This project, for you | No | ~/.claude.json |
project | This project | Yes, through git | .mcp.json at the project root |
user | All your projects | No | ~/.claude.json |
Project scope wrote this file, which you can commit. ${VAR} and ${VAR:-default} expansion works in command, args, env, url and headers:
Check it:
The status is Pending approval, and that is a security feature: a project-scoped server comes from a file in the repository, so in an interactive session Claude Code asks before it runs. claude mcp reset-project-choices clears earlier answers. The docs add a sharp edge: in non-interactive runs (claude -p and the Agent SDK) Claude Code cannot show the prompt and loads project servers without asking. I did not test that behaviour; the runs below pass the config explicitly with --strict-mcp-config, which does not exercise the approval flow.
Claude Uses the Tool, and Is Refused It
Tool names follow mcp__<server>__<tool>, and the same name is what permission rules, skills' allowed-tools, subagent tools and hook matchers use (Source 1). The first session allows exactly one of the two tools on the command line:
Real run MCP-01: prompt, every step, and the answer (Checked)
| # | Who | Event | Content (shortened) |
|---|---|---|---|
| 1 | Main session | ToolSearch call | {"query": "select:mcp__bookshelf-features__feature_status,mcp__bookshelf-features__list_features", "max_results": 2} |
| 2 | Main session | Tool result | [{"type":"tool_reference","tool_name":"mcp__bookshelf-features__feature_status"},{"type":"tool_reference","tool_name":"mcp__bookshelf-features__list_features"}] |
| 3 | Main session | mcp__bookshelf-features__list_features call | {"status": "failing"} |
| 4 | Main session | Tool result | [{"type":"text","text":"#6 pagination for list (failing)\n#7 delete book (failing)"}] |
| 5 | Main session | mcp__bookshelf-features__feature_status call | {} |
| 6 | Main session | Tool result | Permission to use mcp__bookshelf-features__feature_status has been denied because Claude Code is running in don't ask mode. IMPORTANT: You *may* attempt to accomplish this action using other tools tha |
| 7 | Main session | Text | Two bookshelf features are failing, according to the MCP server's `list_features` tool: - **#6: pagination for list** - **#7: delete book**… |
- Tool search came first. MCP tools are not all loaded up front; Claude searched for them by exact name with
ToolSearch, which ran although it was not on my allow list. Tool search is on by default in the documentation. - The answer is right.
list_featureswithstatus: failingreturned #6 and #7, which are the two failing entries infeature_list.json. I checked the file. - Permissions are per tool. Allowing
list_featuresdid not allow its siblingfeature_status: Claude tried it, was refused, said so, and did not try to get around it.
The second session allows no MCP tool at all:
Real run MCP-02: prompt, every step, and the answer (Checked)
| # | Who | Event | Content (shortened) |
|---|---|---|---|
| 1 | Main session | ToolSearch call | {"query": "select:mcp__bookshelf-features__feature_status,mcp__bookshelf-features__list_features", "max_results": 2} |
| 2 | Main session | Tool result | [{"type":"tool_reference","tool_name":"mcp__bookshelf-features__feature_status"},{"type":"tool_reference","tool_name":"mcp__bookshelf-features__list_features"}] |
| 3 | Main session | mcp__bookshelf-features__list_features call | {"status": "failing"} |
| 4 | Main session | Tool result | Permission to use mcp__bookshelf-features__list_features has been denied because Claude Code is running in don't ask mode. IMPORTANT: You *may* attempt to accomplish this action using other tools that |
| 5 | Main session | mcp__bookshelf-features__feature_status call | {} |
| 6 | Main session | Tool result | Permission to use mcp__bookshelf-features__feature_status has been denied because Claude Code is running in don't ask mode. IMPORTANT: You *may* attempt to accomplish this action using other tools tha |
| 7 | Main session | Text | I couldn't get the answer. Both bookshelf MCP tools were denied: `mcp__bookshelf-features__list_features` (filtered to failing) and `mcp__bo… |
Both calls were denied by the session's dontAsk mode, and Claude reported that instead of falling back to reading the file, because I had told it to use the tool. The documentation excerpt I read did not include the syntax for wildcard permission rules over a whole server, so I used exact tool names; check the permissions page before relying on a pattern.
Writing Servers You Can Trust
| Concern | What the docs say | What to do |
|---|---|---|
| Output size | Output above 10,000 tokens triggers a warning; the default limit is 25,000 (MAX_MCP_OUTPUT_TOKENS raises it) | Return summaries and filters, not whole tables |
| Slow tools | MCP_TIMEOUT for start-up, MCP_TOOL_TIMEOUT per call; calls over two minutes move to a background task | Keep tools fast or paginate |
| Untrusted servers | Verify you trust a server; ones that fetch outside content can expose you to prompt injection | Prefer your own or vetted servers; read what they do |
| Repository-supplied servers | Project servers need approval interactively; a cloned repo's blanket approval is ignored in an untrusted folder | Read .mcp.json in any repo you did not write |
| Credentials | Some credential variables read as empty in a remote server's URL and headers, so a project file cannot leak them | Pass secrets under names you choose |
| Databases | The page's own PostgreSQL example uses a read-only database user | Give a server the least privilege it needs |
Two connections to earlier chapters. A hook can guard MCP tools by matching their names, for example mcp__bookshelf-features__.*. And a plugin can bundle this server so that installing the plugin starts it for you; the tool names then gain a plugin prefix.
claude -p session cannot complete an OAuth flow, per the docs), MCP resources and prompts, running Claude Code itself as a server with claude mcp serve, or enterprise allowlists. The last part of the MCP page, about 54,700 of its 154,700 characters, was not in the text I read.Key Takeaways, Exercises and Sources
- A server is a process that answers JSON-RPC. You can test every message by hand before Claude is involved.
- Start read-only and validate arguments in the server, not only in the schema.
- Use both error levels: protocol errors for the impossible,
isError: truefor the correctable. - Scope decides sharing: project scope writes
.mcp.jsonand needs approval; user and local scopes stay on your machine. - Permissions are per tool, and the name is
mcp__server__tool.
Exercise 1 Beginner
Run mcp/test_protocol.sh against your own copy of feature_list.json. Change a status, run it again and watch the answer change. Then send the server a line of invalid JSON and read what it returns.
Learning outcome: speak the protocol by hand.
Exercise 2 Intermediate
Add a third tool, feature_detail, that takes a feature id and returns that feature. Validate the argument, return isError: true for an unknown id, extend the test script, and only then register and use it.
Learning outcome: extend a server test-first.
Exercise 3 Advanced
Write a PreToolUse hook with a matcher for your server's tools that logs every call's arguments to a file. Run a session, then compare the log with what the transcript shows Claude sent.
Learning outcome: observe and audit tool calls.
- Anthropic, Connect Claude Code to tools via MCP — transports,
claude mcpcommands, scopes and.mcp.json, environment expansion, project approval, tool naming,/mcp, output limits and timeouts, security notes. The first roughly 100,000 of 154,700 characters were read; resources, prompts,claude mcp serveand managed-MCP details were not.
Executed 2026-10-31: the protocol test (five messages, no Claude calls), claude mcp add, get and list, and two short sessions, MCP-01 and MCP-02. The list output is filtered to this lab's server. Not run: HTTP and OAuth servers, resources, prompts, claude mcp serve, wildcard permission rules. The server and scripts are in the examples repository under mcp/.