sync - changing tack to pydantic_ai

This commit is contained in:
2025-09-23 13:19:58 -07:00
parent 2cc4baca14
commit eea83e9c83
3 changed files with 496 additions and 645 deletions

903
main.py

File diff suppressed because it is too large Load Diff

View File

@@ -9,131 +9,26 @@ import platform
import subprocess import subprocess
import sys import sys
import time import time
import yaml
import os
import shutil
import logging
from typing import Dict, List, Any, Optional from typing import Dict, List, Any, Optional
class MCPClient: # MCP Protocol imports
def __init__(self, server_command: List[str]): try:
self.server_command = server_command from mcp import ClientSession, StdioServerParameters
self.process = None from mcp.client.stdio import stdio_client
self.request_id = 1 MCP_AVAILABLE = True
except ImportError:
async def start_server(self): MCP_AVAILABLE = False
"""Start the MCP server process.""" print("MCP not available. Install with: pip install mcp")
print(f"Starting MCP server: {' '.join(self.server_command)}")
print(f"Python version: {platform.python_version()}")
self.process = await asyncio.create_subprocess_exec(
*self.server_command,
stdin=asyncio.subprocess.PIPE,
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE
)
# Give the server a moment to start
await asyncio.sleep(0.5)
# Debug: show process object type
print(f"Process object type: {type(self.process)}")
# Check if process has terminated
if self.process.returncode is not None:
stderr = await self.process.stderr.read()
raise Exception(f"Server failed to start. Error: {stderr.decode()}")
print("Server started successfully")
async def send_request(self, method: str, params: Optional[Dict] = None) -> Dict[str, Any]:
"""Send a JSON-RPC request to the MCP server."""
if not self.process:
raise Exception("Server not started")
request = {
"jsonrpc": "2.0",
"id": self.request_id,
"method": method
}
if params is not None:
request["params"] = params
self.request_id += 1
# Send request
request_json = json.dumps(request) + "\n"
print(f"Sending request: {request_json.strip()}")
self.process.stdin.write(request_json.encode())
await self.process.stdin.drain()
# Read response
response_line = await self.process.stdout.readline()
if not response_line:
raise Exception("No response from server")
try:
response_str = response_line.decode().strip()
print(f"Received response: {response_str}")
response = json.loads(response_str)
return response
except json.JSONDecodeError as e:
raise Exception(f"Invalid JSON response: {e}. Response: {response_str}")
async def initialize(self):
"""Initialize the MCP server."""
print("Initializing server...")
try:
response = await self.send_request("initialize", {
"protocolVersion": "2024-11-05",
"capabilities": {
"tools": {}
},
"clientInfo": {
"name": "mcp-tool-lister",
"version": "1.0.0"
}
})
if "error" in response:
raise Exception(f"Initialization failed: {response['error']}")
print("Server initialized successfully")
return response.get("result", {})
except Exception as e:
print(f"Initialization error: {str(e)}")
raise
async def list_tools(self) -> List[Dict[str, Any]]:
"""List available tools from the MCP server."""
print("Requesting tools list...")
try:
# Pass empty parameters object to satisfy server requirements
response = await self.send_request("tools/list", {})
if "error" in response:
raise Exception(f"Failed to list tools: {response['error']}")
tools = response.get("result", {}).get("tools", [])
print(f"Found {len(tools)} tools")
return tools
except Exception as e:
print(f"Tool listing error: {str(e)}")
raise
async def stop_server(self):
"""Stop the MCP server process."""
if self.process:
print("Stopping server...")
self.process.terminate()
try:
await asyncio.wait_for(self.process.wait(), timeout=5.0)
except asyncio.TimeoutError:
print("Server didn't stop gracefully, killing...")
self.process.kill()
await self.process.wait()
print("Server stopped")
def print_tools(tools: List[Dict[str, Any]]): # Configure logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
def print_tools(tools: List[Any]):
"""Pretty print the tools list.""" """Pretty print the tools list."""
if not tools: if not tools:
print("\nNo tools available.") print("\nNo tools available.")
@@ -141,17 +36,17 @@ def print_tools(tools: List[Dict[str, Any]]):
print(f"\n{'='*60}") print(f"\n{'='*60}")
print("AVAILABLE TOOLS") print("AVAILABLE TOOLS")
print(f"{'='*60}") print(f"\n{'='*60}")
for i, tool in enumerate(tools, 1): for i, tool in enumerate(tools, 1):
name = tool.get("name", "Unknown") name = tool.name
description = tool.get("description", "No description available") description = tool.description if hasattr(tool, 'description') else 'No description available'
print(f"\n{i}. {name}") print(f"\n{i}. {name}")
print(f" Description: {description}") print(f" Description: {description}")
# Print input schema if available # Print input schema if available
input_schema = tool.get("inputSchema", {}) input_schema = tool.input_schema if hasattr(tool, 'input_schema') else {}
if input_schema: if input_schema:
properties = input_schema.get("properties", {}) properties = input_schema.get("properties", {})
if properties: if properties:
@@ -166,39 +61,77 @@ def print_tools(tools: List[Dict[str, Any]]):
print(f"\n{'='*60}") print(f"\n{'='*60}")
async def main(): async def main():
if not MCP_AVAILABLE:
sys.exit(1)
if len(sys.argv) < 2: if len(sys.argv) < 2:
print("Usage: python mcp_tool_lister.py <server_command> [args...]") print("Usage: python mcp_tool_lister.py <server_command> [args...]")
print("Example: python mcp_tool_lister.py uvx my-mcp-server") print("Example: python mcp_tool_lister.py uvx garth-mcp-server")
sys.exit(1) sys.exit(1)
server_command = sys.argv[1:] server_command_args = sys.argv[1:]
client = MCPClient(server_command)
# Load config
with open("config.yaml") as f:
config_data = yaml.safe_load(f)
garth_token = config_data.get("garth_token")
if not garth_token:
print("Error: garth_token not found in config.yaml")
sys.exit(1)
env = os.environ.copy()
env["GARTH_TOKEN"] = garth_token
server_command = shutil.which(server_command_args[0])
if not server_command:
logger.error(f"Could not find '{server_command_args[0]}' in your PATH.")
raise FileNotFoundError(f"{server_command_args[0]} not found")
server_params = StdioServerParameters(
command="/bin/bash",
args=["-c", f"exec {' '.join(server_command_args)} 1>&2"],
capture_stderr=True,
env=env,
)
async def log_stderr(stderr):
async for line in stderr:
logger.info(f"[server-stderr] {line.decode().strip()}")
client_context = None
try: try:
# Start and initialize the server logger.info(f"Starting MCP server: {' '.join(server_command_args)}")
await client.start_server() client_context = stdio_client(server_params)
init_result = await client.initialize() streams = await client_context.__aenter__()
if len(streams) == 3:
read_stream, write_stream, stderr_stream = streams
stderr_task = asyncio.create_task(log_stderr(stderr_stream))
else:
read_stream, write_stream = streams
stderr_task = None
session = ClientSession(read_stream, write_stream)
await session.initialize()
# Print server info server_info = session.server_info
server_info = init_result.get("serverInfo", {})
if server_info: if server_info:
print(f"Server: {server_info.get('name', 'Unknown')} v{server_info.get('version', 'Unknown')}") print(f"Server: {server_info.name} v{server_info.version}")
tools_result = await session.list_tools() # Corrected from list__tools()
tools = tools_result.tools if tools_result else []
capabilities = init_result.get("capabilities", {})
if capabilities:
print(f"Server capabilities: {', '.join(capabilities.keys())}")
# List and display tools
tools = await client.list_tools()
print_tools(tools) print_tools(tools)
except KeyboardInterrupt: if stderr_task:
print("\nInterrupted by user") stderr_task.cancel()
except Exception as e: except Exception as e:
print(f"Error: {e}") print(f"Error: {e}")
sys.exit(1) sys.exit(1)
finally: finally:
await client.stop_server() if client_context:
await client_context.__aexit__(None, None, None)
if __name__ == "__main__": if __name__ == "__main__":
asyncio.run(main()) asyncio.run(main())

View File

@@ -1,13 +1,22 @@
aiohttp>=3.8.0 aiohttp>=3.8.0
pyyaml>=6.0 pyyaml>=6.0
mcp>=0.1.0 mcp>=0.1.0
pydantic-ai>=0.0.1
# Pydantic AI dependencies
pydantic>=2.0.0
openai>=1.0.0 # Required for AsyncOpenAI client
# Built-in modules (no installation needed) # Built-in modules (no installation needed)
# asyncio # asyncio
# pathlib # pathlib
# dataclasses # dataclasses
# logging # logging
# For direct Garth MCP server integration # For direct Garth MCP server integration
# Note: You need to install and set up the garth-mcp-server separately # Note: You need to install and set up the garth-mcp-server separately
# Follow: https://github.com/matin/garth-mcp-server # Follow: https://github.com/matin/garth-mcp-server
# Installation commands:
# pip install pydantic-ai
# npm install -g garth-mcp-server