Skip to content

feat(mcp): Option B pass-through - callback wiring, bug fixes, events, config loading#1

Open
agent-of-mkmeral wants to merge 2 commits into
mainfrom
mcp/option-b-pass-through
Open

feat(mcp): Option B pass-through - callback wiring, bug fixes, events, config loading#1
agent-of-mkmeral wants to merge 2 commits into
mainfrom
mcp/option-b-pass-through

Conversation

@agent-of-mkmeral
Copy link
Copy Markdown
Owner

Summary

Implements Option B (MCP as Pass-Through) for deeper MCP integration in the Strands Agents SDK. This approach exposes all MCP ClientSession callbacks directly through MCPClient without modifying the Agent class.

Changes

Bug Fixes

  1. _meta extraction (mcp_client.py): The _meta key is now extracted from tool arguments and passed as meta= to session.call_tool(). This enables protocol-level metadata (e.g., progress tokens) to flow correctly. The original arguments dict is not mutated.

  2. isError field (mcp_types.py, mcp_client.py): Added isError: NotRequired[bool] to MCPToolResult TypedDict, populated from call_tool_result.isError. Downstream consumers can now distinguish error results without parsing the status string.

Callback Wiring (Pass-Through)

All remaining ClientSession callbacks are exposed on MCPClient.__init__() and wired to the session:

Callback Purpose Default
sampling_callback Handle sampling/createMessage from server None
list_roots_callback Handle roots/list from server None
logging_callback Handle log notifications from server Routes to Python logging
progress_callback Per-call progress from call_tool() None
elicitation_callback Handle elicitation requests (existing) None

The default logging callback maps MCP levels to Python levels:

  • emergency, alert, criticalCRITICAL
  • errorERROR
  • warningWARNING
  • notice, infoINFO
  • debugDEBUG

MCP Event Hook Hierarchy (src/strands/hooks/mcp_events.py)

New typed events for MCP operations, compatible with HookRegistry:

  • Lifecycle: MCPServerConnectedEvent, MCPServerDisconnectedEvent
  • Tool calls: MCPToolCallStartEvent, MCPToolCallEndEvent
  • Notifications: MCPLoggingEvent, MCPProgressEvent, MCPSamplingRequestEvent

Config Loading (src/strands/tools/mcp/mcp_config.py)

load_mcp_servers() utility function:

  • Accepts JSON file path, JSON string, or dict
  • Standard mcpServers format (Claude Desktop / Cursor compatible)
  • Supports disabled, disabledTools, prefix, stdio and HTTP transports
  • Shared callbacks applied to all created MCPClient instances

Option B Design Decisions

  • NO Agent changes - users wire callbacks manually on MCPClient
  • No MCPRegistry - not needed for pass-through
  • No MCPPlugin - not needed for pass-through
  • ✅ Minimal, composable approach

Tests

44 new tests (test_mcp_option_b.py):

  • _meta extraction (4 tests)
  • isError field (3 tests)
  • Callback wiring (6 tests)
  • Default logging callback (5 tests)
  • Config loading (15 tests)
  • MCP events (9 tests)
  • MCPToolResult type (2 tests)

10 existing tests updated to account for new call_tool() kwargs.

All 172 MCP tests pass. All 31 hooks tests pass.

Usage Example

from strands.tools.mcp import MCPClient, load_mcp_servers

# Option 1: Manual wiring
client = MCPClient(
    transport_callable=my_transport,
    sampling_callback=my_sampling_handler,
    progress_callback=lambda p, t, m: print(f"{p}/{t}: {m}"),
)

# Option 2: Config file loading
clients = load_mcp_servers(
    "~/.config/mcp/servers.json",
    progress_callback=my_progress_handler,
)
agent = Agent(tools=clients)

…, config loading

## Bug Fixes

- **_meta extraction**: Extract `_meta` from tool arguments dict and pass
  as `meta=` keyword argument to `session.call_tool()`. This enables
  protocol-level metadata (e.g., progress tokens) to flow through correctly.
  The original arguments dict is not mutated.

- **isError field**: Add `isError` field to `MCPToolResult` TypedDict and
  populate it from `call_tool_result.isError`. Downstream consumers can now
  distinguish error results without inspecting the status string.

## Callback Wiring (Option B: Pass-Through)

Wire ALL remaining `ClientSession` callbacks through `MCPClient`:

- `sampling_callback`: Handle `sampling/createMessage` requests from server
- `list_roots_callback`: Handle `roots/list` requests from server
- `logging_callback`: Handle log notifications from server
  (defaults to routing MCP log levels to Python `logging`)
- `progress_callback`: Per-call progress notifications passed to `call_tool()`
- `elicitation_callback`: Already existed, now documented alongside others

Default logging callback maps MCP levels (emergency→CRITICAL, error→ERROR,
warning→WARNING, notice/info→INFO, debug→DEBUG) to Python logging.

## MCPEvent Hook Hierarchy

New `src/strands/hooks/mcp_events.py` with typed events:
- `MCPServerConnectedEvent` / `MCPServerDisconnectedEvent`
- `MCPToolCallStartEvent` / `MCPToolCallEndEvent`
- `MCPLoggingEvent`, `MCPProgressEvent`, `MCPSamplingRequestEvent`

All events extend `BaseHookEvent` and work with `HookRegistry`.

## Config Loading

New `load_mcp_servers()` utility function in `mcp_config.py`:
- Load from JSON file, JSON string, or dict
- Standard `mcpServers` format (Claude Desktop compatible)
- Supports `disabled`, `disabledTools`, `prefix`, stdio and HTTP transports
- Shared callbacks applied to all created MCPClient instances

## Option B Design

- NO Agent changes - users wire callbacks manually on MCPClient
- No MCPRegistry, no MCPPlugin, no Agent modifications
- Minimal, composable, pass-through approach

## Tests

44 new tests covering all changes:
- _meta extraction (4 tests)
- isError field (3 tests)
- Callback wiring (6 tests)
- Default logging callback (5 tests)
- Config loading (15 tests)
- MCP events (9 tests)
- MCPToolResult type (2 tests)

Updated 10 existing tests to account for new call_tool() kwargs.
Bug fixes:
- fix(mcp_config): load_mcp_servers(logging_callback=None) now correctly
  disables logging instead of falling back to MCPClient default. Uses
  sentinel _UNSET to distinguish 'not provided' from 'explicitly None'.

Adversarial tests (57 new tests):
- _meta extraction edge cases (None, string, empty dict)
- Default logging callback with non-string data
- Config loading boundary conditions
- isError backward compatibility
- MCP event immutability and defaults
- stop() without start()
- __init__.py export completeness
- HookRegistry integration with reverse callback ordering
- Simultaneous progress callbacks
- Structured content and metadata pass-through

Contract violations documented (not fixed, as behavior may be intentional):
- Docstring claims FileNotFoundError for missing paths, but ValueError raised
- Docstring claims ValueError for missing mcpServers key, but returns []
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant