Skip to content

Commit 37f86ad

Browse files
krassermclaude
andauthored
Revert dependency upgrade and MCP API migration (#95) (#100)
Fully revert #95, restoring the pre-#95 pyproject.toml, source (agent/core.py, tools/utils.py, terminal/screens.py), tests, and the pre-#95 uv.lock. This pins dependencies back to their pre-#95 versions (pydantic-ai 1.99.0, textual 8.0.0, pytest-asyncio 1.3.0, google-genai 2.4.0, rich 14.3.2, etc.), where the original MCPServerStdio/MCPServerStreamableHTTP API, the file picker, and the synchronous approval test all work without the adaptations #95 introduced. #97's local-HTTP-server fetch tests (added after #95) are preserved. This reverts commit 1244357. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 50d2781 commit 37f86ad

9 files changed

Lines changed: 1361 additions & 1464 deletions

File tree

freeact/agent/core.py

Lines changed: 32 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -2,18 +2,18 @@
22
import contextlib
33
import logging
44
import uuid
5-
from collections.abc import Sequence
5+
from collections.abc import Sequence, Set
66
from dataclasses import replace
77
from pathlib import Path
8-
from typing import AsyncIterator
8+
from typing import Any, AsyncIterator
99

1010
import ipybox
1111
from aiostream.stream import merge
12-
from fastmcp.client.transports import StdioTransport, StreamableHttpTransport
1312
from ipybox.utils import arun
13+
from mcp import types as mcp_types
1414
from pydantic_ai import BinaryContent
1515
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
1717
from pydantic_ai.messages import (
1818
ModelMessage,
1919
ModelRequest,
@@ -51,7 +51,6 @@
5151
from freeact.agent.shell import split_composite_command
5252
from freeact.agent.store import SessionStore, ToolResultMaterializer
5353
from freeact.tools.utils import (
54-
_McpServer,
5554
get_tool_definitions,
5655
load_ipybox_tool_definitions,
5756
load_subagent_task_tool_definitions,
@@ -60,6 +59,18 @@
6059
logger = logging.getLogger("freeact")
6160

6261

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+
6374
class Agent:
6475
"""Code action agent that executes Python code and shell commands.
6576
@@ -136,9 +147,9 @@ def __init__(
136147
)
137148

138149
self._mcp_servers = config.resolved_mcp_servers
139-
self._mcp_server_instances: dict[str, _McpServer] = {}
150+
self._mcp_server_instances: dict[str, MCPServer] = {}
140151

141-
self._tool_mapping: dict[str, _McpServer] = {}
152+
self._tool_mapping: dict[str, MCPServer] = {}
142153
self._tool_definitions: list[ToolDefinition] = []
143154

144155
self._kernel_env = config.resolved_kernel_env
@@ -217,6 +228,7 @@ async def start(self) -> None:
217228
resource_supervisors = [_ResourceSupervisor(self._code_executor, "code-executor")]
218229
for name, server in self._mcp_server_instances.items():
219230
logger.debug(f"Starting MCP server: {name}")
231+
server.tool_prefix = name
220232
resource_supervisors.append(_ResourceSupervisor(server, f"mcp-server-{name}"))
221233

222234
try:
@@ -269,27 +281,28 @@ async def stop(self) -> None:
269281
raise ExceptionGroup("Multiple errors while stopping agent resources", errors)
270282
self._mcp_server_instances = {}
271283

272-
def _create_mcp_servers(self) -> dict[str, _McpServer]:
284+
def _create_mcp_servers(self) -> dict[str, MCPServer]:
273285
if not self._mcp_servers:
274286
return {}
275287

276-
servers: dict[str, _McpServer] = {}
288+
servers: dict[str, MCPServer] = {}
277289

278290
for name, raw_cfg in self._mcp_servers.items():
279291
cfg = dict(raw_cfg)
280-
excluded_tools = frozenset(cfg.pop("excluded_tools", None) or ())
292+
excluded_tools = cfg.pop("excluded_tools", None)
281293
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)
290304
case _:
291305
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)
293306

294307
return servers
295308

freeact/terminal/screens.py

Lines changed: 0 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -88,20 +88,6 @@ def action_collapse_cursor_node(self) -> None:
8888
if self._safe_is_dir(dir_entry.path):
8989
cursor_node.collapse()
9090

91-
async def watch_path(self) -> None:
92-
"""Suppress the reactive reload that races with cursor positioning.
93-
94-
`DirectoryTree.watch_path` fires when the root path is set during
95-
construction and reloads the tree, rebuilding the root's children and
96-
resetting the cursor to the root. That runs concurrently with
97-
`FilePickerScreen._focus_tree_path`, which loads and expands the path
98-
down to the working directory and positions the cursor there; the
99-
reactive reload orphans the navigated nodes, leaving the cursor stuck
100-
at the root. The picker keeps a fixed root, so loading and cursor
101-
positioning are driven entirely by the screen's `on_mount`.
102-
"""
103-
return None
104-
10591

10692
class FilePickerScreen(ModalScreen[Path | None]):
10793
"""Modal file picker opened from `@path` prompt completion."""
@@ -253,10 +239,6 @@ async def _focus_tree_path(self, tree: FilePickerTree, target_path: Path) -> Non
253239
current_path = next_path
254240
if child.allow_expand:
255241
await tree.reload_node(child)
256-
# Build the tree's line layout so the target node's line index is
257-
# assigned; otherwise `node._line` is still -1 and `move_cursor` falls
258-
# back to the root (line 0).
259-
_ = tree._tree_lines
260242
tree.move_cursor(node, animate=False)
261243

262244

freeact/tools/utils.py

Lines changed: 7 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -1,64 +1,18 @@
11
import asyncio
22
import json
3-
from collections.abc import Set
43
from dataclasses import asdict
54
from pathlib import Path
6-
from typing import Any
75

8-
from fastmcp.client.transports import ClientTransport, StdioTransport
96
from ipybox.utils import arun
10-
from pydantic_ai import RunContext
11-
from pydantic_ai.mcp import MCPToolset
7+
from pydantic_ai.mcp import MCPServer, MCPServerStdio
128
from pydantic_ai.tools import ToolDefinition
13-
from pydantic_ai.toolsets import FilteredToolset, PrefixedToolset
14-
from pydantic_ai.toolsets.abstract import AbstractToolset, ToolsetTool
159

1610
IPYBOX_TOOL_PREFIX = "ipybox"
1711
IPYBOX_TOOL_DEFS_PATH = Path(__file__).parent / "ipybox.json"
1812
SUBAGENT_TOOL_DEFS_PATH = Path(__file__).parent / "subagent.json"
1913

2014

21-
class _McpServer:
22-
"""MCP server adapter that namespaces and filters an `MCPToolset`.
23-
24-
Wraps a pydantic-ai [`MCPToolset`][pydantic_ai.mcp.MCPToolset] so that tool
25-
names exposed via `get_tools` are prefixed with `tool_prefix` (avoiding
26-
collisions between servers) and optionally excludes selected tools. Tool
27-
calls route to the underlying toolset via `direct_call_tool` using the
28-
un-prefixed tool name.
29-
"""
30-
31-
def __init__(
32-
self,
33-
transport: ClientTransport,
34-
*,
35-
tool_prefix: str,
36-
excluded_tools: Set[str] = frozenset(),
37-
) -> None:
38-
self.tool_prefix = tool_prefix
39-
self._toolset = MCPToolset(transport)
40-
41-
view: AbstractToolset[Any] = self._toolset
42-
if excluded_tools:
43-
excluded = frozenset(excluded_tools)
44-
view = FilteredToolset(view, lambda ctx, tool_def: tool_def.name not in excluded)
45-
self._view: AbstractToolset[Any] = PrefixedToolset(view, tool_prefix)
46-
47-
async def __aenter__(self) -> "_McpServer":
48-
await self._view.__aenter__()
49-
return self
50-
51-
async def __aexit__(self, *exc_info: object) -> bool | None:
52-
return await self._view.__aexit__(*exc_info)
53-
54-
async def get_tools(self, ctx: RunContext[Any]) -> dict[str, ToolsetTool[Any]]:
55-
return await self._view.get_tools(ctx)
56-
57-
async def direct_call_tool(self, name: str, args: dict[str, Any]) -> Any:
58-
return await self._toolset.direct_call_tool(name=name, args=args)
59-
60-
61-
async def get_tool_definitions(server: _McpServer) -> list[ToolDefinition]:
15+
async def get_tool_definitions(server: MCPServer) -> list[ToolDefinition]:
6216
"""Extract tool definitions from an MCP server.
6317
6418
Args:
@@ -67,10 +21,11 @@ async def get_tool_definitions(server: _McpServer) -> list[ToolDefinition]:
6721
Returns:
6822
List of tool definitions exposed by the server.
6923
"""
24+
from pydantic_ai import RunContext
7025
from pydantic_ai.models.test import TestModel
7126
from pydantic_ai.result import RunUsage
7227

73-
ctx: RunContext[Any] = RunContext(
28+
ctx = RunContext(
7429
deps=None,
7530
model=TestModel(),
7631
usage=RunUsage(),
@@ -120,9 +75,9 @@ async def save_ipybox_tool_definitions() -> None:
12075
Connects to a live ipybox MCP server, extracts tool definitions
12176
(excluding `install_package`), and saves them to the bundled JSON file.
12277
"""
123-
transport = StdioTransport(command="uvx", args=["ipybox"])
124-
server = _McpServer(transport, tool_prefix=IPYBOX_TOOL_PREFIX, excluded_tools={"install_package"})
125-
async with server:
78+
server = MCPServerStdio("uvx", args=["ipybox"], tool_prefix=IPYBOX_TOOL_PREFIX)
79+
server = server.filtered(lambda _, t: t.name != f"{IPYBOX_TOOL_PREFIX}_install_package")
80+
async with server as server:
12681
tool_defs = await get_tool_definitions(server)
12782
await arun(save_tool_definitions, tool_defs, IPYBOX_TOOL_DEFS_PATH)
12883

pyproject.toml

Lines changed: 19 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -10,17 +10,17 @@ readme = "README.md"
1010
license = "Apache-2.0"
1111
dependencies = [
1212
"aiostream>=0.7.1",
13-
"google-genai>=2.7.0",
14-
"pydantic-ai>=1.104.0",
15-
"python-dotenv>=1.2.2",
16-
"pillow>=12.2.0",
13+
"google-genai>=2.4.0",
14+
"pydantic-ai>=1.99.0",
15+
"python-dotenv>=1.0.0",
16+
"pillow>=12.0.0",
1717
"pyyaml>=6.0.3",
18-
"rich>=15.0.0",
19-
"textual>=8.2.7",
18+
"rich>=14.2.0",
19+
"textual>=8.0.0",
2020
"ipybox>=0.9.1",
2121
"mcpygen>=0.1.4",
22-
"sqlite-vec>=0.1.9",
23-
"watchfiles>=1.2.0",
22+
"sqlite-vec>=0.1.0",
23+
"watchfiles>=1.0.0",
2424
"trafilatura>=2.0.0",
2525
]
2626

@@ -35,21 +35,21 @@ default-groups = [
3535

3636
[dependency-groups]
3737
docs = [
38-
"griffe-pydantic>=1.3.1",
38+
"griffe-pydantic>=1.1.8",
3939
"mkdocs>=1.6.1,<2",
40-
"mkdocs-material>=9.7.6,<10",
41-
"mkdocstrings-python>=2.0.3,<3",
42-
"mkdocs-llmstxt>=0.5.0",
43-
"click>=8.4.1",
40+
"mkdocs-material>=9.5.48,<10",
41+
"mkdocstrings-python>=1.12.2,<2",
42+
"mkdocs-llmstxt>=0.4.0",
43+
"click<=8.2.1",
4444
]
4545
dev = [
46-
"invoke>=3.0.3,<4",
47-
"pre-commit>=4.6.0,<5",
48-
"pytest>=9.0.3,<10",
49-
"pytest-asyncio>=1.4.0,<2",
50-
"pytest-cov>=7.1.0,<8",
46+
"invoke>=2.2,<3",
47+
"pre-commit>=4.3.0,<5",
48+
"pytest>=8.4.2,<9",
49+
"pytest-asyncio>=1.2.0,<2",
50+
"pytest-cov>=4.1.0,<5",
5151
"pytest-xdist>=3.8.0",
52-
"types-pyyaml>=6.0.12.20260518",
52+
"types-pyyaml>=6.0.12.20250915",
5353
]
5454

5555
[build-system]

0 commit comments

Comments
 (0)