# Connecting your first tool server

> Connect a local file-system server scoped to one copied directory, give the agent a list-and-summarize test task, and read the transcript for every tool call and its arguments before the server touches real work.

Page: https://schubergphilis.github.io/ai-training/customizing-agents/mcp-first-server/
License: CC BY-SA 4.0, https://creativecommons.org/licenses/by-sa/4.0/

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

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 (Model Context Protocol specification 2026-07-28, modelcontextprotocol.io).
The lesson [Logging from a stdio server](https://schubergphilis.github.io/ai-training/customizing-agents/logging-from-a-stdio-server/)
shows what breaks when it writes anything else.
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
(Model Context Protocol specification 2026-07-28, modelcontextprotocol.io).

On its default runtime, Claude Code asks HTTP servers whether they
support the newer revision. It asks a stdio server in every session when
`MCP_PROTOCOL_NEGOTIATION` is set to `auto`, and Anthropic is rolling out
the same probe of stdio servers as a default for Claude Code v2.1.285 or
later in sessions where Claude Code fetches feature flags
(Connect Claude Code to tools via MCP, Claude Code documentation). The reference server in the exercise depends on
version 1 of the MCP TypeScript software development kit (SDK)
(Filesystem MCP Server, GitHub, modelcontextprotocol/servers, src/filesystem), and only version 2 of that SDK adds revision
2026-07-28 (Connect Claude Code to tools via MCP, Claude Code documentation). When a client asks such an older server
and gets an error or no answer, the client falls back to the `initialize`
handshake (Model Context Protocol specification 2026-07-28, modelcontextprotocol.io). So the stdio connection in the exercise
uses `initialize` whether Claude Code asks or not. The stand-in below
uses neither the `initialize` handshake nor the `_meta` field.

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 (Model Context Protocol specification 2026-07-28, modelcontextprotocol.io).

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: List the tools

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

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

Output:

```text
list_allowed_directories
list_directory
read_text_file
```

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 (Connect Claude Code to tools via MCP, Claude Code documentation). 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 the listing

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

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

The `[FILE]` prefix is the reference server's format, and the stand-in
copies it (Filesystem MCP Server, GitHub, modelcontextprotocol/servers, src/filesystem). A listing in a real transcript has
this form when it comes back from the server.

## 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 (Filesystem MCP Server, GitHub, modelcontextprotocol/servers, src/filesystem). Claude Code sends
roots: the directory you started it in, plus every directory you added
with `--add-dir`, `/add-dir` or the `additionalDirectories` setting
(Connect Claude Code to tools via MCP, Claude Code documentation). 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 (Model Context Protocol specification 2026-07-28, modelcontextprotocol.io).

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 (Deprecated Features, Model Context Protocol specification 2026-07-28, modelcontextprotocol.io). The
specification also says that roots guide a server and that the protocol
doesn't make a server stay inside them (Model Context Protocol specification 2026-07-28, modelcontextprotocol.io). 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 refusal

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

```python
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}")
```

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

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
(Filesystem MCP Server, GitHub, modelcontextprotocol/servers, src/filesystem). It has thirteen tools, and four of them change
things: `write_file`, `edit_file`, `create_directory` and `move_file`
(Filesystem MCP Server, GitHub, modelcontextprotocol/servers, src/filesystem). 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 (Connect Claude Code to tools via MCP, Claude Code documentation):

```sh
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 (Connect Claude Code to tools via MCP, Claude Code documentation), and the
server drops its command-line list in favor of that answer
(Filesystem MCP Server, GitHub, modelcontextprotocol/servers, src/filesystem). 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](https://schubergphilis.github.io/ai-training/customizing-agents/mcp/#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 (Configure permissions, Claude Code documentation).
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 (Connect Claude Code to tools via MCP, Claude Code documentation).
`claude mcp list` shows what you have configured, and `/mcp` inside a
session shows whether it connected (Connect Claude Code to tools via MCP, Claude Code documentation).

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: Which calls need a confirmation?

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

Buckets:

- Runs without confirmation
- Confirm before each call

Items:

- list_directory
- write_file
- read_text_file
- move_file
- list_allowed_directories
- edit_file

## 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.

#### Prompt (illustrative, not a recorded transcript)

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

#### Response

````text
```text
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)

```text
Also check whether the parent folder has other handbooks.
```

#### Response

````text
```text
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: What does the refusal tell you?

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

- The model misread the tool and picked the wrong one, so the tool description needs a rewrite.
- The server refused a path outside its allowed list, which is the scope doing its job.
- The client blocked the call under a permission rule, so a rule on the list tool is too strict.
- The stdio transport dropped the message, so the server should be restarted.

## 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
(Model Context Protocol specification 2026-07-28, modelcontextprotocol.io). The Academy's advanced MCP course covers both
transports (Model Context Protocol: Advanced topics, Claude Academy). 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 (Model Context Protocol specification 2026-07-28, modelcontextprotocol.io). Claude Code adds a remote
server with `--transport http` and a URL, and it supports OAuth for
servers that ask for a login (Connect Claude Code to tools via MCP, Claude Code documentation). 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.

#### Pitfall: A server on real work first

A developer adds a database server to their agent at user scope, so it is
there in every project. They give it the connection string for the team's
staging database. The first thing they ask it is real work: "clean up the
duplicate customer rows". The agent finds a `run_query` tool, writes a
`DELETE`, and runs it. Nobody had listed the tools, nobody had seen a
transcript from this server, and the client's approval prompt was
answered with "don't ask again" on the first call. The rule: the first
task for a new server is one whose right answer you already know and whose
wrong answer doesn't cost anything, and you read every call before the server
meets real data.

#### Checkpoint: The first task

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?

- Ask the agent to triage the open issues right away, since that is what the server is for.
- Ask the agent to list the tools and read back one issue you pick, and check the calls.
- Leave it as it is, because the configuration was reviewed in the pull request that added it.
- Remove the server and tell the team to use the tracker's CLI instead.

## 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`
(Connect Claude Code to tools via MCP, Claude Code documentation).
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 (Model Context Protocol specification 2026-07-28, modelcontextprotocol.io).
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 (Filesystem MCP Server, GitHub, modelcontextprotocol/servers, src/filesystem), and Claude Code's
   roots are the directory you started it in plus what you add with
   `--add-dir` (Connect Claude Code to tools via MCP, Claude Code documentation). Roots are deprecated in MCP revision
   2026-07-28 (Deprecated Features, Model Context Protocol specification 2026-07-28, modelcontextprotocol.io). 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
   (Connect Claude Code to tools via MCP, Claude Code documentation).
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 (Model Context Protocol specification 2026-07-28, modelcontextprotocol.io).

## References

- Model Context Protocol specification 2026-07-28, modelcontextprotocol.io, https://modelcontextprotocol.io/specification/2026-07-28
- Connect Claude Code to tools via MCP, Claude Code documentation, https://code.claude.com/docs/en/mcp
- Filesystem MCP Server, GitHub, modelcontextprotocol/servers, src/filesystem, https://github.com/modelcontextprotocol/servers/tree/main/src/filesystem
- Deprecated Features, Model Context Protocol specification 2026-07-28, modelcontextprotocol.io, https://modelcontextprotocol.io/specification/2026-07-28/deprecated
- Configure permissions, Claude Code documentation, https://code.claude.com/docs/en/permissions
- Model Context Protocol: Advanced topics, Claude Academy, https://academy.claude.com/courses/model-context-protocol-advanced-topics
