Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 2 additions & 5 deletions s14_mcp_plugin/README.ja.md
Original file line number Diff line number Diff line change
Expand Up @@ -119,13 +119,10 @@ tools.append({
"description": tool_def.get("description", ""),
"input_schema": schema,
})
handlers[prefixed] = (
lambda *, client=server, tool=raw_name, **kwargs:
client.call_tool(tool, kwargs)
)
handlers[prefixed] = make_mcp_handler(server, raw_name)
```

model は prefix 付きの名前を見る。handler は server の元の tool name で `MCPClient` を呼ぶ。default argument が現在の client と tool を保持するため、loop 内の lambda がすべて最後の tool を参照することはない。
model は prefix 付きの名前を見る。handler は server の元の tool name で `MCPClient` を呼ぶ。`make_mcp_handler()` が現在の client と tool を closure に保持するため、loop 内の lambda がすべて最後の tool を参照することはない。default argument でも同じことはできるが、それは通常の keyword parameter のままであり、tool input の `tool` key が default を上書きし、許可済みの呼び出しを別の server tool に差し替えてしまう。

### 6. permission は host が決める

Expand Down
7 changes: 2 additions & 5 deletions s14_mcp_plugin/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -119,13 +119,10 @@ tools.append({
"description": tool_def.get("description", ""),
"input_schema": schema,
})
handlers[prefixed] = (
lambda *, client=server, tool=raw_name, **kwargs:
client.call_tool(tool, kwargs)
)
handlers[prefixed] = make_mcp_handler(server, raw_name)
```

The model sees the prefixed name. The handler calls `MCPClient` with the server's original tool name. Default arguments capture the current client and tool so every lambda does not point to the last item in the loop.
The model sees the prefixed name. The handler calls `MCPClient` with the server's original tool name. `make_mcp_handler()` captures the current client and tool in a closure so every lambda does not point to the last item in the loop. Default arguments fix that too, but they stay ordinary keyword parameters: a `tool` key in the tool input would replace them and send an approved call to a different server tool.

### 6. The host decides permissions

Expand Down
7 changes: 2 additions & 5 deletions s14_mcp_plugin/README.zh.md
Original file line number Diff line number Diff line change
Expand Up @@ -119,13 +119,10 @@ tools.append({
"description": tool_def.get("description", ""),
"input_schema": schema,
})
handlers[prefixed] = (
lambda *, client=server, tool=raw_name, **kwargs:
client.call_tool(tool, kwargs)
)
handlers[prefixed] = make_mcp_handler(server, raw_name)
```

模型看到带前缀的名字;handler 仍使用 server 原始工具名调用 `MCPClient`。默认参数保存当前 client 和 tool,避免循环里的 lambda 全部指向最后一个工具。
模型看到带前缀的名字;handler 仍使用 server 原始工具名调用 `MCPClient`。`make_mcp_handler()` 用闭包保存当前 client 和 tool,避免循环里的 lambda 全部指向最后一个工具。默认参数也能做到这一点,但它们仍是普通关键字参数:工具输入里的 `tool` 键会覆盖默认值,把已放行的调用改派到另一个 server 工具。

### 6. 权限由宿主配置决定

Expand Down
10 changes: 6 additions & 4 deletions s14_mcp_plugin/code.py
Original file line number Diff line number Diff line change
Expand Up @@ -320,6 +320,11 @@ def run_connect_mcp(name: str) -> str:
BUILTIN_HANDLERS = {**BASE_HANDLERS, "connect_mcp": run_connect_mcp}


def make_mcp_handler(client: MCPClient, tool: str):
"""Bind client and tool in a closure so tool input cannot replace them."""
return lambda **kwargs: client.call_tool(tool, kwargs)


def assemble_tool_pool() -> tuple[list[dict], dict[str, callable]]:
"""Combine built-in tools with every connected server tool."""
global mcp_tool_policies
Expand Down Expand Up @@ -354,10 +359,7 @@ def assemble_tool_pool() -> tuple[list[dict], dict[str, callable]]:
"description": tool_def.get("description", ""),
"input_schema": schema,
})
handlers[prefixed] = (
lambda *, client=server, tool=raw_name, **kwargs:
client.call_tool(tool, kwargs)
)
handlers[prefixed] = make_mcp_handler(server, raw_name)
policies[prefixed] = MCP_HOST_POLICY.get(
(server_name, raw_name), "confirm"
)
Expand Down
10 changes: 6 additions & 4 deletions s15_integrated_harness/code.py
Original file line number Diff line number Diff line change
Expand Up @@ -2751,6 +2751,11 @@ def connect_mcp(name: str) -> str:
f"Discovered {len(mcp_client.tools)} tools: {', '.join(tool_names)}")


def make_mcp_handler(client: MCPClient, tool: str):
"""Bind client and tool in a closure so tool input cannot replace them."""
return lambda **kwargs: client.call_tool(tool, kwargs)


def assemble_tool_pool() -> tuple[list[dict], dict]:
"""Merge builtin tools + all MCP tools into one pool."""
global mcp_tool_policies
Expand Down Expand Up @@ -2784,10 +2789,7 @@ def assemble_tool_pool() -> tuple[list[dict], dict]:
"description": tool_def.get("description", ""),
"input_schema": schema,
})
handlers[prefixed] = (
lambda *, client=mcp_client, tool=raw_name, **kwargs:
client.call_tool(tool, kwargs)
)
handlers[prefixed] = make_mcp_handler(mcp_client, raw_name)
policies[prefixed] = MCP_HOST_POLICY.get(
(server_name, raw_name), "confirm"
)
Expand Down
15 changes: 15 additions & 0 deletions tests/test_agent_teams_runtime.py
Original file line number Diff line number Diff line change
Expand Up @@ -648,6 +648,21 @@ def test_mcp_permission_uses_host_policy(self):
"Permission denied by user",
)

def test_mcp_handlers_ignore_a_tool_key_in_the_tool_input(self):
for lesson_path in MCP_LESSONS:
with self.subTest(lesson=lesson_path.parent.name):
with tempfile.TemporaryDirectory() as tmp:
lesson = load_lesson(Path(tmp), lesson_path)
lesson.connect_mcp("deploy")
_tools, handlers = lesson.assemble_tool_pool()

output = handlers["mcp__deploy__status"](
service="web", tool="trigger"
)

self.assertNotIn("Triggered", output)
self.assertIn("unexpected keyword argument 'tool'", output)

def test_integrated_permission_requires_approval_for_every_shell_command(self):
with tempfile.TemporaryDirectory() as tmp:
root = Path(tmp)
Expand Down