mirror of
https://github.com/shareAI-lab/analysis_claude_code.git
synced 2026-06-20 20:23:36 +08:00
* feat: s01-s14 docs quality overhaul — tool pipeline, single-agent, knowledge & resilience Rewrite code.py and README (zh/en/ja) for s01-s14, each chapter building incrementally on the previous. Key fixes across chapters: - s01-s04: agent loop, tool dispatch, permission pipeline, hooks - s05-s08: todo write, subagent, skill loading, context compact - s09-s11: memory system, system prompt assembly, error recovery - s12-s14: task graph, background tasks, cron scheduler All chapters CC source-verified. Code inherits fixes forward (PROMPT_SECTIONS, json.dumps cache, real-state context, can_start dep protection, etc.). * feat: s15-s19 docs quality overhaul — multi-agent platform: teams, protocols, autonomy, worktree, MCP tools Rewrite code.py and README (zh/en/ja) for s15-s19, the multi-agent platform chapters. Each chapter inherits all previous fixes and adds one mechanism: - s15: agent teams (TeamCreate, teammate threads, shared task list) - s16: team protocols (plan approval, shutdown handshake, consume_inbox) - s17: autonomous agents (idle polling, auto-claim, consume_lead_inbox) - s18: worktree isolation (git worktree, bind_task, cwd switching, safety) - s19: MCP tools (MCPClient, normalize_mcp_name, assemble_tool_pool, no cache) All appendix source code references verified against CC source. Config priority corrected: claude.ai < plugin < user < project < local. * fix: 5 regressions across s05-s19 — glob safety, todo validation, memory extraction, protocol types, dep crash - s05-s09: glob results now filter with is_relative_to(WORKDIR) (inherited from s02) - s06-s08: todo_write validates content/status required fields (inherited from s05) - s09: extract_memories uses pre-compression snapshot instead of compacted messages - s16: submit_plan docstring clarifies protocol-only (not code-level gate) - s17-s19: match_response restores type mismatch validation (from s16) - s17-s19: claim_task deps list handles missing dep files without crashing * fix: s12 Todo V2 logic reversal, s14/s15 cron range validation, s18/s19 worktree name validation - s12 README (zh/en/ja): fix Todo V2 direction — interactive defaults to Task, non-interactive/SDK defaults to TodoWrite. Fix env var name to CLAUDE_CODE_ENABLE_TASKS (not TODO_V2). - s14/s15: add _validate_cron_field with per-field range checks (minute 0-59, hour 0-23, dom 1-31, month 1-12, dow 0-6), step > 0, range lo <= hi. Replace old try/except validation that only caught exceptions. - s18/s19: add validate_worktree_name() to remove_worktree and keep_worktree, not just create_worktree. * fix: align s16-s19 teaching tool consistency * fix pr265 chapter diagrams * Add comprehensive s20 harness chapter * Fix chapter smoke test regressions * Clarify README tutorial track transition --------- Co-authored-by: Haoran <bill-billion@outlook.com>
283 lines
12 KiB
Markdown
283 lines
12 KiB
Markdown
# s19: MCP Tools — External Tools, Standard Protocol
|
|
|
|
[中文](README.md) · [English](README.en.md) · [日本語](README.ja.md)
|
|
|
|
s01 → ... → s17 → s18 → `s19` → [s20](../s20_comprehensive/)
|
|
|
|
> *"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 s18, 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 s18's teaching-version worktree isolation, autonomous claiming, idle polling, and protocol system. This chapter adds: the `connect_mcp` tool — connect to external services, discover tools, add them to the tool pool.
|
|
|
|
The tutorial uses mock handlers to simulate external servers. The real version would spawn subprocesses and communicate via stdin/stdout JSON-RPC. Mocks let you run the full flow without external dependencies; the tradeoff is you don't see real network communication or process management.
|
|
|
|
---
|
|
|
|
## 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 tutorial uses Python functions to simulate server tool implementations. The real version communicates with subprocesses via stdio JSON-RPC.
|
|
|
|
### 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)` annotations — the tutorial uses text annotations, while real CC uses structured tool annotations for the permission system.
|
|
|
|
### No Cache: Tool Pool Changes, Prompt Changes Too
|
|
|
|
s10-s18's agent_loop used prompt caching to avoid re-serialization. s19 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)
|
|
```
|
|
|
|
Reason: after `connect_mcp`, the tool pool changes — new tools like `mcp__docs__search` are added. The cached tool list is stale; continuing to use it means the model can't call the new tools. The tutorial simply removes caching, at the cost of slightly more serialization time.
|
|
|
|
### MCP Tools: Lead Only
|
|
|
|
In the tutorial, `connect_mcp` is a Lead tool, and `assemble_tool_pool` only serves the Lead's agent_loop. Teammates still use a fixed 8-tool subset (bash, read_file, write_file, send_message, submit_plan, list_tasks, claim_task, complete_task).
|
|
|
|
This is a teaching simplification. In real CC, MCP tools are available to both the main agent and sub-agents — sub-agents inherit the parent's MCP configuration.
|
|
|
|
---
|
|
|
|
## Changes from s18
|
|
|
|
| Component | Before (s18) | After (s19) |
|
|
|------|-----------|-----------|
|
|
| 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 | 17 (s18) | 18 (+connect_mcp) |
|
|
| Teammate tools | 8 (s18) | 8 (unchanged, MCP tools are Lead-only) |
|
|
| Extension method | Write code to add tools | Standard protocol, implement servers in any language |
|
|
|
|
---
|
|
|
|
## Try It Out
|
|
|
|
```sh
|
|
cd learn-claude-code
|
|
python s19_mcp_plugin/code.py
|
|
```
|
|
|
|
Try these prompts:
|
|
|
|
1. `Connect to the docs MCP server and search for something`
|
|
2. `Connect to the deploy server and trigger a deployment`
|
|
3. `Connect both servers — what tools are now available?`
|
|
|
|
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. But the first 19 chapters each add one mechanism in isolation; a real Agent does not run as 19 separate demos.
|
|
|
|
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.
|
|
|
|
s20 Comprehensive Agent → Combine the first 19 chapters into one complete harness. Many mechanisms, one loop.
|
|
|
|
<details>
|
|
<summary>Deep Dive into CC Source</summary>
|
|
|
|
> The following is based on analysis of CC source: `services/mcp/client.ts`, `auth.ts`, `config.ts`, `channelNotification.ts`.
|
|
|
|
### 1. Six Transport Types
|
|
|
|
The tutorial only shows a stdio mock. CC supports 6 transport types (`types.ts:23-25`):
|
|
|
|
| Transport | Communication method |
|
|
|-----------|---------|
|
|
| `stdio` | Subprocess stdin/stdout (cross-platform default) |
|
|
| `sse` | HTTP Server-Sent Events |
|
|
| `http` | Streamable HTTP (POST/SSE bidirectional) |
|
|
| `ws` | WebSocket |
|
|
| `sse-ide` | IDE-embedded SSE transport |
|
|
| `sdk` | In-process SDK transport |
|
|
|
|
On connection, local (stdio) and remote (http/sse/ws) servers are batched concurrently: local batch of 3, remote batch of 20.
|
|
|
|
### 2. Tool Pool Merging Algorithm
|
|
|
|
`assembleToolPool()` (`tools.ts:345-364`):
|
|
|
|
```typescript
|
|
// Dedup with priority: built-in tools win on name collision (sorted first)
|
|
return uniqBy(
|
|
[...builtInTools.sort(byName), ...filteredMcpTools.sort(byName)],
|
|
'name',
|
|
)
|
|
```
|
|
|
|
Built-in and MCP tools are sorted separately, not together. The reason is CC's `claude_code_system_cache_policy` places a global cache breakpoint after the last built-in tool at a specific position — mixing the sort would break this design.
|
|
|
|
### 3. Naming Convention: `mcp__server__tool`
|
|
|
|
`buildMcpToolName()` (`mcpStringUtils.ts:50-52`):
|
|
|
|
```
|
|
mcp__<normalizedServerName>__<normalizedToolName>
|
|
```
|
|
|
|
All non-`[a-zA-Z0-9_-]` characters are replaced with `_` (`normalization.ts:17-23`). The tutorial's `normalize_mcp_name` uses the same rule.
|
|
|
|
### 4. Permission Checks
|
|
|
|
CC has a separate permission system for MCP tools. `checkPermissions()` applies different logic for MCP tools than for built-in tools — MCP tools can declare their own permission requirements (readOnly, destructive, etc.), and CC decides whether user confirmation is needed based on the declaration. The tutorial only uses text annotations `(readOnly)` / `(destructive)` in descriptions, without permission enforcement.
|
|
|
|
### 5. Configuration Sources and Priority
|
|
|
|
MCP server configuration comes from multiple sources. CC's priority from lowest to highest:
|
|
|
|
```
|
|
claude.ai connectors < plugin < user settings.json < approved project .mcp.json < local settings.local.json
|
|
```
|
|
|
|
`claude.ai` connectors are fetched separately, deduplicated by content signature, and merged at the lowest precedence (`config.ts:1267-1289`). When enterprise `managed-mcp.json` exists, all other configurations are excluded.
|
|
|
|
The tutorial passes server names directly to the `MOCK_SERVERS` dict, without config merging.
|
|
|
|
### 6. Channel Notifications: Servers Push Messages Back
|
|
|
|
The tutorial only covers agent → MCP Server unidirectional calls. CC also supports reverse notifications (`channelNotification.ts`):
|
|
|
|
1. Server declares `capabilities.experimental['claude/channel']`
|
|
2. Server sends messages to agent via MCP notification `notifications/claude/channel`
|
|
3. Messages are wrapped in `<channel source="serverName">...</channel>` XML tags
|
|
4. Agent is woken up by SleepTool (within 1 second)
|
|
|
|
Servers can also request permissions: `notifications/claude/channel/permission_request` → Agent replies `notifications/claude/channel/permission`. Users confirm/deny via a 5-letter short ID.
|
|
|
|
### 7. OAuth Authentication Flow
|
|
|
|
CC's MCP authentication (`auth.ts`) supports a full OAuth 2.0 + PKCE flow:
|
|
- OAuth metadata discovery via public client + PKCE (RFC 8414 / RFC 9728)
|
|
- Local callback server receives authorization code
|
|
- Tokens persisted via `getSecureStorage()` (macOS Keychain / Linux encrypted file / Windows Credential Manager)
|
|
- Auto-refresh 5 minutes before expiry
|
|
- Cross-application access (XAA): browser gets id_token → RFC 8693 + RFC 7523 exchange → no repeated browser popups
|
|
|
|
### 8. Connection Lifecycle Error Handling
|
|
|
|
CC has fine-grained error classification and retry for MCP connections (`client.ts:1266-1402`):
|
|
- Terminal errors (ECONNRESET, ETIMEDOUT, EPIPE, etc.): 3 consecutive failures → close + reconnect
|
|
- Tool call 401: Token expired → throw `McpAuthError` → trigger re-authentication
|
|
- Tool call timeout: `Promise.race` timeout (configurable, default ~28 hours)
|
|
- Stdio disconnect: Kill process in SIGINT → SIGTERM → SIGKILL order
|
|
|
|
### The Tutorial's Simplifications
|
|
|
|
- 6 transport types → 1 (mock stdio): Manageable concept count
|
|
- Channel reverse notifications → omitted: Tutorial agent is always the initiator
|
|
- OAuth flow → omitted: Tutorial assumes servers need no auth
|
|
- Multi-layer config priority → omitted: Tutorial passes server name directly
|
|
- Complex error classification → omitted: Tutorial uses try/except as fallback
|
|
- MCP tools Lead-only → omitted sub-agent inheritance: Simplifies code structure
|
|
|
|
</details>
|
|
|
|
<!-- translation-sync: zh@v2, en@v2, ja@v0 -->
|