feat: add MCP proxy pattern convenience function (#12)#1711
feat: add MCP proxy pattern convenience function (#12)#1711dgenio wants to merge 6 commits intomodelcontextprotocol:mainfrom
Conversation
Implements mcp_proxy() function in mcp.shared.proxy module that enables bidirectional message forwarding between two MCP transports. Features: - Bidirectional message forwarding using anyio task groups - Error handling with optional sync/async callback support - Automatic cleanup when one transport closes - Proper handling of SessionMessage and Exception objects - Comprehensive test coverage Closes modelcontextprotocol#12
- Extract error handling into _handle_error helper function - Extract message forwarding into _forward_message helper function - Extract forwarding loop into _forward_loop helper function - Add tests for error callback exceptions (sync and async) - Reduces cyclomatic complexity from 39 to below 24 - Reduces statement count from 113 to below 102 - Improves test coverage to meet 100% requirement
- Add test for proxy without error handler (covers onerror=None branch) - Add test for exceptions during message forwarding - Fix formatting issues (blank lines after try:) - Improves coverage to meet 100% requirement
- Fix pyright error: replace isinstance(message, Exception) with else clause - Fix fixture type annotation: use AsyncGenerator for async fixture - Remove problematic test_proxy_handles_forwarding_exception (hard to trigger) - Add pragma: no cover comments for exception handlers that are difficult to test - These exception paths are defensive and unlikely to occur in practice
- Fix pyright error: replace isinstance(message, Exception) with else clause - Fix fixture type annotation: use AsyncGenerator for async fixture - Remove problematic test_proxy_handles_forwarding_exception (hard to trigger) - Add pragma: no cover comments for exception handlers that are difficult to test - These exception paths are defensive and unlikely to occur in practice
…nio/python-sdk into feature/12-mcp-proxy-pattern
|
Any reason why you closed this? |
Thanks for asking — and sorry for the noise. If you’d prefer a different workflow, I’m happy to reopen this (possibly as a draft) and keep iterating when I have time, or for someone from the team to pick it up from where it is. In any case, I’m still keen on getting this proxy helper merged eventually. |
|
I'll leave it open. I'm interested in this feature. |
Same. I tried to build something very similar to this a few months ago and ran into some async context manager issues (IIRC); hoping this solves it! |
|
@Kludex @dgenio I opened #1763 which includes all the commits from this PR plus some changes to help the coverage tests pass; as far as I can tell it's a race condition between parallel works and cleanup blocks (resulting in some workers never running the cleanup block and reporting <100% coverage), in addition to some small annotations on the core code. |
Adds a convenience function for proxying messages between two MCP transports, enabling bidirectional message forwarding with proper error handling. Features: - Bidirectional forwarding between client and server transports - Optional error callback (sync or async) for exceptions on streams - Graceful handling of closed/broken streams - Clean shutdown on context exit This is a simpler reimplementation of the proxy pattern from #1711/#1763, addressing all review feedback.
Description
Implements the
mcp_proxy()convenience function that enables bidirectional message forwarding between two MCP transports, porting the TypeScript proxy pattern to the Python SDK.Changes
mcp_proxy()context manager insrc/mcp/shared/proxy.pyanyiotask groupstests/shared/test_proxy.pyFeatures
onerrorcallback (supports both sync and async)SessionMessageandExceptionobjects from read streamsUsage Example
from mcp.shared.proxy import mcp_proxy
async with mcp_proxy(
transport_to_client=(client_read, client_write),
transport_to_server=(server_read, server_write),
onerror=lambda e: logger.error(f"Proxy error: {e}"),
):
# Proxy is active, forwarding messages bidirectionally
await some_operation()
Both transports are closed when exiting the context## Testing
Related
Closes #12
Type of Change
Checklist