From d6d5499416fe8a40b8b36353350c26e7d974d682 Mon Sep 17 00:00:00 2001 From: Xingyao Wang Date: Sun, 8 Jun 2025 18:03:18 -0400 Subject: [PATCH] refactor(MCP): Replace `MCPRouter` with FastMCP Proxy (#8877) Co-authored-by: openhands --- .github/dependabot.yml | 1 - openhands/runtime/action_execution_server.py | 152 ++++++----------- .../action_execution_client.py | 2 +- openhands/runtime/mcp/config.json | 5 +- openhands/runtime/mcp/proxy/README.md | 71 ++++++++ openhands/runtime/mcp/proxy/__init__.py | 7 + openhands/runtime/mcp/proxy/manager.py | 138 ++++++++++++++++ poetry.lock | 156 +----------------- pyproject.toml | 1 - tests/runtime/test_mcp_action.py | 18 +- 10 files changed, 289 insertions(+), 262 deletions(-) create mode 100644 openhands/runtime/mcp/proxy/README.md create mode 100644 openhands/runtime/mcp/proxy/__init__.py create mode 100644 openhands/runtime/mcp/proxy/manager.py diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 7ae75e5568..5d76d8fd9f 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -16,7 +16,6 @@ updates: mcp-packages: patterns: - "mcp" - - "mcpm" security-all: applies-to: "security-updates" patterns: diff --git a/openhands/runtime/action_execution_server.py b/openhands/runtime/action_execution_server.py index 2be93939ce..2b628339b5 100644 --- a/openhands/runtime/action_execution_server.py +++ b/openhands/runtime/action_execution_server.py @@ -9,7 +9,6 @@ import argparse import asyncio import base64 import json -import logging import mimetypes import os import shutil @@ -26,8 +25,6 @@ from fastapi import Depends, FastAPI, HTTPException, Request, UploadFile from fastapi.exceptions import RequestValidationError from fastapi.responses import FileResponse, JSONResponse from fastapi.security import APIKeyHeader -from mcpm import MCPRouter, RouterConfig -from mcpm.router.router import logger as mcp_router_logger from openhands_aci.editor.editor import OHEditor from openhands_aci.editor.exceptions import ToolError from openhands_aci.editor.results import ToolResult @@ -37,6 +34,7 @@ from starlette.background import BackgroundTask from starlette.exceptions import HTTPException as StarletteHTTPException from uvicorn import run +from openhands.core.config.mcp_config import MCPStdioServerConfig from openhands.core.exceptions import BrowserUnavailableException from openhands.core.logger import openhands_logger as logger from openhands.events.action import ( @@ -63,20 +61,18 @@ from openhands.events.serialization import event_from_dict, event_to_dict from openhands.runtime.browser import browse from openhands.runtime.browser.browser_env import BrowserEnv from openhands.runtime.file_viewer_server import start_file_viewer_server + +# Import our custom MCP Proxy Manager +from openhands.runtime.mcp.proxy import MCPProxyManager from openhands.runtime.plugins import ALL_PLUGINS, JupyterPlugin, Plugin, VSCodePlugin from openhands.runtime.utils import find_available_tcp_port from openhands.runtime.utils.bash import BashSession from openhands.runtime.utils.files import insert_lines, read_lines -from openhands.runtime.utils.log_capture import capture_logs from openhands.runtime.utils.memory_monitor import MemoryMonitor from openhands.runtime.utils.runtime_init import init_user_and_working_directory from openhands.runtime.utils.system_stats import get_system_stats from openhands.utils.async_utils import call_sync_from_async, wait_all -# Set MCP router logger to the same level as the main logger -mcp_router_logger.setLevel(logger.getEffectiveLevel()) - - if sys.platform == 'win32': from openhands.runtime.utils.windows_bash import WindowsPowershellSession @@ -471,7 +467,7 @@ class ActionExecutor: filepath = self._resolve_path(action.path, working_dir) try: if filepath.lower().endswith(('.png', '.jpg', '.jpeg', '.bmp', '.gif')): - with open(filepath, 'rb') as file: # noqa: ASYNC101 + with open(filepath, 'rb') as file: image_data = file.read() encoded_image = base64.b64encode(image_data).decode('utf-8') mime_type, _ = mimetypes.guess_type(filepath) @@ -481,13 +477,13 @@ class ActionExecutor: return FileReadObservation(path=filepath, content=encoded_image) elif filepath.lower().endswith('.pdf'): - with open(filepath, 'rb') as file: # noqa: ASYNC101 + with open(filepath, 'rb') as file: pdf_data = file.read() encoded_pdf = base64.b64encode(pdf_data).decode('utf-8') encoded_pdf = f'data:application/pdf;base64,{encoded_pdf}' return FileReadObservation(path=filepath, content=encoded_pdf) elif filepath.lower().endswith(('.mp4', '.webm', '.ogg')): - with open(filepath, 'rb') as file: # noqa: ASYNC101 + with open(filepath, 'rb') as file: video_data = file.read() encoded_video = base64.b64encode(video_data).decode('utf-8') mime_type, _ = mimetypes.guess_type(filepath) @@ -497,7 +493,7 @@ class ActionExecutor: return FileReadObservation(path=filepath, content=encoded_video) - with open(filepath, 'r', encoding='utf-8') as file: # noqa: ASYNC101 + with open(filepath, 'r', encoding='utf-8') as file: lines = read_lines(file.readlines(), action.start, action.end) except FileNotFoundError: return ErrorObservation( @@ -530,7 +526,7 @@ class ActionExecutor: mode = 'w' if not file_exists else 'r+' try: - with open(filepath, mode, encoding='utf-8') as file: # noqa: ASYNC101 + with open(filepath, mode, encoding='utf-8') as file: if mode != 'w': all_lines = file.readlines() new_file = insert_lines(insert, all_lines, action.start, action.end) @@ -654,14 +650,11 @@ if __name__ == '__main__': plugins_to_load.append(ALL_PLUGINS[plugin]()) # type: ignore client: ActionExecutor | None = None - mcp_router: MCPRouter | None = None - MCP_ROUTER_PROFILE_PATH = os.path.join( - os.path.dirname(__file__), 'mcp', 'config.json' - ) + mcp_proxy_manager: MCPProxyManager | None = None @asynccontextmanager async def lifespan(app: FastAPI): - global client, mcp_router + global client, mcp_proxy_manager logger.info('Initializing ActionExecutor...') client = ActionExecutor( plugins_to_load, @@ -676,63 +669,36 @@ if __name__ == '__main__': # Check if we're on Windows is_windows = sys.platform == 'win32' - # Initialize and mount MCP Router (skip on Windows) + # Initialize and mount MCP Proxy Manager (skip on Windows) if is_windows: - logger.info('Skipping MCP Router initialization on Windows') - mcp_router = None + logger.info('Skipping MCP Proxy initialization on Windows') + mcp_proxy_manager = None else: - logger.info('Initializing MCP Router...') - mcp_router = MCPRouter( - profile_path=MCP_ROUTER_PROFILE_PATH, - router_config=RouterConfig( - api_key=SESSION_API_KEY, - auth_enabled=bool(SESSION_API_KEY), - ), + logger.info('Initializing MCP Proxy Manager...') + # Create a MCP Proxy Manager + mcp_proxy_manager = MCPProxyManager( + auth_enabled=bool(SESSION_API_KEY), + api_key=SESSION_API_KEY, + logger_level=logger.getEffectiveLevel(), ) + mcp_proxy_manager.initialize() + # Mount the proxy to the app allowed_origins = ['*'] - sse_app = await mcp_router.get_sse_server_app( - allow_origins=allowed_origins, include_lifespan=False - ) - - # Only mount SSE app if MCP Router is initialized (not on Windows) - if mcp_router is not None: - # Check for route conflicts before mounting - main_app_routes = {route.path for route in app.routes} - sse_app_routes = {route.path for route in sse_app.routes} - conflicting_routes = main_app_routes.intersection(sse_app_routes) - - if conflicting_routes: - logger.error(f'Route conflicts detected: {conflicting_routes}') - raise RuntimeError( - f'Cannot mount SSE app - conflicting routes found: {conflicting_routes}' - ) - - app.mount('/', sse_app) - logger.info( - f'Mounted MCP Router SSE app at root path with allowed origins: {allowed_origins}' - ) - - # Additional debug logging - if logger.isEnabledFor(logging.DEBUG): - logger.debug('Main app routes:') - for route in main_app_routes: - logger.debug(f' {route}') - logger.debug('MCP SSE server app routes:') - for route in sse_app_routes: - logger.debug(f' {route}') + try: + await mcp_proxy_manager.mount_to_app(app, allowed_origins) + except Exception as e: + logger.error(f'Error mounting MCP Proxy: {e}', exc_info=True) + raise RuntimeError(f'Cannot mount MCP Proxy: {e}') yield # Clean up & release the resources - logger.info('Shutting down MCP Router...') - if mcp_router: - try: - await mcp_router.shutdown() - logger.info('MCP Router shutdown successfully.') - except Exception as e: - logger.error(f'Error shutting down MCP Router: {e}', exc_info=True) + logger.info('Shutting down MCP Proxy Manager...') + if mcp_proxy_manager: + del mcp_proxy_manager + mcp_proxy_manager = None else: - logger.info('MCP Router instance not found for shutdown.') + logger.info('MCP Proxy Manager instance not found for shutdown.') logger.info('Closing ActionExecutor...') if client: @@ -824,6 +790,9 @@ if __name__ == '__main__': # Check if we're on Windows is_windows = sys.platform == 'win32' + # Access the global mcp_proxy_manager variable + global mcp_proxy_manager + if is_windows: # On Windows, just return a success response without doing anything logger.info( @@ -838,17 +807,10 @@ if __name__ == '__main__': ) # Non-Windows implementation - assert mcp_router is not None - assert os.path.exists(MCP_ROUTER_PROFILE_PATH) - - # Use synchronous file operations outside of async function - def read_profile(): - with open(MCP_ROUTER_PROFILE_PATH, 'r') as f: - return json.load(f) - - current_profile = read_profile() - assert 'default' in current_profile - assert isinstance(current_profile['default'], list) + if mcp_proxy_manager is None: + raise HTTPException( + status_code=500, detail='MCP Proxy Manager is not initialized' + ) # Get the request body mcp_tools_to_sync = await request.json() @@ -856,31 +818,17 @@ if __name__ == '__main__': raise HTTPException( status_code=400, detail='Request must be a list of MCP tools to sync' ) - logger.info( - f'Updating MCP server to: {json.dumps(mcp_tools_to_sync, indent=2)}.\nPrevious profile: {json.dumps(current_profile, indent=2)}' + f'Updating MCP server with tools: {json.dumps(mcp_tools_to_sync, indent=2)}' ) - current_profile['default'] = mcp_tools_to_sync - - # Use synchronous file operations outside of async function - def write_profile(profile): - with open(MCP_ROUTER_PROFILE_PATH, 'w') as f: - json.dump(profile, f) - - write_profile(current_profile) - - # Manually reload the profile and update the servers - mcp_router.profile_manager.reload() - servers_wait_for_update = mcp_router.get_unique_servers() - async with capture_logs('mcpm.router.router') as log_capture: - await mcp_router.update_servers(servers_wait_for_update) - router_error_log = log_capture.getvalue() - - logger.info( - f'MCP router updated successfully with unique servers: {servers_wait_for_update}' - ) - if router_error_log: - logger.warning(f'Some MCP servers failed to be added: {router_error_log}') + mcp_tools_to_sync = [MCPStdioServerConfig(**tool) for tool in mcp_tools_to_sync] + try: + await mcp_proxy_manager.update_and_remount(app, mcp_tools_to_sync, ['*']) + logger.info('MCP Proxy Manager updated and remounted successfully') + router_error_log = '' + except Exception as e: + logger.error(f'Error updating MCP Proxy Manager: {e}', exc_info=True) + router_error_log = str(e) return JSONResponse( status_code=200, @@ -915,7 +863,7 @@ if __name__ == '__main__': ) zip_path = os.path.join(full_dest_path, file.filename) - with open(zip_path, 'wb') as buffer: # noqa: ASYNC101 + with open(zip_path, 'wb') as buffer: shutil.copyfileobj(file.file, buffer) # Extract the zip file @@ -928,7 +876,7 @@ if __name__ == '__main__': else: # For single file uploads file_path = os.path.join(full_dest_path, file.filename) - with open(file_path, 'wb') as buffer: # noqa: ASYNC101 + with open(file_path, 'wb') as buffer: shutil.copyfileobj(file.file, buffer) logger.debug(f'Uploaded file {file.filename} to {destination}') diff --git a/openhands/runtime/impl/action_execution/action_execution_client.py b/openhands/runtime/impl/action_execution/action_execution_client.py index ef72fcdab7..018e338636 100644 --- a/openhands/runtime/impl/action_execution/action_execution_client.py +++ b/openhands/runtime/impl/action_execution/action_execution_client.py @@ -435,7 +435,7 @@ class ActionExecutionClient(Runtime): # We should always include the runtime as an MCP server whenever there's > 0 stdio servers updated_mcp_config.sse_servers.append( MCPSSEServerConfig( - url=self.action_execution_server_url.rstrip('/') + '/sse', + url=self.action_execution_server_url.rstrip('/') + '/mcp/sse', api_key=self.session_api_key, ) ) diff --git a/openhands/runtime/mcp/config.json b/openhands/runtime/mcp/config.json index 6bb5967d80..3fdb1a3c11 100644 --- a/openhands/runtime/mcp/config.json +++ b/openhands/runtime/mcp/config.json @@ -1,3 +1,6 @@ { - "default": [] + "mcpServers": { + "default": {} + }, + "tools": [] } diff --git a/openhands/runtime/mcp/proxy/README.md b/openhands/runtime/mcp/proxy/README.md new file mode 100644 index 0000000000..b42fced9c4 --- /dev/null +++ b/openhands/runtime/mcp/proxy/README.md @@ -0,0 +1,71 @@ +# MCP Proxy Manager + +This module provides a manager class for handling FastMCP proxy instances in OpenHands, including initialization, configuration, and mounting to FastAPI applications. + +## Overview + +The `MCPProxyManager` class encapsulates all the functionality related to creating, configuring, and managing FastMCP proxy instances. It simplifies the process of: + +1. Initializing a FastMCP proxy +2. Configuring the proxy with tools +3. Mounting the proxy to a FastAPI application +4. Updating the proxy configuration +5. Shutting down the proxy + +## Usage + +### Basic Usage + +```python +from openhands.runtime.mcp.proxy import MCPProxyManager +from fastapi import FastAPI + +# Create a FastAPI app +app = FastAPI() + +# Create a proxy manager +proxy_manager = MCPProxyManager( + name="MyProxyServer", + auth_enabled=True, + api_key="my-api-key" +) + +# Initialize the proxy +proxy_manager.initialize() + +# Mount the proxy to the app +await proxy_manager.mount_to_app(app, allow_origins=["*"]) + +# Update the tools configuration +tools = [ + { + "name": "my_tool", + "description": "My tool description", + "parameters": {...} + } +] +proxy_manager.update_tools(tools) + +# Update and remount the proxy +await proxy_manager.update_and_remount(app, tools, allow_origins=["*"]) + +# Shutdown the proxy +await proxy_manager.shutdown() +``` + +### In-Memory Configuration + +The `MCPProxyManager` maintains the configuration in-memory, eliminating the need for file-based configuration. This makes it easier to update the configuration and reduces the complexity of the code. + +## Benefits + +1. **Simplified API**: The `MCPProxyManager` provides a simple and intuitive API for managing FastMCP proxies. +2. **In-Memory Configuration**: Configuration is maintained in-memory, eliminating the need for file I/O operations. +3. **Improved Error Handling**: The manager provides better error handling and logging for proxy operations. +4. **Cleaner Code**: By encapsulating proxy-related functionality in a dedicated class, the code is more maintainable and easier to understand. + +## Implementation Details + +The `MCPProxyManager` uses the `FastMCP.as_proxy()` method to create a proxy server. It manages the lifecycle of the proxy, including initialization, configuration updates, and shutdown. + +When updating the tools configuration, the manager creates a new proxy with the updated configuration and remounts it to the FastAPI application, ensuring that the proxy is always up-to-date with the latest configuration. diff --git a/openhands/runtime/mcp/proxy/__init__.py b/openhands/runtime/mcp/proxy/__init__.py new file mode 100644 index 0000000000..cd91f07fba --- /dev/null +++ b/openhands/runtime/mcp/proxy/__init__.py @@ -0,0 +1,7 @@ +""" +MCP Proxy module for OpenHands. +""" + +from openhands.runtime.mcp.proxy.manager import MCPProxyManager + +__all__ = ['MCPProxyManager'] diff --git a/openhands/runtime/mcp/proxy/manager.py b/openhands/runtime/mcp/proxy/manager.py new file mode 100644 index 0000000000..6df455478d --- /dev/null +++ b/openhands/runtime/mcp/proxy/manager.py @@ -0,0 +1,138 @@ +""" +MCP Proxy Manager for OpenHands. + +This module provides a manager class for handling FastMCP proxy instances, +including initialization, configuration, and mounting to FastAPI applications. +""" + +import logging +from typing import Any, Optional + +from fastapi import FastAPI +from fastmcp import FastMCP +from fastmcp.utilities.logging import get_logger as fastmcp_get_logger + +from openhands.core.config.mcp_config import MCPStdioServerConfig + +logger = logging.getLogger(__name__) +fastmcp_logger = fastmcp_get_logger('fastmcp') + + +class MCPProxyManager: + """ + Manager for FastMCP proxy instances. + + This class encapsulates all the functionality related to creating, configuring, + and managing FastMCP proxy instances, including mounting them to FastAPI applications. + """ + + def __init__( + self, + auth_enabled: bool = False, + api_key: Optional[str] = None, + logger_level: Optional[int] = None, + ): + """ + Initialize the MCP Proxy Manager. + + Args: + name: Name of the proxy server + auth_enabled: Whether authentication is enabled + api_key: API key for authentication (required if auth_enabled is True) + logger_level: Logging level for the FastMCP logger + """ + self.auth_enabled = auth_enabled + self.api_key = api_key + self.proxy: Optional[FastMCP] = None + # Initialize with a valid configuration format for FastMCP + self.config: dict[str, Any] = { + 'mcpServers': {}, + } + + # Configure FastMCP logger + if logger_level is not None: + fastmcp_logger.setLevel(logger_level) + + def initialize(self) -> None: + """ + Initialize the FastMCP proxy with the current configuration. + """ + if len(self.config['mcpServers']) == 0: + logger.info( + f"No MCP servers configured for FastMCP Proxy, skipping initialization." + ) + return None + + # Create a new proxy with the current configuration + self.proxy = FastMCP.as_proxy( + self.config, + auth_enabled=self.auth_enabled, + api_key=self.api_key, + ) + + logger.info(f"FastMCP Proxy initialized successfully") + + async def mount_to_app( + self, app: FastAPI, allow_origins: Optional[list[str]] = None + ) -> None: + """ + Mount the SSE server app to a FastAPI application. + + Args: + app: FastAPI application to mount to + allow_origins: List of allowed origins for CORS + """ + if len(self.config['mcpServers']) == 0: + logger.info( + f"No MCP servers configured for FastMCP Proxy, skipping mount." + ) + return + + if not self.proxy: + raise ValueError('FastMCP Proxy is not initialized') + + # Get the SSE app + # mcp_app = self.proxy.http_app(path='/shttp') + mcp_app = self.proxy.http_app(path='/sse', transport='sse') + app.mount('/mcp', mcp_app) + + # Remove any existing mounts at root path + if '/mcp' in app.routes: + app.routes.remove('/mcp') + + app.mount('/', mcp_app) + logger.info(f"Mounted FastMCP Proxy app at /mcp") + + + async def update_and_remount( + self, + app: FastAPI, + stdio_servers: list[MCPStdioServerConfig], + allow_origins: Optional[list[str]] = None, + ) -> None: + """ + Update the tools configuration and remount the proxy to the app. + + This is a convenience method that combines updating the tools, + shutting down the existing proxy, initializing a new one, and + mounting it to the app. + + Args: + app: FastAPI application to mount to + tools: List of tool configurations + allow_origins: List of allowed origins for CORS + """ + tools = { + t.name: t.model_dump() + for t in stdio_servers + } + self.config['mcpServers'] = tools + + del self.proxy + self.proxy = None + + # Initialize a new proxy + self.initialize() + + # Mount the new proxy to the app + await self.mount_to_app(app, allow_origins) diff --git a/poetry.lock b/poetry.lock index 9065009121..213231a668 100644 --- a/poetry.lock +++ b/poetry.lock @@ -2116,52 +2116,6 @@ files = [ {file = "docstring_parser-0.16.tar.gz", hash = "sha256:538beabd0af1e2db0146b6bd3caa526c35a34d61af9fd2887f3a8a27a739aa6e"}, ] -[[package]] -name = "duckdb" -version = "1.3.0" -description = "DuckDB in-process database" -optional = false -python-versions = ">=3.7.0" -groups = ["main"] -files = [ - {file = "duckdb-1.3.0-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:fc65c1e97aa010359c43c0342ea423e6efa3cb8c8e3f133b0765451ce674e3db"}, - {file = "duckdb-1.3.0-cp310-cp310-macosx_12_0_universal2.whl", hash = "sha256:8fc91b629646679e33806342510335ccbbeaf2b823186f0ae829fd48e7a63c66"}, - {file = "duckdb-1.3.0-cp310-cp310-macosx_12_0_x86_64.whl", hash = "sha256:1a69b970553fd015c557238d427ef00be3c8ed58c3bc3641aef987e33f8bf614"}, - {file = "duckdb-1.3.0-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1003e84c07b84680cee6d06e4795b6e861892474704f7972058594a52c7473cf"}, - {file = "duckdb-1.3.0-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:992239b54ca6f015ad0ed0d80f3492c065313c4641df0a226183b8860cb7f5b0"}, - {file = "duckdb-1.3.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:0ba1c5af59e8147216149b814b1970b8f7e3c240494a9688171390db3c504b29"}, - {file = "duckdb-1.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:57b794ca28e22b23bd170506cb1d4704a3608e67f0fe33273db9777b69bdf26a"}, - {file = "duckdb-1.3.0-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:60a58b85929754abb21db1e739d2f53eaef63e6015e62ba58eae3425030e7935"}, - {file = "duckdb-1.3.0-cp311-cp311-macosx_12_0_universal2.whl", hash = "sha256:1d46b5a20f078b1b2284243e02a1fde7e12cbb8d205fce62e4700bcfe6a09881"}, - {file = "duckdb-1.3.0-cp311-cp311-macosx_12_0_x86_64.whl", hash = "sha256:0044e5ffb2d46308099640a92f99980a44e12bb68642aa9e6b08acbf300d64a1"}, - {file = "duckdb-1.3.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5cb813de2ca2f5e7c77392a67bdcaa174bfd69ebbfdfc983024af270c77a0447"}, - {file = "duckdb-1.3.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7a0c993eb6df2b30b189ad747f3aea1b0b87b78ab7f80c6e7c57117b6e8dbfb0"}, - {file = "duckdb-1.3.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:6728e209570d36ece66dd7249e5d6055326321137cd807f26300733283930cd4"}, - {file = "duckdb-1.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:7e652b7c8dbdb91a94fd7d543d3e115d24a25aa0791a373a852e20cb7bb21154"}, - {file = "duckdb-1.3.0-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:f24038fe9b83dcbaeafb1ed76ec3b3f38943c1c8d27ab464ad384db8a6658b61"}, - {file = "duckdb-1.3.0-cp312-cp312-macosx_12_0_universal2.whl", hash = "sha256:956c85842841bef68f4a5388c6b225b933151a7c06d568390fc895fc44607913"}, - {file = "duckdb-1.3.0-cp312-cp312-macosx_12_0_x86_64.whl", hash = "sha256:efe883d822ed56fcfbb6a7b397c13f6a0d2eaeb3bc4ef4510f84fadb3dfe416d"}, - {file = "duckdb-1.3.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3872a3a1b80ffba5264ea236a3754d0c41d3c7b01bdf8cdcb1c180fc1b8dc8e2"}, - {file = "duckdb-1.3.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:30bf45ad78a5a997f378863e036e917b481d18d685e5c977cd0a3faf2e31fbaf"}, - {file = "duckdb-1.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:85cbd8e1d65df8a0780023baf5045d3033fabd154799bc9ea6d9ab5728f41eb3"}, - {file = "duckdb-1.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:8754c40dac0f26d9fb0363bbb5df02f7a61ce6a6728d5efc02c3bc925d7c89c3"}, - {file = "duckdb-1.3.0-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:176b9818d940c52ac7f31c64a98cf172d7c19d2a006017c9c4e9c06c246e36bf"}, - {file = "duckdb-1.3.0-cp313-cp313-macosx_12_0_universal2.whl", hash = "sha256:03981f7e8793f07a4a9a2ba387640e71d0a99ebcaf8693ab09f96d59e628b713"}, - {file = "duckdb-1.3.0-cp313-cp313-macosx_12_0_x86_64.whl", hash = "sha256:a177d55a38a62fdf79b59a0eaa32531a1dbb443265f6d67f64992cc1e82b755c"}, - {file = "duckdb-1.3.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b1c30e3749823147d5578bc3f01f35d1a0433a1c768908d946056ec8d6e1757e"}, - {file = "duckdb-1.3.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5855f3a564baf22eeeab70c120b51f5a11914f1f1634f03382daeb6b1dea4c62"}, - {file = "duckdb-1.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9b1fac15a48056f7c2739cf8800873063ba2f691e91a9b2fc167658a401ca76a"}, - {file = "duckdb-1.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:fbdfc1c0b83b90f780ae74038187ee696bb56ab727a289752372d7ec42dda65b"}, - {file = "duckdb-1.3.0-cp39-cp39-macosx_12_0_arm64.whl", hash = "sha256:5f6b5d725546ad30abc125a6813734b493fea694bc3123e991c480744573c2f1"}, - {file = "duckdb-1.3.0-cp39-cp39-macosx_12_0_universal2.whl", hash = "sha256:fcbcc9b956b06cf5ee94629438ecab88de89b08b5620fcda93665c222ab18cd4"}, - {file = "duckdb-1.3.0-cp39-cp39-macosx_12_0_x86_64.whl", hash = "sha256:2d32f2d44105e1705d8a0fb6d6d246fd69aff82c80ad23293266244b66b69012"}, - {file = "duckdb-1.3.0-cp39-cp39-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0aa7a5c0dcb780850e6da1227fb1d552af8e1a5091e02667ab6ace61ab49ce6c"}, - {file = "duckdb-1.3.0-cp39-cp39-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7cb254fd5405f3edbd7d962ba39c72e4ab90b37cb4d0e34846089796c8078419"}, - {file = "duckdb-1.3.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:a7d337b58c59fd2cd9faae531b05d940f8d92bdc2e14cb6e9a5a37675ad2742d"}, - {file = "duckdb-1.3.0-cp39-cp39-win_amd64.whl", hash = "sha256:b3cea3a345755c7dbcb58403dbab8befd499c82f0d27f893a4c1d4b8cf56ec54"}, - {file = "duckdb-1.3.0.tar.gz", hash = "sha256:09aaa4b1dca24f4d1f231e7ae66b6413e317b7e04e2753541d42df6c8113fac7"}, -] - [[package]] name = "dulwich" version = "0.22.8" @@ -2969,8 +2923,8 @@ files = [ google-api-core = {version = ">=1.34.1,<2.0.dev0 || >=2.11.dev0,<3.0.0dev", extras = ["grpc"]} google-auth = ">=2.14.1,<2.24.0 || >2.24.0,<2.25.0 || >2.25.0,<3.0.0dev" proto-plus = [ - {version = ">=1.22.3,<2.0.0dev"}, {version = ">=1.25.0,<2.0.0dev", markers = "python_version >= \"3.13\""}, + {version = ">=1.22.3,<2.0.0dev"}, ] protobuf = ">=3.20.2,<4.21.0 || >4.21.0,<4.21.1 || >4.21.1,<4.21.2 || >4.21.2,<4.21.3 || >4.21.3,<4.21.4 || >4.21.4,<4.21.5 || >4.21.5,<6.0.0dev" @@ -2992,8 +2946,8 @@ googleapis-common-protos = ">=1.56.2,<2.0.0" grpcio = {version = ">=1.49.1,<2.0.0", optional = true, markers = "python_version >= \"3.11\" and extra == \"grpc\""} grpcio-status = {version = ">=1.49.1,<2.0.0", optional = true, markers = "python_version >= \"3.11\" and extra == \"grpc\""} proto-plus = [ - {version = ">=1.22.3,<2.0.0"}, {version = ">=1.25.0,<2.0.0", markers = "python_version >= \"3.13\""}, + {version = ">=1.22.3,<2.0.0"}, ] protobuf = ">=3.19.5,<3.20.0 || >3.20.0,<3.20.1 || >3.20.1,<4.21.0 || >4.21.0,<4.21.1 || >4.21.1,<4.21.2 || >4.21.2,<4.21.3 || >4.21.3,<4.21.4 || >4.21.4,<4.21.5 || >4.21.5,<7.0.0" requests = ">=2.18.0,<3.0.0" @@ -3211,8 +3165,8 @@ google-api-core = {version = ">=1.34.1,<2.0.dev0 || >=2.11.dev0,<3.0.0", extras google-auth = ">=2.14.1,<2.24.0 || >2.24.0,<2.25.0 || >2.25.0,<3.0.0" grpc-google-iam-v1 = ">=0.14.0,<1.0.0" proto-plus = [ - {version = ">=1.22.3,<2.0.0"}, {version = ">=1.25.0,<2.0.0", markers = "python_version >= \"3.13\""}, + {version = ">=1.22.3,<2.0.0"}, ] protobuf = ">=3.20.2,<4.21.0 || >4.21.0,<4.21.1 || >4.21.1,<4.21.2 || >4.21.2,<4.21.3 || >4.21.3,<4.21.4 || >4.21.4,<4.21.5 || >4.21.5,<7.0.0" @@ -5398,30 +5352,6 @@ cli = ["python-dotenv (>=1.0.0)", "typer (>=0.12.4)"] rich = ["rich (>=13.9.4)"] ws = ["websockets (>=15.0.1)"] -[[package]] -name = "mcpm" -version = "1.12.0" -description = "MCPM - Model Context Protocol Manager" -optional = false -python-versions = ">=3.10" -groups = ["main"] -files = [ - {file = "mcpm-1.12.0-py3-none-any.whl", hash = "sha256:ed3a87300420bcdb9cd12ef290179fda5bd51eb2f4cd3e793084d83eed91b249"}, - {file = "mcpm-1.12.0.tar.gz", hash = "sha256:e9d2b852b90d7fd62dede584f035dd6b2b3d068d233e96b82aead835f81a911a"}, -] - -[package.dependencies] -click = ">=8.1.3" -duckdb = ">=1.2.2" -mcp = ">=1.8.0" -prompt-toolkit = ">=3.0.0" -psutil = ">=7.0.0" -pydantic = ">=2.5.1" -requests = ">=2.28.0" -rich = ">=12.0.0" -ruamel-yaml = ">=0.18.10" -watchfiles = ">=1.0.4" - [[package]] name = "mdurl" version = "0.1.2" @@ -6500,8 +6430,8 @@ files = [ [package.dependencies] googleapis-common-protos = ">=1.52,<2.0" grpcio = [ - {version = ">=1.63.2,<2.0.0", markers = "python_version < \"3.13\""}, {version = ">=1.66.2,<2.0.0", markers = "python_version >= \"3.13\""}, + {version = ">=1.63.2,<2.0.0", markers = "python_version < \"3.13\""}, ] opentelemetry-api = ">=1.15,<2.0" opentelemetry-exporter-otlp-proto-common = "1.34.0" @@ -8937,82 +8867,6 @@ files = [ [package.dependencies] pyasn1 = ">=0.1.3" -[[package]] -name = "ruamel-yaml" -version = "0.18.12" -description = "ruamel.yaml is a YAML parser/emitter that supports roundtrip preservation of comments, seq/map flow style, and map key order" -optional = false -python-versions = ">=3.7" -groups = ["main"] -files = [ - {file = "ruamel.yaml-0.18.12-py3-none-any.whl", hash = "sha256:790ba4c48b6a6e6b12b532a7308779eb12d2aaab3a80fdb8389216f28ea2b287"}, - {file = "ruamel.yaml-0.18.12.tar.gz", hash = "sha256:5a38fd5ce39d223bebb9e3a6779e86b9427a03fb0bf9f270060f8b149cffe5e2"}, -] - -[package.dependencies] -"ruamel.yaml.clib" = {version = ">=0.2.7", markers = "platform_python_implementation == \"CPython\" and python_version < \"3.14\""} - -[package.extras] -docs = ["mercurial (>5.7)", "ryd"] -jinja2 = ["ruamel.yaml.jinja2 (>=0.2)"] - -[[package]] -name = "ruamel-yaml-clib" -version = "0.2.12" -description = "C version of reader, parser and emitter for ruamel.yaml derived from libyaml" -optional = false -python-versions = ">=3.9" -groups = ["main"] -markers = "platform_python_implementation == \"CPython\"" -files = [ - {file = "ruamel.yaml.clib-0.2.12-cp310-cp310-macosx_13_0_arm64.whl", hash = "sha256:11f891336688faf5156a36293a9c362bdc7c88f03a8a027c2c1d8e0bcde998e5"}, - {file = "ruamel.yaml.clib-0.2.12-cp310-cp310-manylinux2014_aarch64.whl", hash = "sha256:a606ef75a60ecf3d924613892cc603b154178ee25abb3055db5062da811fd969"}, - {file = "ruamel.yaml.clib-0.2.12-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd5415dded15c3822597455bc02bcd66e81ef8b7a48cb71a33628fc9fdde39df"}, - {file = "ruamel.yaml.clib-0.2.12-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f66efbc1caa63c088dead1c4170d148eabc9b80d95fb75b6c92ac0aad2437d76"}, - {file = "ruamel.yaml.clib-0.2.12-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:22353049ba4181685023b25b5b51a574bce33e7f51c759371a7422dcae5402a6"}, - {file = "ruamel.yaml.clib-0.2.12-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:932205970b9f9991b34f55136be327501903f7c66830e9760a8ffb15b07f05cd"}, - {file = "ruamel.yaml.clib-0.2.12-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a52d48f4e7bf9005e8f0a89209bf9a73f7190ddf0489eee5eb51377385f59f2a"}, - {file = "ruamel.yaml.clib-0.2.12-cp310-cp310-win32.whl", hash = "sha256:3eac5a91891ceb88138c113f9db04f3cebdae277f5d44eaa3651a4f573e6a5da"}, - {file = "ruamel.yaml.clib-0.2.12-cp310-cp310-win_amd64.whl", hash = "sha256:ab007f2f5a87bd08ab1499bdf96f3d5c6ad4dcfa364884cb4549aa0154b13a28"}, - {file = "ruamel.yaml.clib-0.2.12-cp311-cp311-macosx_13_0_arm64.whl", hash = "sha256:4a6679521a58256a90b0d89e03992c15144c5f3858f40d7c18886023d7943db6"}, - {file = "ruamel.yaml.clib-0.2.12-cp311-cp311-manylinux2014_aarch64.whl", hash = "sha256:d84318609196d6bd6da0edfa25cedfbabd8dbde5140a0a23af29ad4b8f91fb1e"}, - {file = "ruamel.yaml.clib-0.2.12-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bb43a269eb827806502c7c8efb7ae7e9e9d0573257a46e8e952f4d4caba4f31e"}, - {file = "ruamel.yaml.clib-0.2.12-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:811ea1594b8a0fb466172c384267a4e5e367298af6b228931f273b111f17ef52"}, - {file = "ruamel.yaml.clib-0.2.12-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:cf12567a7b565cbf65d438dec6cfbe2917d3c1bdddfce84a9930b7d35ea59642"}, - {file = "ruamel.yaml.clib-0.2.12-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:7dd5adc8b930b12c8fc5b99e2d535a09889941aa0d0bd06f4749e9a9397c71d2"}, - {file = "ruamel.yaml.clib-0.2.12-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1492a6051dab8d912fc2adeef0e8c72216b24d57bd896ea607cb90bb0c4981d3"}, - {file = "ruamel.yaml.clib-0.2.12-cp311-cp311-win32.whl", hash = "sha256:bd0a08f0bab19093c54e18a14a10b4322e1eacc5217056f3c063bd2f59853ce4"}, - {file = "ruamel.yaml.clib-0.2.12-cp311-cp311-win_amd64.whl", hash = "sha256:a274fb2cb086c7a3dea4322ec27f4cb5cc4b6298adb583ab0e211a4682f241eb"}, - {file = "ruamel.yaml.clib-0.2.12-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:20b0f8dc160ba83b6dcc0e256846e1a02d044e13f7ea74a3d1d56ede4e48c632"}, - {file = "ruamel.yaml.clib-0.2.12-cp312-cp312-manylinux2014_aarch64.whl", hash = "sha256:943f32bc9dedb3abff9879edc134901df92cfce2c3d5c9348f172f62eb2d771d"}, - {file = "ruamel.yaml.clib-0.2.12-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95c3829bb364fdb8e0332c9931ecf57d9be3519241323c5274bd82f709cebc0c"}, - {file = "ruamel.yaml.clib-0.2.12-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:749c16fcc4a2b09f28843cda5a193e0283e47454b63ec4b81eaa2242f50e4ccd"}, - {file = "ruamel.yaml.clib-0.2.12-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:bf165fef1f223beae7333275156ab2022cffe255dcc51c27f066b4370da81e31"}, - {file = "ruamel.yaml.clib-0.2.12-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:32621c177bbf782ca5a18ba4d7af0f1082a3f6e517ac2a18b3974d4edf349680"}, - {file = "ruamel.yaml.clib-0.2.12-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:b82a7c94a498853aa0b272fd5bc67f29008da798d4f93a2f9f289feb8426a58d"}, - {file = "ruamel.yaml.clib-0.2.12-cp312-cp312-win32.whl", hash = "sha256:e8c4ebfcfd57177b572e2040777b8abc537cdef58a2120e830124946aa9b42c5"}, - {file = "ruamel.yaml.clib-0.2.12-cp312-cp312-win_amd64.whl", hash = "sha256:0467c5965282c62203273b838ae77c0d29d7638c8a4e3a1c8bdd3602c10904e4"}, - {file = "ruamel.yaml.clib-0.2.12-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:4c8c5d82f50bb53986a5e02d1b3092b03622c02c2eb78e29bec33fd9593bae1a"}, - {file = "ruamel.yaml.clib-0.2.12-cp313-cp313-manylinux2014_aarch64.whl", hash = "sha256:e7e3736715fbf53e9be2a79eb4db68e4ed857017344d697e8b9749444ae57475"}, - {file = "ruamel.yaml.clib-0.2.12-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0b7e75b4965e1d4690e93021adfcecccbca7d61c7bddd8e22406ef2ff20d74ef"}, - {file = "ruamel.yaml.clib-0.2.12-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:96777d473c05ee3e5e3c3e999f5d23c6f4ec5b0c38c098b3a5229085f74236c6"}, - {file = "ruamel.yaml.clib-0.2.12-cp313-cp313-musllinux_1_1_i686.whl", hash = "sha256:3bc2a80e6420ca8b7d3590791e2dfc709c88ab9152c00eeb511c9875ce5778bf"}, - {file = "ruamel.yaml.clib-0.2.12-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:e188d2699864c11c36cdfdada94d781fd5d6b0071cd9c427bceb08ad3d7c70e1"}, - {file = "ruamel.yaml.clib-0.2.12-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4f6f3eac23941b32afccc23081e1f50612bdbe4e982012ef4f5797986828cd01"}, - {file = "ruamel.yaml.clib-0.2.12-cp313-cp313-win32.whl", hash = "sha256:6442cb36270b3afb1b4951f060eccca1ce49f3d087ca1ca4563a6eb479cb3de6"}, - {file = "ruamel.yaml.clib-0.2.12-cp313-cp313-win_amd64.whl", hash = "sha256:e5b8daf27af0b90da7bb903a876477a9e6d7270be6146906b276605997c7e9a3"}, - {file = "ruamel.yaml.clib-0.2.12-cp39-cp39-macosx_12_0_arm64.whl", hash = "sha256:fc4b630cd3fa2cf7fce38afa91d7cfe844a9f75d7f0f36393fa98815e911d987"}, - {file = "ruamel.yaml.clib-0.2.12-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:bc5f1e1c28e966d61d2519f2a3d451ba989f9ea0f2307de7bc45baa526de9e45"}, - {file = "ruamel.yaml.clib-0.2.12-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5a0e060aace4c24dcaf71023bbd7d42674e3b230f7e7b97317baf1e953e5b519"}, - {file = "ruamel.yaml.clib-0.2.12-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e2f1c3765db32be59d18ab3953f43ab62a761327aafc1594a2a1fbe038b8b8a7"}, - {file = "ruamel.yaml.clib-0.2.12-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:d85252669dc32f98ebcd5d36768f5d4faeaeaa2d655ac0473be490ecdae3c285"}, - {file = "ruamel.yaml.clib-0.2.12-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:e143ada795c341b56de9418c58d028989093ee611aa27ffb9b7f609c00d813ed"}, - {file = "ruamel.yaml.clib-0.2.12-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:2c59aa6170b990d8d2719323e628aaf36f3bfbc1c26279c0eeeb24d05d2d11c7"}, - {file = "ruamel.yaml.clib-0.2.12-cp39-cp39-win32.whl", hash = "sha256:beffaed67936fbbeffd10966a4eb53c402fafd3d6833770516bf7314bc6ffa12"}, - {file = "ruamel.yaml.clib-0.2.12-cp39-cp39-win_amd64.whl", hash = "sha256:040ae85536960525ea62868b642bdb0c2cc6021c9f9d507810c0c604e66f5a7b"}, - {file = "ruamel.yaml.clib-0.2.12.tar.gz", hash = "sha256:6c8fbb13ec503f99a91901ab46e0b07ae7941cd527393187039aec586fdfd36f"}, -] - [[package]] name = "ruff" version = "0.11.11" @@ -11760,4 +11614,4 @@ cffi = ["cffi (>=1.11)"] [metadata] lock-version = "2.1" python-versions = "^3.12,<3.14" -content-hash = "d9f6c24fa80dd191f180af0c802ea11ecf514d86aaa421cb19a9bb497362c101" +content-hash = "8c960ca43a540bfd96dc029d45fa4e0a4a3f75c2996feecaa8b989c348655f70" diff --git a/pyproject.toml b/pyproject.toml index a2b6a2e37c..04c0407bdb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -67,7 +67,6 @@ poetry = "^2.1.2" anyio = "4.9.0" pythonnet = "*" fastmcp = "^2.5.2" -mcpm = "1.12.0" python-frontmatter = "^1.1.0" # TODO: Should these go into the runtime group? ipywidgets = "^8.1.5" diff --git a/tests/runtime/test_mcp_action.py b/tests/runtime/test_mcp_action.py index e0339233b1..9eb10b9bf0 100644 --- a/tests/runtime/test_mcp_action.py +++ b/tests/runtime/test_mcp_action.py @@ -114,9 +114,11 @@ def test_default_activated_tools(): ) with open(mcp_config_path, 'r') as f: mcp_config = json.load(f) - assert 'default' in mcp_config + assert 'mcpServers' in mcp_config + assert 'default' in mcp_config['mcpServers'] + assert 'tools' in mcp_config # no tools are always activated yet - assert len(mcp_config['default']) == 0 + assert len(mcp_config['tools']) == 0 @pytest.mark.asyncio @@ -249,7 +251,11 @@ async def test_both_stdio_and_sse_mcp( assert obs_cat.exit_code == 0 mcp_action_fetch = MCPAction( - name='fetch', arguments={'url': 'http://localhost:8000'} + # NOTE: the tool name is `fetch_fetch` because the tool name is `fetch` + # And FastMCP Proxy will pre-pend the server name (in this case, `fetch`) + # to the tool name, so the full tool name becomes `fetch_fetch` + name='fetch', + arguments={'url': 'http://localhost:8000'}, ) obs_fetch = await runtime.call_tool_mcp(mcp_action_fetch) logger.info(obs_fetch, extra={'msg_type': 'OBSERVATION'}) @@ -304,7 +310,9 @@ async def test_microagent_and_one_stdio_mcp_in_config( logger.info(f'updated_config: {updated_config}') # ======= Test the stdio server in the config ======= - mcp_action_sse = MCPAction(name='list_directory', arguments={'path': '/'}) + mcp_action_sse = MCPAction( + name='filesystem_list_directory', arguments={'path': '/'} + ) obs_sse = await runtime.call_tool_mcp(mcp_action_sse) logger.info(obs_sse, extra={'msg_type': 'OBSERVATION'}) assert isinstance(obs_sse, MCPObservation), ( @@ -332,7 +340,7 @@ async def test_microagent_and_one_stdio_mcp_in_config( assert obs_cat.exit_code == 0 mcp_action_fetch = MCPAction( - name='fetch', arguments={'url': 'http://localhost:8000'} + name='fetch_fetch', arguments={'url': 'http://localhost:8000'} ) obs_fetch = await runtime.call_tool_mcp(mcp_action_fetch) logger.info(obs_fetch, extra={'msg_type': 'OBSERVATION'})