Claude CodeLesson
MCP servers
An agent that can only read files and run commands can't file an issue or review a pull request unless someone writes glue for each system it should reach. MCP is one plug for all of them: a system that speaks it offers its actions as tools, and the agent calls them like its own.
Know MCP already? Skip to In Terrarium.
Outside the box · General
The concept
The Model Context Protocol (MCP) is an open standard for connecting an AI tool to outside systems. An MCP server offers a set of tools, and Claude Code connects to it as a client. You add a server with claude mcp add. By default it is private to you and this project, kept in ~/.claude.json. With --scope project it goes in a .mcp.json file at the project's root, which you commit so everyone gets the same servers:
{
"mcpServers": {
"tracker": {
"type": "http",
"url": "https://tracker.example.com/mcp"
}
}
}
Each tool gets a full name, mcp__<server>__<tool>, such as mcp__tracker__create_issue. Permission rules and hook matchers use that full name. In a hook matcher, mcp__tracker__.* matches every tool from one server; a bare mcp__tracker is compared as an exact string and matches nothing.
Tool definitions cost context, so by default Claude Code defers them: a session starts with only each tool's name and each server's instructions, and Claude loads a tool's definition with the ToolSearch tool before it calls it. A result too large for the context is saved to a file, and Claude reads it from there.
Full referenceClaude Code docs: Connect Claude Code to tools via MCP
In Terrarium
Terrarium commits no .mcp.json. Its MCP servers come with the cloud session it runs in: GitHub's, whose tools are named mcp__github__*, and the cloud host's own server for sessions and scheduled tasks, named mcp__claude-code-remote__*. A local session has the GitHub tools only if its user configured them. docs/agents/github-integration.md maps each GitHub job to its tool.
Terrarium's own code reaches these tools by their full names, in several places.
Allowed by name. The committed allow list in .claude/settings.json pre-approves the GitHub tool that opens a PR and the one that reads a PR, its checks included:
5 "permissions": {
6 "allow": [
7 "mcp__github__create_pull_request",
8 "mcp__github__pull_request_read",
9 "Workflow(clear-backlog-explore)",
10 "Workflow(clear-backlog-build)"
11 ]
12 },
Guarded by name. The guard that checks every GitHub post for its session header, scripts/hooks/github-provenance-guard.ts, keeps a registry with one row per write tool: which parameter holds the text, and which shape that text must take. Commit and PR attribution shows the check, and Blocking hooks how a guard refuses a call. A row names its parameter only when it isn't body, and each name comes from the tool's real schema, loaded with ToolSearch, never guessed from the tool's name:
67export const GITHUB_PROVENANCE_TOOLS: readonly ProvenanceTool[] = [
68 // A comment IS its body — one with none is malformed, not exempt.
69 { tool: 'mcp__github__add_issue_comment' },
⋯ // …
81 // MCP-API commits bypass `.githooks/commit-msg` (#723): trailer, not header.
82 { tool: 'mcp__github__create_or_update_file', field: 'message', surface: 'commit' },
⋯ // …
86]
Matched by name, or by server. Each guard's PreToolUse matcher in .claude/settings.json lists its tools one by one, as the search guard's does:
104 {
105 "matcher": "mcp__github__search_issues|mcp__github__search_pull_requests",
106 "hooks": [
107 {
108 "type": "command",
109 "command": "cd \"${CLAUDE_PROJECT_DIR:-.}\" && node_modules/.bin/tsx scripts/hooks/github-search-fields-guard.ts",
110 "onFailure": "block"
111 }
112 ]
113 }
The provenance guard's matcher has the same shape, one line naming every write tool. The hint that points to the GitHub doc at a session's first GitHub call matches the whole server instead:
129 {
130 "matcher": "Bash|mcp__github__.*",
131 "hooks": [
132 {
133 "type": "command",
134 "command": "cd \"${CLAUDE_PROJECT_DIR:-.}\" && sh scripts/hooks/read-first-hint.sh || true"
135 }
136 ]
137 },
Scripts that need many GitHub records, such as scripts/github/list-open-issues.ts, skip MCP and ask GitHub's REST API for a few fields with gh api.
Why it's built this way
- Writes the guard can see. Agents post as the owner, so a guard makes each post name its session. Reading shell text, it misses a
gh apicall inside a script, but it sees every MCP call's body. So agents write through MCP. The cost: the guard lists write tools by name, so a new one goes unchecked until added. ADR-0017 (docs/adr/0017-provenance-footer-on-agent-authored-content.md). - Name tools, not the server. A server-wide
mcp__github__.*matcher would run each guard on every GitHub read, paying the start-up cost Claude Code hooks explains, and a guard that can't start would block them all. So each guard's matcher lists only the tools it checks. The cost: a long matcher line, written by hand, checked against the registry by a unit test. docs/agents/guards.md. - Allow the calls every PR makes. Confirm dialogs on opening a PR and polling its checks stalled the open-then-watch flow every session follows, so the committed allow list pre-approves those tools. The cost: it helps only a trusted local session (see What to watch for).
- Servers from the host. GitHub access is a connection the owner sets up outside the repository, and the cloud proxy adds the owner's credential to each request. Terrarium has nothing to put in a
.mcp.json. The cost: a local session without its own GitHub server falls back togh. docs/agents/github-integration.md.
What to watch for
- Load a tool by its full name. A deferred tool loads only by its full name,
mcp__github__list_issues, notlist_issues; aselect:that mixes the two silently loads only the full one, and calling an unknown name fails with no hint. docs/agents/github-integration.md lists the query forms and the recovery. - The allow list may be ignored. An untrusted workspace drops
permissions.allowbefore matching any rule, and a multi-repository cloud session does not read it at all (Cloud sessions); its GitHub tools stay quiet through another path. Don't add an entry there to silence a prompt you see in the cloud: docs/agents/environment-caveats.md says why it does nothing. - Some GitHub calls fail or overflow.
ghcommands built on GitHub's GraphQL API fail in a cloud session, and the GitHub list tools return whole records that can overflow. docs/agents/github-integration.md has the table of which tool works where and the ways to keep results small.
Try it in your project
Add a server your team uses with claude mcp add --scope project, and commit the .mcp.json it writes. Add a PreToolUse hook whose matcher is the bare mcp__<server> and that logs each call, then ask Claude for something only that server can do: the log stays empty, since a bare name is compared as an exact string. Change the matcher to mcp__<server>__.*, ask again, and read which calls, reads included, now reach the log.
Where next
- LessonClaude Code hooks
- LessonBlocking hooks
- LessonCommit and PR attribution
- LessonPull requests
- LessonAuthor trust
Walkthroughs through this Lesson
- WalkthroughAsking an agent for a change
Last updated · Latest source on GitHub ↗