Learn Claude Code
s02

Tools

Tools & Execution

One Handler Per Tool

121 LOC4 toolsTool dispatch map
Overview

你跟 Claude Code 说一件事,它自己挑该用哪个工具——读文件用 Read、跑 Stata 用 mcp__stata-mcp__stata_do、跨文件搜词用 Grep、写审计报告用 Write。代码这边只是按工具名字找到对应的执行函数,调用一下,把结果回传。挑哪个工具完全是模型的判断。

Running example
Research project
耐心资本对企业 ESG 表现的实证项目
What happens in this section

要核对 04_中间数据/main_panel.dta 里 PC(A2 主口径)的字段定义和 02_变量字典/测算方法说明.md 的口径是否一致,再确认 do6_baseline.do 里 reghdfe 的 PC 解释变量名跟 panel 数据集对得上。一句指令要走读 .md → 调 stata-mcp 看变量 label → grep 找代码用法 → 写一份核对报告四步,每步用不同工具。

s01 > [ s02 ] s03 > s04 > s05 > s06 | s07 > s08 > s09 > s10 > s11 > s12

"Adding a tool means adding one handler" -- the loop stays the same; new tools register into the dispatch map.

Harness layer: Tool dispatch -- expanding what the model can reach.

Problem

With only bash, the agent shells out for everything. cat truncates unpredictably, sed fails on special characters, and every bash call is an unconstrained security surface. Dedicated tools like read_file and write_file let you enforce path sandboxing at the tool level.

The key insight: adding tools does not require changing the loop.

Solution

+--------+      +-------+      +------------------+
|  User  | ---> |  LLM  | ---> | Tool Dispatch    |
| prompt |      |       |      | {                |
+--------+      +---+---+      |   bash: run_bash |
                    ^           |   read: run_read |
                    |           |   write: run_wr  |
                    +-----------+   edit: run_edit |
                    tool_result | }                |
                                +------------------+

The dispatch map is a dict: {tool_name: handler_function}.
One lookup replaces any if/elif chain.

How It Works

  1. Each tool gets a handler function. Path sandboxing prevents workspace escape.
def safe_path(p: str) -> Path:
    path = (WORKDIR / p).resolve()
    if not path.is_relative_to(WORKDIR):
        raise ValueError(f"Path escapes workspace: {p}")
    return path

def run_read(path: str, limit: int = None) -> str:
    text = safe_path(path).read_text()
    lines = text.splitlines()
    if limit and limit < len(lines):
        lines = lines[:limit]
    return "\n".join(lines)[:50000]
  1. The dispatch map links tool names to handlers.
TOOL_HANDLERS = {
    "bash":       lambda **kw: run_bash(kw["command"]),
    "read_file":  lambda **kw: run_read(kw["path"], kw.get("limit")),
    "write_file": lambda **kw: run_write(kw["path"], kw["content"]),
    "edit_file":  lambda **kw: run_edit(kw["path"], kw["old_text"],
                                        kw["new_text"]),
}
  1. In the loop, look up the handler by name. The loop body itself is unchanged from s01.
for block in response.content:
    if block.type == "tool_use":
        handler = TOOL_HANDLERS.get(block.name)
        output = handler(**block.input) if handler \
            else f"Unknown tool: {block.name}"
        results.append({
            "type": "tool_result",
            "tool_use_id": block.id,
            "content": output,
        })

Add a tool = add a handler + add a schema entry. The loop never changes.

What Changed From s01

ComponentBefore (s01)After (s02)
Tools1 (bash only)4 (bash, read, write, edit)
DispatchHardcoded bash callTOOL_HANDLERS dict
Path safetyNonesafe_path() sandbox
Agent loopUnchangedUnchanged

Try It

cd claude-code-for-researchers
python agents/s02_tool_use.py
  1. Read 02_变量字典/测算方法说明.md to understand the A2 framework
  2. Run stata-mcp summarize PC_A2 ESG controls on main_panel.dta
  3. Grep '\\cite{' across 07_论文写作/ and 01_文献/_按研究主题索引.md
  4. Write audit-variable-dict.md listing fields in main_panel.dta that have no entry in 02_变量字典/