Files
invariant-gateway/tests/integration/resources/mcp/stdio/client/main.py
T
Luca Beurer-Kellner e18c6b5bdb Add an option to add extra metadata that is pushed and passed to Guardrails during an MCP session (#47)
* use select() before readline

* support for setting static metadata for MCP sessions

* nest extra mcp metadata in metadata object

* unify session metadata

* extra metadata tests

* use empty object as parameters, if None

* list_tools as tool call

* offset indices in tests

* test: adjust addresses

* mcp: make error reporting configurable

* line logging

* log version

* verbose logging + loud exception failure

* add server and client name to policy get

* append trace even if not pushing

* port tools/list message support to SSE

* use python -m build

* adjust guardrail failure address

* support for blocking tools/list in SSE

* use error-based failure response format by default

* tools/list test

* don't list_tools in stdio connect

* flaky test: handle second possible result in anthropic streaming case

---------

Co-authored-by: knielsen404 <kristian@invariantlabs.ai>
2025-05-19 13:44:37 +02:00

146 lines
4.4 KiB
Python

"""A MCP client implementation that interacts with MCP server to make tool calls."""
import os
from datetime import timedelta
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 stdio 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,
metadata_keys: Optional[dict[str, str]] = None,
):
"""
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,
]
# add metadata cli args
if metadata_keys is not None:
for key, value in metadata_keys.items():
args.append("--metadata-" + key + "=" + value)
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, read_timeout_seconds=timedelta(seconds=15)
)
)
# initialize the session
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
"""
# 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],
metadata_keys: Optional[dict[str, str]] = None,
) -> 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,
metadata_keys=metadata_keys
)
listed_tools = await client.session.list_tools()
if tool_name == "tools/list":
# list tools
return listed_tools
else:
return await client.call_tool(tool_name, tool_args)
finally:
await client.cleanup()