Claude Architect WorkshopPDFEPUBlearnwithparam.comkaas.team

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.

I doWatch the failure and its fix.

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.

The file names the variable, the machine holds the value..mcp.jsonin git, sharedVariable name${STOCK_TOKEN}Environmentholds the valueServerreads the tokenthe env block is written in .mcp.jsonThe file names the variable, the machine holds the value..mcp.jsonin git, sharedVariable name${STOCK_TOKEN}Environmentholds the valueServerreads the tokenthe env block is written in .mcp.json

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.

  1. ColleagueHow many units of product P-1 are in stock?
    Show the result
    How many units of product P-1 are in stock?
  2. Agentthe token is in the environment
    Show the result
    {
      "mcpServers": {
        "stock": {
          "command": "python",
          "args": ["stock_server.py"],
          "env": {
            "STOCK_TOKEN": "${STOCK_TOKEN}"
          }
        }
      }
    }
    
  3. Server1 connected, server tool: mcp__stock__get_stock
    Show the result
    ToolSearch
    mcp__stock__get_stock
  4. 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.

starter/.mcp.json
1{2 "mcpServers": {3 "stock": {4 "command": "python",5 "args": ["stock_server.py"],6 "env": {7 "STOCK_TOKEN": "demo-token-1234"8 }9 }10 }11}

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?

  1. AA variable name, because the file is only a template.
  2. 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.

  1. ColleagueHow many units of product P-1 are in stock?
    Show the result
    How many units of product P-1 are in stock?
  2. Agentthe token is in the file
    Show the result
    {
      "mcpServers": {
        "stock": {
          "command": "python",
          "args": ["stock_server.py"],
          "env": {
            "STOCK_TOKEN": "demo-token-1234"
          }
        }
      }
    }
    
  3. Server1 connected, server tool: mcp__stock__get_stock
    Show the result
    ToolSearch
    mcp__stock__get_stock
  4. 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.

How the agent reaches the shared server.ColleagueAgentServerquestiontool callresultanswer
.mcp.jsongreen lines are what the fix adds
1{2 "mcpServers": {3 "stock": {4 "command": "python",5 "args": ["stock_server.py"],6 "env": {7 "STOCK_TOKEN": "${STOCK_TOKEN}"8 }9 }10 }11}
  1. ColleagueHow many units of product P-1 are in stock?
    Show the result
    How many units of product P-1 are in stock?
  2. Agentthe token is in the environment
    Show the result
    {
      "mcpServers": {
        "stock": {
          "command": "python",
          "args": ["stock_server.py"],
          "env": {
            "STOCK_TOKEN": "${STOCK_TOKEN}"
          }
        }
      }
    }
    
  3. Server2 connected, server tool: mcp__stock__get_stock
    Show the result
    ToolSearch
    mcp__stock__get_stock
  4. 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.

app/fixture/stock_server.py
1"""The Lumen Supply stock server, started by `claude` from `.mcp.json`.23It answers only when its STOCK_TOKEN variable is the demo token.4"""56import os78from mcp.server.mcpserver import MCPServer910DEMO_TOKEN = "demo-token-1234" # noqa: S105 - a fixture value11PRODUCTS = 3012server = MCPServer("stock")131415def allowed() -> bool:16 """Whether the caller passed the token through the environment."""17 return os.environ.get("STOCK_TOKEN") == DEMO_TOKEN181920def number(product: str) -> int | None:21 """The catalogue number in an id such as P-7, or None."""22 head, _, tail = product.partition("-")23 if head != "P" or not tail.isdigit():24 return None2526 return int(tail) if 1 <= int(tail) <= PRODUCTS else None272829@server.tool()30def get_stock(product: str) -> str:31 """Read the units in stock for one product id, from P-1 to P-30.3233 Read only. It does not return a price.34 """35 n = number(product)36 if not allowed():37 return "unauthorized"3839 return (40 "unknown product" if n is None else f"Stock: {12 + 5 * (n - 1)} units."41 )424344@server.tool()45def get_price(product: str) -> str:46 """Read the price in dollars for one product id, from P-1 to P-30.4748 Read only. It does not return a stock level.49 """50 n = number(product)51 if not allowed():52 return "unauthorized"5354 return "unknown product" if n is None else f"Price: ${19 + 3 * (n - 1)}."555657if __name__ == "__main__":58 server.run()
We doChange one setting at a time and predict.

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.

app/sdk_agent.py
1"""The same committed file, handed to the Agent SDK."""23from pathlib import Path45from claude_agent_sdk import ClaudeAgentOptions67from app.runner import BUILTIN8910def options(project: Path) -> ClaudeAgentOptions:11 """Options that load the project's `.mcp.json` and nothing else."""12 return ClaudeAgentOptions(13 cwd=project,14 mcp_servers=project / ".mcp.json",15 disallowed_tools=BUILTIN,16 )
You doFix the starting code, then test yourself.

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.

Terminal
./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?

  1. ADelete the token from the file in the next commit and leave the history as it is.
  2. BWrite ${STOCK_TOKEN} in the file, set the value per person, and rotate the old token.
  3. CKeep the token in the file and mark the repository private so only the team reads it.
  4. 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?

  1. AIn each developer's own user file, since it reaches every session they open.
  2. BIn the system prompt of the agent that uses it.
  3. CIn a README that lists the command to run by hand.
  4. 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?

  1. APut it in their user-level MCP file, which stays on their machine.
  2. BAdd it to the project .mcp.json and ask the others to ignore it.
  3. CWrite a custom server first, since a community one cannot be trusted.
  4. 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.

  1. AThe tools of every connected server are available to the agent, though definitions may load on demand.
  2. BSSE is the current transport for a remote server, and stdio is the legacy one.
  3. CA resource can act as a catalogue of what the server offers.
  4. 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?

  1. AAdd more examples of the question to the system prompt.
  2. BRename both tools to the same short name.
  3. CRewrite each description to state its input, its output and its limit.
  4. 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?

  1. ASplit it into narrow tools, one for each job.
  2. BList all 12 modes in the system prompt.
  3. CForce the tool with tool_choice so it is always called.
  4. 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?

  1. AThe policy tool is named badly, so it is always picked.
  2. BThe model ignored the prompt and chose by chance.
  3. CTool choice is stored in a config file the prompt overrides.
  4. 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.

  1. AA one-word name, since the model picks by name alone.
  2. BThe inputs the tool takes and what they look like.
  3. CThe token the tool needs, so the model can pass it along.
  4. 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.

json
{
  "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?

  1. ANothing. Claude Code refuses to load the file.
  2. 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

Conclusions

  • A committed .mcp.json is 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