"""A MCP client implementation that interacts with MCP server to make tool calls.""" import os import time from contextlib import AsyncExitStack from typing import Any, Optional from mcp import ClientSession, StdioServerParameters, types from mcp.client.stdio import stdio_client class MCPClient: """MCP Client for interacting with a MCP server and processing queries""" def __init__(self): self.session: Optional[ClientSession] = None self.exit_stack = AsyncExitStack() async def connect_to_server( self, invariant_gateway_package_whl_file: str, project_name: str, server_script_path: str, push_to_explorer: bool, ): """ Connect to an MCP server. Args: invariant_gateway_package_whl_file: Path to the Invariant Gateway package .whl file project_name: Name of the project in Invariant Explorer server_script_path: Path to the server script push_to_explorer: Whether to push traces to the Invariant Explorer """ args = [ "--from", invariant_gateway_package_whl_file, "invariant-gateway", "mcp", "--project-name", project_name, ] if push_to_explorer: args.append("--push-explorer") args.extend( [ "--exec", "uv", "--directory", os.path.abspath(os.path.dirname(server_script_path)), "run", os.path.basename(server_script_path), ] ) server_params = StdioServerParameters( command="uvx", args=args, env={ "INVARIANT_API_KEY": os.environ.get("INVARIANT_API_KEY"), "INVARIANT_API_URL": "http://invariant-gateway-test-explorer-app-api:8000", }, ) stdio_transport = await self.exit_stack.enter_async_context( stdio_client(server_params) ) self.stdio, self.write = stdio_transport self.session = await self.exit_stack.enter_async_context( ClientSession(self.stdio, self.write) ) await self.session.initialize() async def call_tool( self, tool_name: str, tool_args: dict[str, Any] ) -> types.CallToolResult: """ Make a tool call on the MCP server. Args: tool_name: Name of the tool to call tool_args: Arguments for the tool call """ response = await self.session.list_tools() if tool_name not in [tool.name for tool in response.tools]: raise ValueError(f"Tool '{tool_name}' not found in available tools") # Execute tool call result = await self.session.call_tool(tool_name, tool_args) return result async def cleanup(self): """Clean up resources""" await self.exit_stack.aclose() async def run( invariant_gateway_package_whl_file: str, project_name: str, server_script_path: str, push_to_explorer: bool, tool_name: str, tool_args: dict[str, Any], ) -> types.CallToolResult: """ Main function to setup the MCP client and server. It calls a tool on the server with the given args. Args: invariant_gateway_package_whl_file: Path to the Invariant Gateway package .whl file project_name: Name of the project in Invariant Explorer server_script_path: Path to the server script push_to_explorer: Whether to push traces to the Invariant Explorer tool_name: Name of the tool to call tool_args: Arguments for the tool call """ client = MCPClient() try: await client.connect_to_server( invariant_gateway_package_whl_file, project_name, server_script_path, push_to_explorer, ) return await client.call_tool(tool_name, tool_args) finally: # Sleep for a while to allow the server to process the background tasks # like pushing traces to the explorer time.sleep(2) await client.cleanup()