mirror of
https://github.com/shareAI-lab/analysis_claude_code.git
synced 2026-09-20 12:13:38 +08:00
183 lines
7.3 KiB
Markdown
183 lines
7.3 KiB
Markdown
# s18: MCP Tools — External Tools, Standard Protocol
|
|
|
|
[English](README.md) · [中文](README.zh.md) · [日本語](README.ja.md)
|
|
|
|
s01 → ... → s16 → s17 → `s18` → [s19](../s19_comprehensive/) → s20 → s21
|
|
|
|
> *"External tools, standard protocol"* — Discover, assemble, invoke. Agent doesn't need to know who wrote them.
|
|
>
|
|
> **Harness layer**: Plugins — External capabilities via a standard protocol.
|
|
|
|
---
|
|
|
|
## The Problem
|
|
|
|
From s01 through s17, every tool the agent uses was hand-written — bash, read, write, task, worktree. Input validation, execution logic, error handling — all written line by line.
|
|
|
|
Now you have 3 external services to integrate: the company's Jira API (query issues, create tickets), an in-house deployment system (trigger deploys, view logs), and the team's Notion knowledge base (search docs, create pages). You don't want to rewrite tool code for every service.
|
|
|
|
You need a standard protocol — as long as an external service implements it, the agent can call its tools directly, regardless of what language the service is written in.
|
|
|
|
---
|
|
|
|
## The Solution
|
|
|
|

