Skip to content

Connecting your first tool server

In this lesson we connect one tool server to a coding agent and read what happened. The server is the reference file-system server from the Model Context Protocol (MCP) project, scoped to a copy of a four-file directory. The task we give the agent is small on purpose: list the files and summarize each one. That task exists to produce a transcript, and the transcript is what we read.

The last lesson took the operator’s view of a remote server and asked what it may do and under whose name. This one is the first connection, on your machine, with a server that can’t reach anything you didn’t hand it. We run through the exchange between client and server in a stand-in first, so you know what to look for, and then you make the real connection in the exercise.

An MCP client talks to a local server over the stdio transport: it starts the server as a child process and exchanges one message per line over the process’s standard input and output, in JSON-RPC, a format for Remote Procedure Calls (RPC) in JSON. The server may log to standard error, and it may write nothing else to standard output [1]. Revisions up to 2025-11-25 open a session with an initialize handshake in which client and server agree on a protocol version and exchange their capabilities. Revision 2026-07-28 has no handshake: each request puts its protocol version and the client’s capabilities in a _meta field [1]. Claude Code asks only HTTP servers whether they support the newer revision, and asks a stdio server only when MCP_PROTOCOL_NEGOTIATION is set to auto, so the stdio connection in the exercise uses the older initialize handshake by default [2]. The stand-in below does neither. Almost all the traffic is two messages. The client sends tools/list once and gets back each tool’s name, description and inputSchema. When the model asks for a tool, the client sends tools/call with the tool’s name and its arguments, and the server answers with a content list and an isError flag [1].

The lesson ships a stand-in for that server, in about a hundred lines of Python, at site/examples/customizing-agents/mcp-first-server/stand_in.py. It speaks stdio the same way and offers three of the reference server’s tools. A path outside the directories named on its command line is refused. It can’t write anything. Next to it, client.py starts the stand-in and sends one request at a time, and handbook/ is the directory we scope it to: README.md, deploy.md, on-call.md and onboarding.md. Here is the client side of tools/list.

Example · run it

Run list_tools.py in the fixture directory, and compare with the output below.

server = client.start(client.HANDBOOK)
response = client.request(server, "tools/list")
for tool in response["result"]["tools"]:
print(tool["name"])
Output
list_allowed_directories
list_directory
read_text_file

Output verified in CI from site/examples/customizing-agents/mcp-first-server/list_tools.py.

The client passes those names and their descriptions to the model. Claude Code turns tool search on by default, and the model then looks a description up when it needs one instead of reading every description on every turn [2]. The list is still the whole of what the model can do with the server.

Now a call. The client sends tools/call with list_directory and the handbook’s path, and prints the text of the result and the error flag.

Checkpoint · predict

What does this print? client.call returns the result’s text and its isError flag.

server = client.start(client.HANDBOOK)
text, is_error = client.call(server, "list_directory", {"path": client.HANDBOOK})
print(text)
print(f"isError: {is_error}")

Output verified in CI from site/examples/customizing-agents/mcp-first-server/list_directory.py.

The [FILE] prefix is the reference server’s format, and the stand-in copies it [3]. A listing in a real transcript has this form when it comes back from the server.

The reference server has an allowed list of directories. The list comes from its command-line arguments, but a client that sends roots replaces that list with the roots [3]. Claude Code sends roots: the directory you started it in, plus every directory you added with --add-dir, /add-dir or the additionalDirectories setting [2]. So under Claude Code the command-line argument is a fallback the server never uses, and the scope is where you start the agent. A call whose path is outside the list fails. The stand-in does the same, and the refusal is a normal result with isError set, which is how the specification says a tool reports a failure the model should read [1].

MCP revision 2026-07-28 deprecates roots. They stay in the specification until at least the first revision released on or after 2027-07-28, and new servers should take their directories from tool parameters, resource URIs or their own configuration instead [4]. The specification also says that roots guide a server and that the protocol doesn’t make a server stay inside them [1]. So the refusal comes from the reference server’s own check, and the steps below give the server the same directory as its argument and as its root. The scope then stays the same whichever list the server uses.

Checkpoint · predict

The client asks for the parent of the handbook directory, and prints the result text up to its first colon, then the flag.

server = client.start(client.HANDBOOK)
parent = os.path.dirname(client.HANDBOOK)
text, is_error = client.call(server, "list_directory", {"path": parent})
print(text.split(":")[0])
print(f"isError: {is_error}")

