diff --git a/sandbox/pyproject.toml b/sandbox/pyproject.toml index 2d0a5e7..d556781 100644 --- a/sandbox/pyproject.toml +++ b/sandbox/pyproject.toml @@ -14,7 +14,7 @@ authors = [ ] dependencies = [ "httpx", - "mcp", + "mcp>=1.0.0", "websockets", "plaid-python" ] diff --git a/sandbox/src/mcp_server_plaid/server.py b/sandbox/src/mcp_server_plaid/server.py index 3ea21af..f3a6ffb 100644 --- a/sandbox/src/mcp_server_plaid/server.py +++ b/sandbox/src/mcp_server_plaid/server.py @@ -2,7 +2,12 @@ Plaid MCP Server Implementation. This module implements a Model Context Protocol (MCP) server for Plaid API integration. -It allows AI assistants to interact with Plaid's financial data APIs through the MCP protocol. +It uses a robust detection mechanism to support both MCP 1.x (decorator API) and MCP 2.x +(callback API). + +Feature Detection Strategy: + - Checks for Server.list_tools decorator (MCP 1.x only) + - If absent, uses Server on_list_tools/on_call_tool callbacks (MCP 2.x) """ import asyncio @@ -15,8 +20,8 @@ import mcp.types as types import plaid from mcp.server import NotificationOptions -from mcp.server import Server from mcp.server.models import InitializationOptions +from mcp.server import Server as _PlaidServer # Server may be legacy or lowlevel from plaid.api import plaid_api from mcp_server_plaid.clients.bill import AskBillClient @@ -33,12 +38,18 @@ REQUEST_TIMEOUT = 30.0 -async def serve(client_id: str, secret: str, enabled_categories: str) -> Server: - """Initialize and configure the MCP server with Plaid tools.""" - server = Server("plaid") +# ── Robust Feature Detection ───────────────────────────────────────── - ask_bill_client = AskBillClient("wss://hello-finn.herokuapp.com/") +# Use method-level detection (hasattr). +# Server.list_tools exists as a decorator in MCP 1.x, but is removed +# in MCP 2.x (replaced by on_list_tools/on_call_tool callback params). +_HAS_DECORATOR_API = hasattr(_PlaidServer, "list_tools") + +# ── Helper: Build Plaid clients (shared between API versions) ────────── +def _make_clients(client_id, secret, enabled_categories): + """Create shared Plaid and Bill clients with tool registry.""" + # Plaid client configuration = plaid.Configuration( host=plaid.Environment.Sandbox, api_key={ @@ -48,30 +59,85 @@ async def serve(client_id: str, secret: str, enabled_categories: str) -> Server: ) plaid_client = plaid_api.PlaidApi(plaid.ApiClient(configuration)) + # Bill client (AskBill WebSocket) + ask_bill_client = AskBillClient("wss://hello-finn.herokuapp.com/") + + # Tool registry (only created once, cached by singleton) tool_registry = register_all_tools(enabled_categories) + return plaid_client, ask_bill_client, tool_registry + + +# ── MCP 2.x (callback) Server ───────────────────────────────────────── + +def _build_new_api_server(client_id, secret, enabled_categories): + """Create server for MCP 2.x using lowlevel callback API.""" + plaid_client, ask_bill_client, tool_registry = _make_clients( + client_id, secret, enabled_categories + ) + + def _make_on_list_tools(): + async def on_list_tools(ctx, params): + return types.ListToolsResult(tools=tool_registry.get_tools()) + return on_list_tools + + def _make_on_call_tool(): + async def on_call_tool(ctx, params): + name = params.name + arguments = params.arguments or {} + + if not tool_registry.has_tool(name): + raise ValueError(f"Unknown tool: {name}") + + handler = tool_registry.get_handler(name) + if handler is None: + raise ValueError(f"No handler registered for tool: {name}") + + result = await handler( + arguments, + bill_client=ask_bill_client, + plaid_client=plaid_client, + ) + return types.CallToolResult(content=result, is_error=False) + + return on_call_tool + + # In MCP 2.x, Server is lowlevel.Server with on_list_tools/on_call_tool params + server = _PlaidServer( + "plaid", + on_list_tools=_make_on_list_tools(), + on_call_tool=_make_on_call_tool(), + ) + return server + + +# ── MCP 1.x (decorator) Server ──────────────────────────────────────── + +def _build_old_api_server(client_id, secret, enabled_categories): + """Create server for MCP 1.x using legacy decorator API.""" + plaid_client, ask_bill_client, tool_registry = _make_clients( + client_id, secret, enabled_categories + ) + + server = _PlaidServer("plaid") + @server.list_tools() async def handle_list_tools() -> List[types.Tool]: - """Handler for the call_tool MCP method.""" return tool_registry.get_tools() @server.call_tool() async def handle_call_tool( - name: str, arguments: Dict[str, Any] | None + name: str, arguments: Dict[str, Any] | None ) -> List[types.TextContent]: - """Handler for the call_tool MCP method.""" - # Validate tool exists if not tool_registry.has_tool(name): raise ValueError(f"Unknown tool: {name}") - # Get the handler for the tool handler = tool_registry.get_handler(name) if handler is None: raise ValueError(f"No handler registered for tool: {name}") - # Call the handler with the arguments and context return await handler( - arguments or {}, # Ensure arguments is not None + arguments or {}, bill_client=ask_bill_client, plaid_client=plaid_client, ) @@ -79,6 +145,23 @@ async def handle_call_tool( return server +# ── Common server builder ───────────────────────────────────────────── + +def serve(client_id: str, secret: str, enabled_categories: str): + """ + Initialize and configure the MCP server with Plaid tools. + + Automatically detects MCP version (1.x vs 2.x) and returns + the appropriate server implementation. + """ + if _HAS_DECORATOR_API: + return _build_old_api_server(client_id, secret, enabled_categories) + else: + return _build_new_api_server(client_id, secret, enabled_categories) + + +# ── Main (runs the server after initialisation) ────────────────────── + @click.command() @click.option("--client-id", type=str, help="Plaid client ID", envvar="PLAID_CLIENT_ID", required=True) @click.option("--secret", type=str, help="Plaid secret", envvar="PLAID_SECRET", required=True) @@ -86,7 +169,8 @@ async def handle_call_tool( envvar="TOOLS_TO_ENABLE") def main(client_id: str, secret: str, enabled_categories: str): """Entry point for the MCP server.""" - # Validate required environment variables + logger.info("Using MCP API: %s", "new (callbacks)" if not _HAS_DECORATOR_API else "old (decorators)") + if not client_id or not secret: logger.error( "PLAID_CLIENT_ID and PLAID_SECRET environment variables must be set" @@ -96,18 +180,22 @@ def main(client_id: str, secret: str, enabled_categories: str): async def _run(): logger.info("Setting up stdio communication channels") async with mcp.server.stdio.stdio_server() as (read_stream, write_stream): - server = await serve(client_id, secret, enabled_categories) - await server.run( - read_stream, - write_stream, - InitializationOptions( + server = serve(client_id, secret, enabled_categories) + + if _HAS_DECORATOR_API: + # MCP 1.x: use get_capabilities + InitializationOptions manually + init_options = InitializationOptions( server_name="plaid", server_version=__version__, capabilities=server.get_capabilities( notification_options=NotificationOptions(), experimental_capabilities={}, ), - ), - ) + ) + else: + # MCP 2.x: use create_initialization_options() + init_options = server.create_initialization_options() + + await server.run(read_stream, write_stream, init_options) asyncio.run(_run())