Skip to content

Commit cf1022f

Browse files
committed
isolate mcp stuff
restore comment remove old thing weird f the flakes grrrrrrr so annoying clean up name change
1 parent 76ab215 commit cf1022f

10 files changed

Lines changed: 128 additions & 168 deletions

File tree

docs/api-reference/marvin-agents-agent.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ T = TypeVar('T')
1919

2020
### `Agent`
2121
```python
22-
class Agent(name: str = lambda: random.choice(AGENT_NAMES)(), instructions: str | None = None, description: str | None = None, verbose: bool = False, prompt: str | Path = Path('agent.jinja'), tools: list[Callable[..., Any]] = lambda: [](), memories: list[Memory] = list(), mcp_servers: list[MCPServer] = list(), model: KnownModelName | Model | None = None, model_settings: ModelSettings = ModelSettings())
22+
class Agent(name: str = lambda: random.choice(AGENT_NAMES)(), instructions: str | None = None, description: str | None = None, verbose: bool = False, prompt: str | Path = Path('agent.jinja'), tools: list[Callable[..., Any]] = lambda: [](), memories: list[Memory] = lambda: [](), mcp_servers: list[MCPServer] = lambda: [](), model: KnownModelName | Model | None = None, model_settings: ModelSettings = ModelSettings())
2323
```
2424
An agent that can process tasks and maintain state.
2525

