Files
aditya ee82d6101e feat(skill): add MCP adapter for external tools
Implement MCPToolAdapter to connect to external MCP servers, enumerate
tools, and register them in ToolRegistry with source="mcp". Includes
connect/reconnect/disconnect lifecycle with timeout enforcement, input
validation on invoke, capability inference, Behave/Robot/ASV tests, and
docs/reference/mcp_adapter.md.

ISSUES CLOSED: #159
2026-02-25 13:17:26 +00:00

145 lines
5.6 KiB
Markdown

# MCP Tool Adapter
This document covers the MCP (Model Context Protocol) adapter introduced in
**M2.3.mcp-runtime**: connecting to external MCP servers, discovering tools,
invoking them with schema validation, and registering them in the ToolRegistry.
## Overview
The `MCPToolAdapter` bridges any MCP-compatible tool server into the
CleverAgents runtime. It manages the full lifecycle:
| Operation | Method | Description |
|-----------|--------|-------------|
| Connect | `connect(timeout?)` | Start the server process or HTTP session, perform handshake |
| Disconnect | `disconnect()` | Clean shutdown of the server connection |
| Reconnect | `reconnect(timeout?)` | Disconnect and re-connect; clears cached tool list |
| Discover | `discover_tools(filter?)` | Enumerate available tools from the server |
| Invoke | `invoke(name, args)` | Call a tool with automatic input validation |
| Register | `register_tools(registry, namespace)` | Bulk-register into a ToolRegistry |
## Server Configuration
### `MCPServerConfig` Fields
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `name` | `string` | Yes | Server identifier |
| `transport` | `string` | Yes | `stdio`, `sse`, or `streamable-http` |
| `command` | `string` | stdio only | Command to spawn the server process |
| `args` | `list[string]` | No | Command-line arguments |
| `env` | `dict` | No | Environment variables for the server process |
| `url` | `string` | sse/http only | Server URL for remote connections |
| `headers` | `dict` | No | HTTP headers for remote connections |
### Validation Rules
- `stdio` transport **requires** `command`
- `sse` / `streamable-http` transport **requires** `url`
## Tool Discovery
After connecting, call `discover_tools()` to enumerate all tools exposed by
the MCP server. Each returned `MCPToolDescriptor` includes the tool name,
description, and JSON Schema for inputs.
### Filtering
Use `MCPToolFilter` with `include` or `exclude` lists to control which
tools are exposed.
### `MCPToolDescriptor` Fields
| Field | Type | Description |
|-------|------|-------------|
| `name` | `string` | Tool name as exposed by the server |
| `description` | `string` | Human-readable description |
| `input_schema` | `dict` | JSON Schema for tool inputs |
## Tool Invocation
Call `invoke(name, arguments)` to execute a discovered tool. Input arguments
are validated against the tool's JSON Schema before the call is made. If
validation fails, the result is returned immediately with `success=False`.
### `MCPToolResult` Fields
| Field | Type | Description |
|-------|------|-------------|
| `success` | `bool` | Whether the invocation succeeded |
| `data` | `dict` | Result payload from the server |
| `error` | `string` or `null` | Error message on failure |
| `duration_ms` | `float` | Wall-clock execution time in milliseconds |
## Reconnect
Use `reconnect(timeout?)` to recover from a dropped connection or after a
server restart. It performs a clean `disconnect()` followed by a fresh
`connect()` and clears the cached tool list. Call `discover_tools()` again
after reconnecting.
```python
try:
adapter.invoke("create_issue", {"title": "Bug"})
except RuntimeError:
# Connection dropped — attempt recovery
adapter.reconnect(timeout=10.0)
adapter.discover_tools()
```
If the new connection attempt fails or times out, `reconnect()` raises
`ConnectionError`.
## ToolRegistry Integration
Discovered MCP tools can be bulk-registered into any `ToolRegistry` via
`register_tools(registry, namespace)`. Each tool is registered with the
name `{namespace}/{tool_name}`. Re-registration after tool list changes
replaces stale entries automatically.
Registered tools always carry `source="mcp"` and
`source_metadata={"server": "<server-name>"}` for provenance tracking.
`checkpointable` is always `False` for MCP tools — external server calls
cannot be rolled back transactionally.
## Capability Inference
MCP tools expose limited metadata (name, description, inputSchema). The adapter
automatically infers extended `ToolCapability` metadata using name-based heuristics:
| Tool name keywords | Inferred capability |
|---|---|
| `read`, `get`, `list`, `search`, `find` | `read_only: true` |
| `write`, `create`, `update`, `delete`, `set` | `writes: true` |
| Both read and write keywords | `writes: true` (write takes precedence) |
| Neither | `read_only: false`, `writes: false` |
Inferences are applied automatically when registering tools via
`register_tools()`. They can be overridden via the `overrides` block in
tool or skill YAML.
Use `MCPToolAdapter.infer_capabilities(tool_name)` directly to check
what would be inferred for a given tool name.
## Error Classification
| Error Type | When | Exception / Field |
|------------|------|-------------------|
| Connection failure | `connect()` with unreachable server | `ConnectionError` |
| Not connected | `invoke()` / `discover_tools()` before `connect()` | `RuntimeError` |
| Tool not found | `invoke()` with unknown tool name | `result.error` contains "not found" |
| Validation failure | `invoke()` with invalid input | `result.error` contains "validation" |
| Timeout | Server call exceeds deadline | `result.error` contains "timeout" |
| Server error | Server returns `isError: true` | `result.error` contains "server error" |
## Transport Layer
The `MCPTransport` base class defines the abstract interface. Concrete
implementations handle stdio (child process) and HTTP (SSE / streamable-http)
protocols. For testing, provide a mock transport to the adapter constructor.
## Thread Safety
`MCPToolAdapter` uses `threading.RLock` internally. All public methods are
safe to call from multiple threads concurrently.