Output verified in CI from site/examples/customizing-agents/mcp-first-server/outside_scope.py.

The scope is the one control you set before the server runs, and it holds when the model forgets an instruction. A model told “only look in the handbook” can forget. A server whose allowed list holds one directory has nothing else to list, whatever the model asks for.

Now connect the real server. It is an npm package, @modelcontextprotocol/server-filesystem, and its README shows it started with npx -y and the allowed directories as arguments [3]. It has thirteen tools, and four of them change things: write_file, edit_file, create_directory and move_file [3]. So the directory you give it is a directory it can rewrite, and the copy below is where it does that.

Copy the handbook somewhere disposable, change into the copy, and add the server from there. In Claude Code the command is claude mcp add, with -- between Claude’s own options and the command that runs the server [2]:

Terminal window
cp -r site/examples/customizing-agents/mcp-first-server/handbook /tmp/handbook
cd /tmp/handbook
claude mcp add handbook -- npx -y @modelcontextprotocol/server-filesystem@2026.8.31 /tmp/handbook
claude

The cd is the scope. Claude Code answers the server’s roots/list request with the directory it was started in [2], and the server drops its command-line list in favor of that answer [3]. Start claude from the repository root instead, and the server, write tools included, is scoped to the whole repository while the argument still says /tmp/handbook. Start it from inside the copy, so the argument and the root name the same directory, and let list_allowed_directories be the first call you read in every transcript.

npx -y downloads the package on first start and runs it. The previous lesson’s installation hygiene asks for a version-controlled dependency and a release a few days old instead, and that rule stands for anything real. A throwaway copy with no secrets in it is the one case where a fetch at start-up is acceptable, and the exact version in the command above still keeps the download from changing under you between runs.

The name handbook is yours, and Claude Code names the server’s tools mcp__handbook__<tool> in permission rules [5]. Without a --scope flag the server is added at local scope, which is you in this project. The project scope writes a .mcp.json the whole team’s clients read, and Claude Code asks you before it uses a project-scoped server in an interactive session [2]. claude mcp list shows what you have configured, and /mcp inside a session shows whether it connected [2].

The other agents that speak MCP take the same three facts in their own configuration: a name, a command with its arguments, and a scope. If yours is another product, look up its “add a server” page and give it the same command line.

Checkpoint · sort

You are setting per-tool approval for the handbook server. Place each tool where it belongs.

With the server connected, give the agent the test task. It is a task whose right answer you already know, so every tool call in the transcript can be checked against it. Ask for a list and a summary, and say that nothing is to change. The exchange below is illustrative: it shows the calls a good run makes, and your agent’s words differ.

Prompt (illustrative, not a recorded transcript)

Using the handbook server, list the files in /tmp/handbook and give me a one-line summary of each. Don’t change anything.

Response
list_allowed_directories()
-> /tmp/handbook
list_directory(path="/tmp/handbook")
-> [FILE] README.md
[FILE] deploy.md
[FILE] on-call.md
[FILE] onboarding.md
read_text_file(path="/tmp/handbook/README.md")
read_text_file(path="/tmp/handbook/deploy.md")
read_text_file(path="/tmp/handbook/on-call.md")
read_text_file(path="/tmp/handbook/onboarding.md")

Four files. README.md is the index. deploy.md gives the four deploy steps and the rollback. on-call.md sets the rota change and the fifteen-minute response time. onboarding.md plans the first two weeks for a new colleague.

Read it as you read the toy transcript in the concepts course. Each call shows the tool name with its arguments, and the result follows. Check the tool name of each call against the list you saw at tools/list, and check each path against the directory you scoped. Then look for a call from the write bucket. There should be none. Your client shows the same information in its own format, and a call you can’t see the arguments of is a call you can’t check, so find the setting that shows them before you trust the transcript. One difference from the stand-in: the real server lists a directory in the order the file system returns it, so your listing may not be sorted.

Now the mismatch. In a second run the learner asked one more thing.

Prompt (illustrative, not a recorded transcript)

Also check whether the parent folder has other handbooks.

Response
list_directory(path="/tmp")
-> Access denied - path outside allowed directories: /tmp not in /tmp/handbook