|
|
|
|
MCP (Model Context Protocol) defines how agents discover and invoke external tools. Core concepts:
|
|
|
|
| Concept | Purpose |
|
|
|------|------|
|
|
| MCPClient | The agent-side client — connects to servers, discovers tools, invokes tools |
|
|
| MCP Server | The external service — implements `tools/list` + `tools/call` |
|
|
| assemble_tool_pool | Assembles built-in tools and MCP tools into one tool pool |
|
|
| mcp\_\_server\_\_tool naming | Prevents tool name collisions across different servers |
|
|
|
|
Carries forward s17's worktree isolation, autonomous claiming, and team protocols. This chapter adds the `connect_mcp` tool, which connects to a service, discovers its tools, and adds them to the tool pool.
|
|
|
|
The chapter registers in-process server handlers so the full discovery and invocation flow runs offline. Each handler exposes the two operations the client needs: `tools/list` and `tools/call`.
|
|
|
|
---
|
|
|
|
## How It Works
|
|
|
|
### MCPClient: Discovery + Invocation
|
|
|
|
```python
|
|
class MCPClient:
|
|
def __init__(self, name: str):
|
|
self.name = name
|
|
self.tools: list[dict] = []
|
|
self._handlers: dict[str, callable] = {}
|
|
|
|
def register(self, tool_defs, handlers):
|
|
"""Simulates tools/list discovery."""
|
|
self.tools = tool_defs
|
|
self._handlers = handlers
|
|
|
|
def call_tool(self, tool_name: str, args: dict) -> str:
|
|
"""Simulates tools/call."""
|
|
handler = self._handlers.get(tool_name)
|
|
if not handler:
|
|
return f"MCP error: unknown tool '{tool_name}'"
|
|
return handler(**args)
|
|
```
|
|
|
|
The registered Python functions provide the server-side tool implementations used by `tools/call`.
|
|
|
|
### connect_mcp: Connect + Discover
|
|
|
|
```python
|
|
def connect_mcp(name: str) -> str:
|
|
if name in mcp_clients:
|
|
return f"MCP server '{name}' already connected"
|
|
factory = MOCK_SERVERS.get(name)
|
|
if not factory:
|
|
return f"Unknown server '{name}'. Available: ..."
|
|
mcp_client = factory()
|
|
mcp_clients[name] = mcp_client
|
|
return f"Connected to '{name}'. Discovered: ..."
|
|
```
|
|
|
|
After connecting, the server's tools are immediately available.
|
|
|
|
### normalize_mcp_name: Name Normalization
|
|
|
|
```python
|
|
_DISALLOWED_CHARS = re.compile(r'[^a-zA-Z0-9_-]')
|
|
|
|
def normalize_mcp_name(name: str) -> str:
|
|
return _DISALLOWED_CHARS.sub('_', name)
|
|
```
|
|
|
|
All non-`[a-zA-Z0-9_-]` characters are replaced with `_`. Prevents special characters in server or tool names from causing naming conflicts or injection issues.
|
|
|
|
### assemble_tool_pool: Assemble Tool Pool
|
|
|
|
```python
|
|
def assemble_tool_pool() -> tuple[list[dict], dict]:
|
|
tools = list(BUILTIN_TOOLS)
|
|
handlers = dict(BUILTIN_HANDLERS)
|
|
for server_name, mcp_client in mcp_clients.items():
|
|
safe_server = normalize_mcp_name(server_name)
|
|
for tool_def in mcp_client.tools:
|
|
safe_tool = normalize_mcp_name(tool_def["name"])
|
|
prefixed = f"mcp__{safe_server}__{safe_tool}"
|
|
tools.append(...)
|
|
handlers[prefixed] = (
|
|
lambda *, c=mcp_client, t=tool_def["name"], **kw:
|
|
c.call_tool(t, kw))
|
|
return tools, handlers
|
|
```
|
|
|
|
The prefix `mcp__{server}__{tool}` prevents tool name collisions across different servers. Names are normalized through `normalize_mcp_name`.
|
|
|
|
MCP tool descriptions include `(readOnly)` or `(destructive)` labels, making the distinction visible in the tool metadata.
|
|
|
|
### No Cache: Tool Pool Changes, Prompt Changes Too
|
|
|
|
s10-s17's agent_loop used prompt caching to avoid re-serialization. s18 removes the cache:
|
|
|
|
```python
|
|
def agent_loop(messages, context):
|
|
tools, handlers = assemble_tool_pool() # Rebuild every time
|
|
system = assemble_system_prompt(context) # Regenerate every time
|
|
...
|
|
if any(b.name == "connect_mcp" ...):
|
|
tools, handlers = assemble_tool_pool() # Rebuild after connection
|
|
system = assemble_system_prompt(context)
|
|
```
|
|
|
|
After `connect_mcp`, the tool pool gains entries such as `mcp__docs__search`. Reusing the old serialized tool list would hide those entries from the model, so the loop rebuilds the pool and system prompt after every connection.
|
|
|
|
### MCP Tools: Lead Only
|
|
|
|
`connect_mcp` belongs to the Lead, and `assemble_tool_pool` serves the Lead's agent loop. Teammates keep their task, file, message, and plan tools; the Lead invokes external services and dispatches the resulting work.
|
|
|
|
---
|
|
|
|
## Changes from s17
|
|
|
|
| Component | Before (s17) | After (s18) |
|
|
|------|-----------|-----------|
|
|
| Tool source | All hand-written built-in | Hand-written + MCP external tools with dynamic discovery |
|
|
| Tool pool | Fixed BUILTIN_TOOLS | assemble_tool_pool dynamically assembles mcp\_\_ prefixed tools |
|
|
| Name safety | None | normalize_mcp_name normalization |
|
|
| New type | — | MCPClient class (simulates tools/list + tools/call) |
|
|
| Namespace | — | mcp\_\_server\_\_tool prevents collisions |
|
|
| Tool descriptions | No annotations | (readOnly)/(destructive) annotations |
|
|
| Prompt cache | Yes (since s10) | Removed — tool pool is dynamic, cache goes stale |
|
|
| Lead tools | Worktree and team tools | + connect_mcp and dynamically discovered MCP tools |
|
|
| Teammate tools | Task, file, message, and plan tools | Unchanged |
|
|
| Extension method | Write code to add tools | Standard protocol, implement servers in any language |
|
|
|
|
---
|
|
|
|
## Try It Out
|
|
|
|
```sh
|
|
cd learn-claude-code
|
|
python s18_mcp_plugin/code.py
|
|
```
|
|
|
|
Try these prompts:
|
|
|
|
1. `Search the docs for the worktree cleanup policy.`
|
|
2. `Deploy the current project and report the result.`
|
|
3. `What documentation and deployment actions can you perform?`
|
|
|
|
What to observe: After connecting to an MCP server, do tool names have `mcp__docs__` or `mcp__deploy__` prefixes? Are both servers' tools available simultaneously? Do MCP tool descriptions include (readOnly)/(destructive) annotations?
|
|
|
|
---
|
|
|
|
## What's Next
|
|
|
|
The Agent can now connect external tools through a standard protocol. The first 18 chapters introduced these mechanisms one at a time so each boundary stayed visible.
|
|
|
|
Tools, permissions, hooks, todo, task graph, memory, compact, background work, cron, teams, worktrees, and MCP should all attach to the same loop, not live in separate examples.
|
|
|
|
s19 Comprehensive Agent → Combine the mechanisms from s01-s18 into one complete harness. Many mechanisms, one loop.
|
|
|
|
|
|
<!-- translation-sync: zh@v2, en@v2, ja@v0 -->
|