| 1 | # mcp_handler.py DOX |
| 2 | |
| 3 | ## Purpose |
| 4 | |
| 5 | - Own the `mcp_handler.py` helper module. |
| 6 | - This module loads global and project-scoped MCP server configuration and exposes MCP tools to agents. |
| 7 | - Keep this file-level DOX profile synchronized with `mcp_handler.py` because this directory is intentionally flat. |
| 8 | |
| 9 | ## Ownership |
| 10 | |
| 11 | - `mcp_handler.py` owns the runtime implementation. |
| 12 | - `mcp_handler.py.dox.md` owns durable notes about responsibilities, contracts, side effects, and verification for that implementation. |
| 13 | - Classes: |
| 14 | - `MCPTool` (`Tool`) |
| 15 | - `get_log_object(self) -> LogItem` |
| 16 | - `async execute(self, **kwargs)` |
| 17 | - `async before_execution(self, **kwargs)` |
| 18 | - `async after_execution(self, response: Response, **kwargs)` |
| 19 | - `MCPServerRemote` (`BaseModel`) |
| 20 | - `get_error(self) -> str` |
| 21 | - `get_log(self) -> str` |
| 22 | - `get_tools(self) -> List[dict[str, Any]]` |
| 23 | - `get_all_tools(self) -> List[dict[str, Any]]` |
| 24 | - `has_tool(self, tool_name: str) -> bool` |
| 25 | - `async call_tool(self, tool_name: str, input_data: Dict[str, Any]) -> CallToolResult` |
| 26 | - `update(self, config: dict[str, Any]) -> 'MCPServerRemote'` |
| 27 | - `async initialize(self) -> 'MCPServerRemote'` |
| 28 | - `MCPServerLocal` (`BaseModel`) |
| 29 | - `get_error(self) -> str` |
| 30 | - `get_log(self) -> str` |
| 31 | - `get_tools(self) -> List[dict[str, Any]]` |
| 32 | - `get_all_tools(self) -> List[dict[str, Any]]` |
| 33 | - `has_tool(self, tool_name: str) -> bool` |
| 34 | - `async call_tool(self, tool_name: str, input_data: Dict[str, Any]) -> CallToolResult` |
| 35 | - `update(self, config: dict[str, Any]) -> 'MCPServerLocal'` |
| 36 | - `async initialize(self) -> 'MCPServerLocal'` |
| 37 | - `MCPConfig` (`BaseModel`) |
| 38 | - `get_instance(cls) -> 'MCPConfig'` |
| 39 | - `clear_project_instances(cls)` |
| 40 | - `parse_config_string(cls, config_str: str) -> List[Dict[str, Any]]` |
| 41 | - `merge_config_strings(cls, global_config: str, project_config: str) -> tuple[List[Dict[str, Any]], str]` |
| 42 | - `get_project_instance(cls, project_name: str | None, *, force: bool = False) -> 'MCPConfig'` |
| 43 | - `refresh_project(cls, project_name: str) -> 'MCPConfig'` |
| 44 | - `get_for_agent(cls, agent: Any) -> 'MCPConfig'` |
| 45 | - `wait_for_lock(cls)` |
| 46 | - `update(cls, config_str: str) -> Any` |
| 47 | - `normalize_config(cls, servers: Any)` |
| 48 | - `get_server_log(self, server_name: str) -> str` |
| 49 | - `get_servers_status(self) -> list[dict[str, Any]]` |
| 50 | - `get_server_detail(self, server_name: str) -> dict[str, Any]` |
| 51 | - `is_initialized(self) -> bool` |
| 52 | - `MCPClientBase` (`ABC`) |
| 53 | - `async update_tools(self) -> 'MCPClientBase'` |
| 54 | - `has_tool(self, tool_name: str) -> bool` |
| 55 | - `get_tools(self) -> List[dict[str, Any]]` |
| 56 | - `async call_tool(self, tool_name: str, input_data: Dict[str, Any]) -> CallToolResult` |
| 57 | - `get_log(self)` |
| 58 | - `MCPClientLocal` (`MCPClientBase`) |
| 59 | - `CustomHTTPClientFactory` (`ABC`) |
| 60 | - `MCPClientRemote` (`MCPClientBase`) |
| 61 | - `get_session_id(self) -> Optional[str]` |
| 62 | - Top-level functions: |
| 63 | - `_mcp_get(item: Any, key: str, default: Any=...) -> Any` |
| 64 | - `normalize_name(name: str) -> str` |
| 65 | - `_determine_server_type(config_dict: dict) -> str`: Determine the server type based on configuration, with backward compatibility. |
| 66 | - `_is_streaming_http_type(server_type: str) -> bool`: Check if the server type is a streaming HTTP variant. |
| 67 | - `_split_qualified_tool_name(tool_name: str) -> tuple[str, str]`: Split `server.tool` names while preserving dots inside MCP tool names. |
| 68 | - `_normalize_disabled_tools(value: Any) -> list[str]`: Normalize the optional per-server disabled tool list. |
| 69 | - `_split_stdio_command(command: Any) -> tuple[str, list[str]]`: Split shell-style local MCP command lines into an executable plus leading arguments. |
| 70 | - `_split_stdio_arg_fragment(arg: str) -> list[str]`: Split collapsed option/value argument fragments while preserving obvious single values with spaces. |
| 71 | - `_normalize_stdio_args(value: Any) -> list[str]`: Normalize local MCP argument lists after manager/raw JSON parsing. |
| 72 | - `initialize_mcp(mcp_servers_config: str)` |
| 73 | - Notable constants/configuration names: `DEFAULT_MCP_SERVERS_CONFIG`, `MCP_MEDIA_TOKENS_ESTIMATE`, `MAX_MCP_RESOURCE_TEXT_CHARS`, `MCP_SESSION_CLEANUP_TIMEOUT_SECONDS`, `MCP_OPERATION_TIMEOUT_GRACE_SECONDS`, `T`. |
| 74 | |
| 75 | ## Runtime Contracts |
| 76 | |
| 77 | - Helper modules own reusable framework APIs and must preserve public callers unless all callers, tests, and docs are updated together. |
| 78 | - Update this file whenever public functions, classes, persistence behavior, path/security assumptions, side effects, or cross-module contracts change. |
| 79 | - `MCPTool` is a `Tool`. |
| 80 | - `MCPTool` defines `execute(...)`. |
| 81 | - Global MCP configuration remains backed by settings; project MCP configuration is loaded through `helpers.projects` and merged with global config when an active agent context has `context.project`. |
| 82 | - Project-scoped MCP servers overlay global servers by normalized name. The resulting `MCPConfig` cache key is derived from both config strings so project instances refresh when either scope changes. |
| 83 | - Server status and detail responses include `scope`, and MCP tools resolve through `MCPConfig.get_for_agent(agent)` before execution. |
| 84 | - MCP tool names are qualified as `server_name.tool_name`; server names are normalized without dots, and the tool portion may contain dots. |
| 85 | - Agent-facing MCP prompt descriptions filter through the central profile tool |
| 86 | policy, and `MCPTool.execute()` rechecks the same policy with the explicit MCP |
| 87 | canonical ID before invocation. |
| 88 | - `MCPConfig.get_tool()` tries the supplied qualified name first, then restores an advertised Responses alias from the calling agent's name map; names that still do not identify an MCP tool return `None` unchanged for downstream local-tool resolution. |
| 89 | - Servers may define `disabled_tools` as a list of MCP tool names. Disabled tools are omitted from agent-facing prompts, status counts, `has_tool`, and calls, while detail views can still retrieve them through `get_all_tools()` with a `disabled` flag so users can re-enable them. |
| 90 | - Server-specific `init_timeout` and `tool_timeout` override global MCP client timeout settings for list-tools and call-tool operations. |
| 91 | - Local stdio server configs accept either strict MCP JSON (`command: "uvx", args: [...]`) or manager-style command lines (`command: "uvx package"`) and normalize them before spawning the process. |
| 92 | - MCP image and image-resource content is materialized to scoped artifact files and returned both as model-visible image attachments and as path metadata (`attachments`/`media_paths`) for downstream delivery. |
| 93 | - MCP config locks must not be held across awaited server initialization or tool-call operations. Slow or wedged MCP servers must not block status reads, prompt construction, unrelated MCP servers, or later tool calls through the shared config lock. |
| 94 | - MCP client session work runs inside disposable isolated `DeferredTask` workers with an outer timeout. Normal `AsyncExitStack` cleanup is also bounded; if cleanup or transport shutdown does not finish, the operation reports failure or warning while Agent Zero keeps control of the agent loop. |
| 95 | - Server status marks initialized server objects with cached initialization errors as disconnected, even if the config object exists. |
| 96 | - Observed side-effect areas: filesystem writes, network calls, WebSocket state, settings/state persistence, secret handling. |
| 97 | - Imported dependency areas include: `abc`, `anyio.streams.memory`, `asyncio`, `contextlib`, `datetime`, `helpers`, `helpers.defer`, `helpers.log`, `helpers.print_style`, `helpers.tool`, `httpx`, `json`, `mcp`, `mcp.client.sse`, `mcp.client.stdio`, `mcp.client.streamable_http`, `mcp.shared.message`, `shlex`. |
| 98 | |
| 99 | ## Key Concepts |
| 100 | |
| 101 | - Important called helpers/classes observed in the source: `TypeVar`, `name.strip.lower`, `re.sub`, `Field`, `PrivateAttr`, `threading.Lock`, `DeferredTask`, `_split_qualified_tool_name`, `config_dict.lower`, `server_type.lower`, `MCPConfig.get_instance.is_initialized`, `MCPConfig.get_for_agent`, `projects.validate_project_name`, `projects.load_project_mcp_servers`, `settings.get_settings`, `self.agent.context.log.log`, `str.strip`, `media_artifacts.guess_extension`, `callable`, `self._content_item_dump`, `join`, `Response`, `self.get_log_object`, `self._raw_tool_response`, `additional.pop`, `self._coerce_media_token_estimate`. |
| 102 | - Keep request/response, tool, or helper semantics documented here at the same time as source changes. |
| 103 | |
| 104 | ## Work Guidance |
| 105 | |
| 106 | - Preserve public helper APIs used by core code and plugins unless every caller is updated. |
| 107 | - Keep path, auth, secret, persistence, network, and subprocess behavior explicit and bounded. |
| 108 | - Prefer adding cohesive helper functions here only when behavior is reused across modules. |
| 109 | - Keep MCP timeout and cleanup changes covered by deterministic tests that do not require real MCP servers or network credentials. |
| 110 | |
| 111 | ## Verification |
| 112 | |
| 113 | - Run targeted tests for changed helper behavior; run security regressions for auth, filesystem, WebSocket, tunnel, upload, or secret-handling helpers. |
| 114 | - Related tests observed by source search: |
| 115 | - `tests/test_mcp_handler_multimodal.py` |
| 116 | - `tests/test_tool_policy.py` |
| 117 | |
| 118 | ## Child DOX Index |
| 119 | |
| 120 | No child DOX files. |