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.
What the client asks the server
Section titled “What the client asks the server”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.
List the tools
Section titled “List the tools”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"])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.
Predict the listing
Section titled “Predict the listing”The lesson's stand-in file-system server offers list_directory, which prefixes each entry with [FILE] or [DIR], and is scoped to a handbook directory that holds README.md, deploy.md, on-call.md and onboarding.md. The client prints the result text and then the isError flag.
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}")[FILE] README.md [FILE] deploy.md [FILE] on-call.md [FILE] onboarding.md isError: False
Output verified in CI from site/examples/customizing-agents/mcp-first-server/list_directory.py.
Which four files are in the handbook, in what order does a sorted listing put them, and is a successful call an error?
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.
Scope the server to one directory
Section titled “Scope the server to one directory”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.
Predict the refusal
Section titled “Predict the refusal”The lesson's stand-in file-system server is scoped to one handbook directory and refuses a path outside it with a result whose text starts with Access denied and whose isError flag is set. The client prints the part of the text before the first colon, then the flag.
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}")Access denied - path outside allowed directories isError: True
Output verified in CI from site/examples/customizing-agents/mcp-first-server/outside_scope.py.
The path is the handbook's parent directory. Is that inside the allowed list, and how does a tool report a failure?
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.
Connect the real server
Section titled “Connect the real server”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]:
cp -r site/examples/customizing-agents/mcp-first-server/handbook /tmp/handbookcd /tmp/handbookclaude mcp add handbook -- npx -y @modelcontextprotocol/server-filesystem@2026.8.31 /tmp/handbookclaudeThe 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.
Which calls need a confirmation?
Section titled “Which calls need a confirmation?”The reference file-system MCP server has read tools and write tools. The client can require a confirmation before each call of a named tool.
You are setting per-tool approval for the handbook server. Place each
tool where it belongs.
Which tools leave the directory different from how they found it?
Read the transcript
Section titled “Read the transcript”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.
Using the handbook server, list the files in /tmp/handbook and give me a one-line summary of each. Don’t change anything.
list_allowed_directories() -> /tmp/handbooklist_directory(path="/tmp/handbook") -> [FILE] README.md [FILE] deploy.md [FILE] on-call.md [FILE] onboarding.mdread_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.
Also check whether the parent folder has other handbooks.
list_directory(path="/tmp") -> Access denied - path outside allowed directories: /tmp not in /tmp/handbookI 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.
What does the refusal tell you?
Section titled “What does the refusal tell you?”A learner connected a file-system MCP server scoped to /tmp/handbook. The transcript shows the model calling list_directory with path /tmp and the server answering Access denied, path outside allowed directories.
The transcript shows list_directory(path="/tmp") answered with
Access denied - path outside allowed directories. What happened?
Who refused the call, and what would have to change for it to succeed?
Widen the scope?
Section titled “Widen the scope?”A learner runs Claude Code from a project's docs directory with a file-system MCP server connected, so the server's allowed list is that directory. The next task needs the agent to read one file in a second directory.
A server is scoped to docs/. The next task needs one file from
contracts/ as well. What do you do?
Which option gives the new task exactly what it needs, and not more?
Local and remote
Section titled “Local and remote”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.
The first task
Section titled “The first task”A teammate has added a new MCP server for the team's issue tracker to the project's shared configuration and asks what to do with it first.
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?
Which option produces a transcript you can check against something you already know?
Put the exchange in order
Section titled “Put the exchange in order”A local MCP server runs over stdio as a child process of the agent, and the client and server exchange messages defined by the MCP specification.
- The agent starts the server as a child process over stdio
- The client asks `tools/list` once
- The client sends `tools/call` for each use of a tool
- The server returns a result, with `isError` set if it refused
What must the client know before it can call a tool, and when does a refusal show?
A call outside the scope
Section titled “A call outside the scope”A file-system MCP server is scoped to one handbook directory. The model calls list_directory on the handbook's parent directory.
What does the server send back for the call on the parent directory?
How does a server refuse, as far as the protocol goes?
Which tools need a confirmation?
Section titled “Which tools need a confirmation?”The reference file-system MCP server has read tools and write tools. The client can require a confirmation before each call of a named tool.
Which two tools do you set to confirm before each call?
Which of these change something on disk?
The first task
Section titled “The first task”A learner has just added a wiki MCP server to their coding agent and hasn't used it yet.
You have just added a wiki server. What is the first task you give the agent through it?
Which task lets you check each call against an answer you already have?
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
- A local server runs over stdio as a child process of the agent. The
client asks
tools/listonce and sendstools/callper use, and a refusal comes back as a result withisErrorset [1]. - 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 readlist_allowed_directoriesfirst. - In Claude Code,
claude mcp add <name> -- <command>adds a server at local scope by default, and a project-scoped.mcp.jsonserver is approved by you before its first use in an interactive session [2]. - 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.
- 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
References
Section titled “References”- Anthropic and the MCP contributors. Model Context Protocol specification 2026-07-28. modelcontextprotocol.io. Reference.
MCP specification - Anthropic. Connect Claude Code to tools via MCP. Claude Code documentation. Reference.
Claude Code mcp - Anthropic and the MCP contributors. Filesystem MCP Server. GitHub, modelcontextprotocol/servers README. Reference.
MCP filesystem server - Anthropic and the MCP contributors. Deprecated Features, Model Context Protocol specification 2026-07-28. modelcontextprotocol.io. Reference.
MCP deprecated features - Anthropic. Configure permissions. Claude Code documentation. Reference.
Claude Code permissions - Anthropic. Model Context Protocol: Advanced topics. Claude Academy. Course.
Academy model-context-protocol-advanced-topics