docs/api-reference/marvin-engine-orchestrator.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ class Orchestrator(tasks: list[Task[Any]], thread: Thread | str | None = None, h
2626
```
2727
- **`get_all_tasks`**
2828
```python
29-
def get_all_tasks(self, filter: Literal['incomplete', 'ready'] | None = None) -> list[Task[Any]]
29+
def get_all_tasks(self, _filter: Literal['incomplete', 'ready'] | None = None) -> list[Task[Any]]
3030
```
3131
Get all tasks, optionally filtered by status.
3232

examples/agent_mcp.py

Lines changed: 6 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import asyncio
2-
import os
32
from pathlib import Path
43

54
from pydantic_ai.mcp import MCPServerStdio
@@ -11,25 +10,23 @@
1110
run_python_server = MCPServerStdio(
1211
command="deno",
1312
args=["run", "-A", "jsr:@pydantic/mcp-run-python", "stdio"],
14-
env=dict(os.environ),
1513
)
1614

1715
# Requires uv: `uvx mcp-server-git`
1816
git_server = MCPServerStdio(
1917
command="uvx",
2018
args=["mcp-server-git"],
21-
env=dict(os.environ),
2219
)
2320

2421

2522
def write_summary_of_work(description: str, file_path: str) -> str:
26-
"""log your efforts"""
23+
"""log your efforts in your own style"""
2724
Path(file_path).write_text(description)
2825
return f"Summary written to {file_path}"
2926

3027

31-
git_agent = Agent(
32-
name="Git Agent",
28+
linus = Agent(
29+
name="Linus",
3330
instructions="Use the available tools as needed to accomplish the user's goal.",
3431
mcp_servers=[run_python_server, git_server],
3532
tools=[write_summary_of_work],
@@ -38,13 +35,12 @@ def write_summary_of_work(description: str, file_path: str) -> str:
3835

3936
async def main():
4037
task = (
41-
"Get the latest commit hash from this repository (path '.') and report how many characters long it is."
42-
" Finally, report the square root of that number and write a summary of your work to a file called 'summary.txt'"
38+
"Get the latest commit hash from this repo and report how many characters long it is."
39+
" Then, report the square root of that number and write a 'summary.txt' based on your work."
4340
)
4441
pprint(f"--- Running task: ---\n{task}\n" + "-" * 20)
45-
4642
pprint("\n--- Starting Agent Run ---")
47-
result = await git_agent.run_async(task)
43+
result = await linus.run_async(task)
4844
pprint("\n--- Final Result ---")
4945
pprint(result)
5046

pyproject.toml

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -45,13 +45,7 @@ dev = [
4545
[project.optional-dependencies]
4646
audio = ["pyaudio>=0.2.14"]
4747

48-
slackbot = [
49-
"pydantic-ai",
50-
"prefect",
51-
"numpy",
52-
"raggy",
53-
"turbopuffer==0.1.23",
54-
]
48+
slackbot = ["pydantic-ai", "prefect", "numpy", "raggy", "turbopuffer==0.1.23"]
5549

5650
[project.scripts]
5751
marvin = "marvin.cli.main:app"
@@ -69,6 +63,9 @@ asyncio_mode = "auto"
6963
asyncio_default_fixture_loop_scope = "session"
7064
timeout = 60
7165
testpaths = ["tests"]
66+
filterwarnings = [
67+
"ignore:.*search.*method is deprecated.*:DeprecationWarning:qdrant_client\\.async_qdrant_fastembed",
68+
]
7269
norecursedirs = [
7370
"*.egg-info",
7471
".git",
@@ -102,4 +99,4 @@ requires = ["hatchling>=1.21.0", "hatch-vcs>=0.4.0"]
10299
build-backend = "hatchling.build"
103100

104101
[tool.uv.workspace]
105-
members = ["examples/fastmcp"]
102+
members = ["examples/fastmcp"]

src/marvin/_internal/integrations/mcp.py

Lines changed: 81 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,16 @@
11
"""
2-
Module for integrating Model Context Protocol (MCP) servers with Marvin.
3-
4-
**Status:** Experimental
2+
Experimental module for integrating Model Context Protocol (MCP) servers with Marvin.
53
"""
64

75
import asyncio
6+
import os
87
import uuid
8+
from contextlib import AsyncExitStack, asynccontextmanager
99
from functools import partial
10-
from typing import TYPE_CHECKING, Any, Coroutine
10+
from typing import TYPE_CHECKING, Any, AsyncIterator, Coroutine
1111

1212
from mcp.types import CallToolResult
13-
from pydantic_ai.mcp import MCPServer
13+
from pydantic_ai.mcp import MCPServer, MCPServerStdio
1414
from pydantic_ai.messages import ToolCallPart, ToolReturnPart
1515
from pydantic_ai.tools import Tool, ToolDefinition
1616

@@ -20,6 +20,7 @@
2020
from marvin.utilities.logging import get_logger
2121

2222
if TYPE_CHECKING:
23+
import marvin.agents.agent
2324
import marvin.engine.orchestrator
2425

2526
logger = get_logger(__name__)
@@ -169,10 +170,9 @@ async def discover_mcp_tools(
169170
# Use a default argument to capture the current value of wrapped_func
170171
# This prevents the closure from capturing the loop variable by reference.
171172
async def async_wrapped_func_fixed(
172-
_bound_partial=wrapped_func, # No type hint here for Pydantic inspection
173+
_bound_partial=wrapped_func, # TODO: investigate type hinting here # pyright: ignore
173174
**kwargs: Any,
174175
) -> Any:
175-
# Call the captured partial, not the loop variable
176176
return await _bound_partial(**kwargs)
177177

178178
mcp_tools.append(
@@ -185,3 +185,77 @@ async def async_wrapped_func_fixed(
185185
)
186186

187187
return mcp_tools
188+
189+
190+
@asynccontextmanager
191+
async def manage_mcp_servers(
192+
actor: "marvin.agents.agent.Agent | Actor",
193+
) -> AsyncIterator[list["MCPServer"]]:
194+
"""Context manager to start and stop MCP servers for a given actor."""
195+
from marvin.agents.agent import Agent
196+
197+
logger.debug(f"[manage_mcp_servers] Preparing MCP servers for {actor.name}...")
198+
mcp_exit_stack = AsyncExitStack()
199+
active_servers: list["MCPServer"] = []
200+
servers_started = False
201+
202+
if not isinstance(actor, Agent) or not hasattr(actor, "get_mcp_servers"):
203+
logger.debug(
204+
f"[manage_mcp_servers] Actor {actor.name} is not an Agent or does not have get_mcp_servers method."
205+
)
206+
yield active_servers
207+
return
208+
209+
servers_to_manage = actor.get_mcp_servers()
210+
211+
if not servers_to_manage:
212+
logger.debug(
213+
f"[manage_mcp_servers] Actor {actor.name} has no configured MCP servers."
214+
)
215+
yield active_servers
216+
return
217+
218+
logger.debug(
219+
f"[manage_mcp_servers] Found {len(servers_to_manage)} server configurations."
220+
)
221+
for i, server in enumerate(servers_to_manage):
222+
logger.debug(f"[manage_mcp_servers] Processing server #{i + 1}: {server!r}")
223+
try:
224+
# Set environment variables for stdio servers if not already set
225+
if isinstance(server, MCPServerStdio) and server.env is None:
226+
logger.debug(
227+
f"[manage_mcp_servers] Server #{i + 1} is MCPServerStdio with no env set. Setting env=dict(os.environ)."
228+
)
229+
server.env = dict(os.environ)
230+
231+
logger.debug(
232+
f"[manage_mcp_servers] Entering context for server #{i + 1}..."
233+
)
234+
await mcp_exit_stack.enter_async_context(server)
235+
active_servers.append(server)
236+
logger.debug(
237+
f"[manage_mcp_servers] Context entered successfully for server #{i + 1}."
238+
)
239+
servers_started = True
240+
except Exception as e:
241+
logger.error(
242+
f"[manage_mcp_servers] Failed to start MCP server #{i + 1} ({server!r}): {e}",
243+
exc_info=True,
244+
)
245+
# Optionally re-raise or handle specific errors? For now, just log.
246+
247+
if servers_started:
248+
logger.debug(
249+
f"[manage_mcp_servers] Yielding control with {len(active_servers)} active servers."
250+
)
251+
else:
252+
logger.debug(
253+
"[manage_mcp_servers] No servers were successfully started, yielding control."
254+
)
255+
256+
try:
257+
yield active_servers
258+
finally:
259+
logger.debug("[manage_mcp_servers] Cleaning up MCP servers...")
260+
await mcp_exit_stack.aclose()
261+
logger.debug("[manage_mcp_servers] MCP server cleanup complete.")

src/marvin/agents/agent.py

Lines changed: 8 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -61,12 +61,12 @@ class Agent(Actor):
6161
)
6262

6363
memories: list[Memory] = field(
64-
default_factory=list,
64+
default_factory=lambda: [],
6565
metadata={"description": "List of memory modules available to the agent"},
6666
)
6767

6868
mcp_servers: list["MCPServer"] = field(
69-
default_factory=list,
69+
default_factory=lambda: [],
7070
metadata={"description": "List of MCP servers available to the agent"},
7171
repr=False,
7272
)
@@ -157,25 +157,19 @@ async def get_agentlet(
157157
else:
158158
logger.warning(f"Ignoring non-callable, non-EndTurn item: {item}")
159159

160-
# --- Wrap standard Marvin tools --- #
161160
unique_marvin_tools = [wrap_tool_errors(tool) for tool in marvin_tool_callables]
162161

163-
# --- Discover MCP tools --- #
164-
mcp_tools_instances: list[Tool] = []
162+
mcp_tool_instances: list[Tool] = []
165163
if active_mcp_servers:
166164
orchestrator = marvin.engine.orchestrator.get_current_orchestrator()
167-
mcp_tools_instances = await discover_mcp_tools(
165+
mcp_tool_instances = await discover_mcp_tools(
168166
mcp_servers=active_mcp_servers,
169167
actor=self,
170168
orchestrator=orchestrator,
171169
)
172170

173-
# --- Combine standard tools for 'tools' arg --- #
174-
combined_standard_tools: list[Callable | Tool] = (
175-
unique_marvin_tools + mcp_tools_instances
176-
)
171+
combined_tools: list[Any] = unique_marvin_tools + mcp_tool_instances
177172

178-
# --- Determine EndTurn ToolOutput --- #
179173
tool_output_name = "EndTurn"
180174
tool_output_description = "Ends the current turn."
181175
if len(final_end_turn_defs) == 1:
@@ -199,27 +193,22 @@ async def get_agentlet(
199193
description=tool_output_description,
200194
)
201195

202-
# --- Create the pydantic_ai.Agent --- #
203196
agent_kwargs = {
204197
"model": self.get_model(),
205198
"model_settings": self.get_model_settings(),
206199
"output_type": final_tool_output, # Use the constructed ToolOutput
207200
"name": self.name,
208-
# "tools": combined_standard_tools, # Add conditionally below
209-
# "mcp_servers": active_mcp_servers, # Add conditionally below
210201
}
211202

212-
# Conditionally add standard tools
213-
if combined_standard_tools:
214-
agent_kwargs["tools"] = combined_standard_tools
203+
if combined_tools:
204+
agent_kwargs["tools"] = combined_tools
215205

216-
# Conditionally add mcp_servers
217206
if active_mcp_servers:
218207
agent_kwargs["mcp_servers"] = active_mcp_servers
219208

220209
agentlet = pydantic_ai.Agent[Any, Any](**agent_kwargs)
221210

222-
# --- Assign Marvin-specific attributes for internal use --- #
211+
# for internal use
223212
agentlet._marvin_tools = unique_marvin_tools # Store wrapped callables
224213
agentlet._marvin_end_turn_tools = final_end_turn_defs # Store original defs
225214

0 commit comments

Comments
 (0)