|
2 | 2 | import contextlib |
3 | 3 | import logging |
4 | 4 | import uuid |
5 | | -from collections.abc import Sequence |
| 5 | +from collections.abc import Sequence, Set |
6 | 6 | from dataclasses import replace |
7 | 7 | from pathlib import Path |
8 | | -from typing import AsyncIterator |
| 8 | +from typing import Any, AsyncIterator |
9 | 9 |
|
10 | 10 | import ipybox |
11 | 11 | from aiostream.stream import merge |
12 | | -from fastmcp.client.transports import StdioTransport, StreamableHttpTransport |
13 | 12 | from ipybox.utils import arun |
| 13 | +from mcp import types as mcp_types |
14 | 14 | from pydantic_ai import BinaryContent |
15 | 15 | from pydantic_ai.direct import model_request_stream |
16 | | -from pydantic_ai.mcp import ToolResult |
| 16 | +from pydantic_ai.mcp import MCPServer, MCPServerStdio, MCPServerStreamableHTTP, ToolResult |
17 | 17 | from pydantic_ai.messages import ( |
18 | 18 | ModelMessage, |
19 | 19 | ModelRequest, |
|
51 | 51 | from freeact.agent.shell import split_composite_command |
52 | 52 | from freeact.agent.store import SessionStore, ToolResultMaterializer |
53 | 53 | from freeact.tools.utils import ( |
54 | | - _McpServer, |
55 | 54 | get_tool_definitions, |
56 | 55 | load_ipybox_tool_definitions, |
57 | 56 | load_subagent_task_tool_definitions, |
|
60 | 59 | logger = logging.getLogger("freeact") |
61 | 60 |
|
62 | 61 |
|
| 62 | +class _MCPServerStdioFiltered(MCPServerStdio): |
| 63 | + """MCPServerStdio that filters out specified tools.""" |
| 64 | + |
| 65 | + def __init__(self, excluded_tools: Set[str], **kwargs: Any): |
| 66 | + super().__init__(**kwargs) |
| 67 | + self._excluded_tools = excluded_tools |
| 68 | + |
| 69 | + async def list_tools(self) -> list[mcp_types.Tool]: |
| 70 | + tools = await super().list_tools() |
| 71 | + return [t for t in tools if t.name not in self._excluded_tools] |
| 72 | + |
| 73 | + |
63 | 74 | class Agent: |
64 | 75 | """Code action agent that executes Python code and shell commands. |
65 | 76 |
|
@@ -136,9 +147,9 @@ def __init__( |
136 | 147 | ) |
137 | 148 |
|
138 | 149 | self._mcp_servers = config.resolved_mcp_servers |
139 | | - self._mcp_server_instances: dict[str, _McpServer] = {} |
| 150 | + self._mcp_server_instances: dict[str, MCPServer] = {} |
140 | 151 |
|
141 | | - self._tool_mapping: dict[str, _McpServer] = {} |
| 152 | + self._tool_mapping: dict[str, MCPServer] = {} |
142 | 153 | self._tool_definitions: list[ToolDefinition] = [] |
143 | 154 |
|
144 | 155 | self._kernel_env = config.resolved_kernel_env |
@@ -217,6 +228,7 @@ async def start(self) -> None: |
217 | 228 | resource_supervisors = [_ResourceSupervisor(self._code_executor, "code-executor")] |
218 | 229 | for name, server in self._mcp_server_instances.items(): |
219 | 230 | logger.debug(f"Starting MCP server: {name}") |
| 231 | + server.tool_prefix = name |
220 | 232 | resource_supervisors.append(_ResourceSupervisor(server, f"mcp-server-{name}")) |
221 | 233 |
|
222 | 234 | try: |
@@ -269,27 +281,28 @@ async def stop(self) -> None: |
269 | 281 | raise ExceptionGroup("Multiple errors while stopping agent resources", errors) |
270 | 282 | self._mcp_server_instances = {} |
271 | 283 |
|
272 | | - def _create_mcp_servers(self) -> dict[str, _McpServer]: |
| 284 | + def _create_mcp_servers(self) -> dict[str, MCPServer]: |
273 | 285 | if not self._mcp_servers: |
274 | 286 | return {} |
275 | 287 |
|
276 | | - servers: dict[str, _McpServer] = {} |
| 288 | + servers: dict[str, MCPServer] = {} |
277 | 289 |
|
278 | 290 | for name, raw_cfg in self._mcp_servers.items(): |
279 | 291 | cfg = dict(raw_cfg) |
280 | | - excluded_tools = frozenset(cfg.pop("excluded_tools", None) or ()) |
| 292 | + excluded_tools = cfg.pop("excluded_tools", None) |
281 | 293 | match cfg: |
282 | | - case {"command": command}: |
283 | | - transport: StdioTransport | StreamableHttpTransport = StdioTransport( |
284 | | - command=command, |
285 | | - args=cfg.get("args", []), |
286 | | - env=cfg.get("env"), |
287 | | - ) |
288 | | - case {"url": url}: |
289 | | - transport = StreamableHttpTransport(url=url, headers=cfg.get("headers")) |
| 294 | + case {"command": _}: |
| 295 | + if excluded_tools: |
| 296 | + servers[name] = _MCPServerStdioFiltered( |
| 297 | + excluded_tools=frozenset(excluded_tools), |
| 298 | + **cfg, |
| 299 | + ) |
| 300 | + else: |
| 301 | + servers[name] = MCPServerStdio(**cfg) |
| 302 | + case {"url": _}: |
| 303 | + servers[name] = MCPServerStreamableHTTP(**cfg) |
290 | 304 | case _: |
291 | 305 | raise ValueError(f"Invalid server config for {name}: must have 'command' or 'url'") |
292 | | - servers[name] = _McpServer(transport, tool_prefix=name, excluded_tools=excluded_tools) |
293 | 306 |
|
294 | 307 | return servers |
295 | 308 |
|
|
0 commit comments