mirror of
https://github.com/sstent/AICycling_mcp.git
synced 2026-02-13 19:06:39 +00:00
sync - changing tack to pydantic_ai
This commit is contained in:
@@ -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())
|
||||||
|
|||||||
@@ -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
|
||||||
Reference in New Issue
Block a user