I can’t list /tmp. The handbook server only allows /tmp/handbook. If you want me to look there, add that directory to the server’s allowed list.

The model did what it was asked and called the right tool with a path outside the scope. The server refused, the model reported the refusal, and the transcript shows both, which is the scope doing its work. Had you started the agent in /tmp “to be safe”, the call would have listed your whole temporary directory. The write tools would have reached it too. The fix for a call like this is never to widen the scope in the moment. Ask what the task needs, and add that directory, and only that one.

Checkpoint · choice

The transcript shows list_directory(path="/tmp") answered with Access denied - path outside allowed directories. What happened?

Everything in this lesson ran over stdio, so the server was a child process of your agent. It ran under your user account and had no credential of its own. A remote server is reached over the Streamable HTTP transport, runs as its own process, and answers HTTP requests from many clients [1]. The Academy’s advanced MCP course covers both transports [6]. The consequence for credentials follows from the transport. A stdio server takes its credentials from the environment of the process that started it, which is yours. An HTTP server is where the specification’s authorization flow applies: the client obtains an OAuth access token for that one server, and the specification asks for short-lived tokens so a leaked one does little [1]. Claude Code adds a remote server with --transport http and a URL, and it supports OAuth for servers that ask for a login [2]. The previous lesson’s question about whose identity the agent acts under is a question about that token. The transcript reading you did here is the same for both. The arguments you check now travel over a network to code you didn’t start.

Checkpoint · scenario

A teammate added an issue-tracker server to the project’s .mcp.json. Your client has just asked you to approve it. What do you do first?

Exercise

Make the connection this lesson described, on a copy. Copy the handbook directory from the fixture to a place you can delete, change into the copy, add @modelcontextprotocol/server-filesystem with the copy as its only argument, and start your coding agent from inside the copy. The server has write tools, so the copy is the only directory it may ever see, and where you start the agent decides that under Claude Code. Ask for a list of the files and a one-line summary of each, and say that nothing is to change. Then read the transcript and write down, in a few lines, each tool the agent called and the path it passed.

A good result: the first call is list_allowed_directories and it answers with your copy alone, every later call is list_directory or read_text_file, every path is under your copy, and the copy is unchanged afterwards, which diff -r against the fixture confirms. When you’re done, remove the server with claude mcp remove handbook [2]. Then ask yourself: if the agent had called write_file, at which point would you have found out?

Stretch: Grant a second directory with /add-dir, ask the same question about both, and check that every path in the transcript is under one of the two.

Recap

  1. A local server runs over stdio as a child process of the agent. The client asks tools/list once and sends tools/call per use, and a refusal comes back as a result with isError set [1].
  2. Scope the server before it runs. The reference file-system server allows the roots its client sends, or its command-line directories when the client sends none [3], and Claude Code’s roots are the directory you started it in plus what you add with --add-dir [2]. Roots are deprecated in MCP revision 2026-07-28 [4]. Give the copy as the argument too, so a client that stops sending roots still leaves the server on the copy. Start the agent inside the copy, and read list_allowed_directories first.
  3. In Claude Code, claude mcp add <name> -- <command> adds a server at local scope by default, and a project-scoped .mcp.json server is approved by you before its first use in an interactive session [2].
  4. The first task for a new server is one whose answer you know. Read the transcript for the tool name, the arguments and the bucket, read or write, of every call.
  5. A remote server over Streamable HTTP does the same work with code you didn’t start, and it holds an OAuth token issued for that one server, which the specification wants short-lived [1].

You can now

  • Adds a tool via MCP or CLI with least privilege

  1. Anthropic and the MCP contributors. Model Context Protocol specification 2026-07-28. modelcontextprotocol.io. Reference. MCP specification
  2. Anthropic. Connect Claude Code to tools via MCP. Claude Code documentation. Reference. Claude Code mcp
  3. Anthropic and the MCP contributors. Filesystem MCP Server. GitHub, modelcontextprotocol/servers README. Reference. MCP filesystem server
  4. Anthropic and the MCP contributors. Deprecated Features, Model Context Protocol specification 2026-07-28. modelcontextprotocol.io. Reference. MCP deprecated features
  5. Anthropic. Configure permissions. Claude Code documentation. Reference. Claude Code permissions
  6. Anthropic. Model Context Protocol: Advanced topics. Claude Academy. Course. Academy model-context-protocol-advanced-topics