Foundations, lesson 15
Sharing an MCP server
Share the MCP server, keep the token in the environment
A team shares one MCP server through .mcp.json, and the file carries the access token inline. The file is committed, so the token is now in git for everyone who clones the repository, and in every copy of its history. This is the agent at Lumen Supply, an online electronics shop. You will watch a token travel with the file, then fix the file so it names the variable and the value stays on each machine.
The file is shared, the value is not
An MCP server is a separate program that offers tools to an agent over a standard protocol. A project .mcp.json file lists the servers Claude Code starts, and it is committed, so every teammate gets the same ones. That is the point of it. It also means anything written in it is shared.
Claude Code reads ${NAME} in that file from the environment. The file holds the name and each machine holds the value.
A variable name in the file gives a working server
The file names the variable. Claude Code fills in the value from the environment when it starts the server. The agent calls the tool and answers.
The call list also shows Bash, Read and ToolSearch. The agent asked for Bash and Read too, but this run allows only the stock tool. Claude Code defers MCP tool definitions until they are needed, so the agent calls ToolSearch to load the stock tool's definition before it calls the tool.
Open the agent card to read the committed file. It holds no token.
- ColleagueHow many units of product P-1 are in stock?
Show the result
How many units of product P-1 are in stock?
- Agentthe token is in the environment
Show the result
{ "mcpServers": { "stock": { "command": "python", "args": ["stock_server.py"], "env": { "STOCK_TOKEN": "${STOCK_TOKEN}" } } } } - Server1 connected, server tool: mcp__stock__get_stock
Show the result
ToolSearch mcp__stock__get_stock
- AgentP-1 has 12 units in stock.
Show the result
P-1 has 12 units in stock.
The token is in the environment: safe to commit.
The environment block as written
The env block tells Claude Code which variables to give the stock server. The starting file writes the token itself into the block, so the committed file holds the secret. A leaked token also stays in history after the file is fixed, so the old value must be rotated.
What does the committed file hold?
Take the stock question. The starting .mcp.json has the token written in its env block, and the team commits the file.
Predict: What is in the file that would be committed?
- AA variable name, because the file is only a template.
- BThe token itself, because the file was written with the value.
Show the answer
B. The file holds exactly what is written in it. Nothing turns a value into a name on the way, so the secret sits in plain text for everyone with the repository.
The token is written into the file
This is the same question with the starting code. The server answers, so nothing looks wrong at runtime. The committed file holds the token, and the result card is red.
This is a real Claude Haiku 5.5 run through the Claude Code CLI, recorded once and replayed here.
- ColleagueHow many units of product P-1 are in stock?
Show the result
How many units of product P-1 are in stock?
- Agentthe token is in the file
Show the result
{ "mcpServers": { "stock": { "command": "python", "args": ["stock_server.py"], "env": { "STOCK_TOKEN": "demo-token-1234" } } } } - Server1 connected, server tool: mcp__stock__get_stock
Show the result
ToolSearch mcp__stock__get_stock
- AgentP-1 has 12 units in stock.
Show the result
P-1 has 12 units in stock.
The token is in the file: the secret is in git.
Write the name of the variable, not the value
The fix writes the name in the form ${STOCK_TOKEN} and never the value. Each teammate sets the variable on their own machine.
The server still gets its token. The repository never holds it. A token that was already committed stays readable in history, so rotate it: revoke the old value and issue a new one.
A server only one person needs stays in that person's own user file. Before writing a custom server, check whether a maintained community one fits. The run shows both files active: two servers connected, and the stock tool answers.
- ColleagueHow many units of product P-1 are in stock?
Show the result
How many units of product P-1 are in stock?
- Agentthe token is in the environment
Show the result
{ "mcpServers": { "stock": { "command": "python", "args": ["stock_server.py"], "env": { "STOCK_TOKEN": "${STOCK_TOKEN}" } } } } - Server2 connected, server tool: mcp__stock__get_stock
Show the result
ToolSearch mcp__stock__get_stock
- AgentP-1 has 12 units in stock.
Show the result
P-1 has 12 units in stock.
The token is in the environment: safe to commit.
Descriptions decide which tool is called
A shared server only helps if the model picks the right tool from it, and it picks from each tool's description. A description that says "gets data" gives the model nothing to choose by, so a question can go to the wrong tool. State the inputs, give an example of one, and say what the tool does not return.
The stock server, shown below, says that get_stock reads units for a product id from P-1 to P-30 and returns no price. Two tools with overlapping descriptions are the same fault, so give each its own inputs and limits.
Split a catch-all tool, one that takes a mode field, into narrow tools. Prompt wording pulls too: "always check the policy first" makes the policy tool run on every question, even a greeting. Word it for the case that needs it.
The same question, two committed files
Use the switch to run the question with the token inline, then with the name of the variable. The card marked "This is where the runs split" shows where the token lives.
Try the price question too. Both files reach the server the same way.
This part is interactive in the lab: run ./workshop watch mcp-integration.
Try it: token and questions
Change one setting at a time. Pick a question, then switch between the inline token and the variable name.
Every combination is a recorded run, so this works with no internet.
This part is interactive in the lab: run ./workshop watch mcp-integration.
The same file in the Agent SDK
The Agent SDK takes the path of the same .mcp.json, so the file you committed serves both the command line and code. Built-in tools are turned off here, so the model has only the server's tools.
This server is a local process that talks over stdio. A remote server uses streamable HTTP, and SSE is deprecated. Every connected server's tools are available to the agent, though Claude Code may defer loading their definitions behind tool search. A resource is content the agent reads, such as a catalogue of what exists, while a tool acts.
You do: take the token out
Copy the starting file and open work/mcp-integration/.mcp.json. The stock server's token is written into the file, so every clone of the repository carries it.
./workshop start mcp-integration
./workshop check mcp-integration
./workshop watch mcp-integration
The checks want no token in the file and the variable name in its place. The server must still receive STOCK_TOKEN, and the options must point at the committed file.
Check your understanding
Answer each question, then read why every option is right or wrong. The wrong options are the mistakes engineers make on the job.
1. A committed .mcp.json holds a token in its env block, and 12 people can clone the repository. What is the right fix?
- ADelete the token from the file in the next commit and leave the history as it is.
- BWrite ${STOCK_TOKEN} in the file, set the value per person, and rotate the old token.
- CKeep the token in the file and mark the repository private so only the team reads it.
- DAdd a line to the system prompt that tells the agent never to print the token.
Show the answer
Answer: B
- A The old commit still holds the token, so all 12 can still read it. Rotate it too.
- B Correct. The file names the variable, the value stays on each machine, and the committed token is rotated.
- C Everyone with access still reads it, and a private repository can be shared or cloned.
- D The token is in the file, not in the agent's answer. A prompt does not remove it.
2. Six developers need the same stock server to start on every clone of a repository. Where does its configuration live?
- AIn each developer's own user file, since it reaches every session they open.
- BIn the system prompt of the agent that uses it.
- CIn a README that lists the command to run by hand.
- DIn a .mcp.json at the repository root.
Show the answer
Answer: D
- A A user file lives on one machine. The other five never receive it.
- B A prompt cannot start a server. Servers start from configuration.
- C A README is read once, and each of the six must repeat the steps.
- D Correct. It is committed, so every clone starts the same server.
3. One developer wants to try a server that nobody else needs yet. Which choice keeps it away from the other 11?
- APut it in their user-level MCP file, which stays on their machine.
- BAdd it to the project .mcp.json and ask the others to ignore it.
- CWrite a custom server first, since a community one cannot be trusted.
- DUse the legacy SSE transport so it stays separate.
Show the answer
Answer: A
- A Correct. The user file is personal, and it runs beside the shared servers.
- B Everyone who clones gets it started. Asking them to ignore it does not stop that.
- C A maintained community server is the start. Write your own only when none fits.
- D SSE is the older transport and it does not keep a server private.
4. Which two statements about MCP servers are true? Select two.
- AThe tools of every connected server are available to the agent, though definitions may load on demand.
- BSSE is the current transport for a remote server, and stdio is the legacy one.
- CA resource can act as a catalogue of what the server offers.
- DA resource and a tool are the same thing under two names.
Show the answer
Answer: A, C
- A Correct. Each connected server's tools join the same list, and a definition may load on demand.
- B Streamable HTTP is current for a remote server. Stdio suits a local one.
- C Correct. A resource gives the agent content to read, such as a list of what exists.
- D A tool acts. A resource is content the agent reads.
5. Two tools are described as "gets order info", and 4 of every 10 calls go to the wrong one. What fixes it?
- AAdd more examples of the question to the system prompt.
- BRename both tools to the same short name.
- CRewrite each description to state its input, its output and its limit.
- DRemove one tool and let the other handle both jobs.
Show the answer
Answer: C
- A That treats the symptom. The two descriptions still look the same to the model.
- B Two tools with one name are harder to tell apart, not easier.
- C Correct. Distinct inputs and limits let the model tell the two tools apart.
- D A catch-all tool with two jobs is the same problem in a single piece.
6. A single tool takes 12 modes through a mode field, and the model passes the wrong mode in 3 of 10 calls. What is the better design?
- ASplit it into narrow tools, one for each job.
- BList all 12 modes in the system prompt.
- CForce the tool with tool_choice so it is always called.
- DRaise the turn limit so a wrong mode can be retried.
Show the answer
Answer: A
- A Correct. A narrow tool has one input and one purpose, so there is less to get wrong.
- B The tool still takes any mode. The prompt is a request, not a limit.
- C Forcing the call does not choose the right mode.
- D Each retry is another chance at the wrong mode, and some modes write data.
7. A system prompt says "always check the policy first", and now the policy tool runs on all 10 of 10 questions, even a greeting. What is the likely cause?
- AThe policy tool is named badly, so it is always picked.
- BThe model ignored the prompt and chose by chance.
- CTool choice is stored in a config file the prompt overrides.
- DThe wording "always" biased the choice toward that tool.
Show the answer
Answer: D
- A A name does not force a call. The prompt wording does.
- B It followed the prompt. That is the problem.
- C Wording in a prompt can bias the pick. No config file is involved.
- D Correct. Prompt wording can bias tool choice, so reword it for the case that needs it.
8. Which two things belong in a good tool description? Select two.
- AA one-word name, since the model picks by name alone.
- BThe inputs the tool takes and what they look like.
- CThe token the tool needs, so the model can pass it along.
- DThe limits of the tool, such as what it does not return.
Show the answer
Answer: B, D
- A The model reads the whole description to decide. A bare name gives it little.
- B Correct. The form of the inputs tells the model when the tool fits.
- C A secret in a description is shared with every reader. Keep it in the environment.
- D Correct. A stated limit keeps a call from going to the wrong tool.
Play: predict the output
The committed file now holds a variable in place of the token.
{
"mcpServers": {
"stock": {
"command": "python",
"args": ["stock_server.py"],
"env": { "STOCK_TOKEN": "${STOCK_TOKEN}" }
}
}
}
Predict: A new teammate opens the project without STOCK_TOKEN set. What does the stock server receive?
- ANothing. Claude Code refuses to load the file.
- BThe literal text ${STOCK_TOKEN}, with a warning in claude mcp list.
Show the answer
B. An unset variable with no default is passed as written, so the server fails to log in for a reason far from the cause. Set the variable, or give a default with ${STOCK_TOKEN:-value} where one is safe.
References
- MCP in Claude Code: project scope, .mcp.json and where variables are expanded.
- MCP resources specification: how a server exposes data as resources beside its tools.
- Agent SDK MCP: the same server config passed to an SDK agent.
- MCP security best practices: token handling and the other risks of running a server.
Conclusions
- A committed
.mcp.jsonis shared, so it names variables and never holds values. - Each machine sets the variable, so the secret stays out of git.
- A personal server lives in the user's own file, and both are active at once.
You can now write a shared MCP config that a whole team can commit safely.
.mcp.json
The project file that lists shared MCP servers
Expansion
Filling in ${NAME} from the environment
Project scope
Servers shared by everyone who clones the repository
User scope
Servers that belong to one person