=== NEW SERVERS ADDED (7) === - servers/closebot — 119 tools, 14 modules, 4,656 lines TS (Stage 7) - servers/google-console — Google Search Console MCP (Stage 7) - servers/meta-ads — Meta/Facebook Ads MCP (Stage 8) - servers/twilio — Twilio communications MCP (Stage 8) - servers/competitor-research — Competitive intel MCP (Stage 6) - servers/n8n-apps — n8n workflow MCP apps (Stage 6) - servers/reonomy — Commercial real estate MCP (Stage 1) === FACTORY INFRASTRUCTURE ADDED === - infra/factory-tools — mcp-jest, mcp-validator, mcp-add, MCP Inspector - 60 test configs, 702 auto-generated test cases - All 30 servers score 100/100 protocol compliance - infra/command-center — Pipeline state, operator playbook, dashboard config - infra/factory-reviews — Automated eval reports === DOCS ADDED === - docs/MCP-FACTORY.md — Factory overview - docs/reports/ — 5 pipeline evaluation reports - docs/research/ — Browser MCP research === RULES ESTABLISHED === - CONTRIBUTING.md — All MCP work MUST go in this repo - README.md — Full inventory of 37 servers + infra docs - .gitignore — Updated for Python venvs TOTAL: 37 MCP servers + full factory pipeline in one repo. This is now the single source of truth for all MCP work.
199 lines
5.8 KiB
Python
199 lines
5.8 KiB
Python
#!/usr/bin/env python3
|
|
# SPDX-License-Identifier: AGPL-3.0-or-later
|
|
|
|
"""
|
|
Base server adapter for MCP testing.
|
|
|
|
This module defines the base server adapter class that will be extended
|
|
by specific server implementations.
|
|
"""
|
|
|
|
import asyncio
|
|
import json
|
|
import logging
|
|
from abc import ABC, abstractmethod
|
|
from typing import Dict, Any, List, Optional, Tuple, Union
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
class MCPServerAdapter(ABC):
|
|
"""
|
|
Base class for MCP server adapters.
|
|
|
|
Server adapters are responsible for starting, communicating with, and stopping
|
|
MCP servers during testing. This abstract base class defines the interface that
|
|
all server adapters must implement.
|
|
"""
|
|
|
|
def __init__(self, protocol_version: str, debug: bool = False):
|
|
"""
|
|
Initialize a server adapter.
|
|
|
|
Args:
|
|
protocol_version: The MCP protocol version to use
|
|
debug: Whether to enable debug logging
|
|
"""
|
|
self.protocol_version = protocol_version
|
|
self.debug = debug
|
|
self.server_info = None
|
|
self._request_id = 0
|
|
|
|
@abstractmethod
|
|
async def start(self) -> bool:
|
|
"""
|
|
Start the server.
|
|
|
|
Returns:
|
|
True if started successfully, False otherwise
|
|
"""
|
|
pass
|
|
|
|
@abstractmethod
|
|
async def stop(self) -> bool:
|
|
"""
|
|
Stop the server.
|
|
|
|
Returns:
|
|
True if stopped successfully, False otherwise
|
|
"""
|
|
pass
|
|
|
|
@abstractmethod
|
|
async def send_request(self, method: str, params: Dict[str, Any] = None) -> Dict[str, Any]:
|
|
"""
|
|
Send a request to the server and wait for a response.
|
|
|
|
Args:
|
|
method: The JSON-RPC method name
|
|
params: The method parameters
|
|
|
|
Returns:
|
|
The server's response
|
|
|
|
Raises:
|
|
RuntimeError: If the server is not started or the request fails
|
|
"""
|
|
pass
|
|
|
|
@abstractmethod
|
|
async def send_notification(self, method: str, params: Dict[str, Any] = None) -> None:
|
|
"""
|
|
Send a notification to the server (no response expected).
|
|
|
|
Args:
|
|
method: The JSON-RPC method name
|
|
params: The method parameters
|
|
|
|
Raises:
|
|
RuntimeError: If the server is not started or the notification fails
|
|
"""
|
|
pass
|
|
|
|
async def initialize(self) -> Dict[str, Any]:
|
|
"""
|
|
Initialize the server.
|
|
|
|
This sends the standard initialize request to the server.
|
|
|
|
Returns:
|
|
The server's initialization response
|
|
|
|
Raises:
|
|
RuntimeError: If initialization fails
|
|
"""
|
|
params = {
|
|
"protocolVersion": self.protocol_version,
|
|
"options": {}
|
|
}
|
|
|
|
response = await self.send_request("initialize", params)
|
|
|
|
if "error" in response:
|
|
error_msg = response.get("error", {}).get("message", "Unknown error")
|
|
logger.error(f"Server initialization failed: {error_msg}")
|
|
raise RuntimeError(f"Failed to initialize server: {error_msg}")
|
|
|
|
if "result" not in response:
|
|
raise RuntimeError("Invalid initialize response, missing 'result' field")
|
|
|
|
self.server_info = response["result"]
|
|
return response
|
|
|
|
async def shutdown(self) -> Optional[Dict[str, Any]]:
|
|
"""
|
|
Send a shutdown request to the server.
|
|
|
|
Returns:
|
|
The server's shutdown response, or None if the server doesn't support shutdown
|
|
"""
|
|
try:
|
|
response = await self.send_request("shutdown", {})
|
|
await self.send_notification("exit")
|
|
return response
|
|
except Exception as e:
|
|
logger.warning(f"Failed to shut down server: {str(e)}")
|
|
return None
|
|
|
|
async def list_tools(self) -> List[Dict[str, Any]]:
|
|
"""
|
|
Get the list of available tools from the server.
|
|
|
|
Returns:
|
|
A list of tool definitions
|
|
|
|
Raises:
|
|
RuntimeError: If the request fails
|
|
"""
|
|
response = await self.send_request("tools/list", {})
|
|
|
|
if "error" in response:
|
|
error_msg = response.get("error", {}).get("message", "Unknown error")
|
|
raise RuntimeError(f"Failed to list tools: {error_msg}")
|
|
|
|
if "result" not in response:
|
|
raise RuntimeError("Invalid tools/list response, missing 'result' field")
|
|
|
|
return response["result"]
|
|
|
|
async def call_tool(self, name: str, params: Dict[str, Any]) -> Dict[str, Any]:
|
|
"""
|
|
Call a tool on the server.
|
|
|
|
Args:
|
|
name: The name of the tool to call
|
|
params: The tool parameters
|
|
|
|
Returns:
|
|
The tool's response
|
|
|
|
Raises:
|
|
RuntimeError: If the tool call fails
|
|
"""
|
|
request_params = {
|
|
"name": name,
|
|
"params": params
|
|
}
|
|
|
|
response = await self.send_request("callTool", request_params)
|
|
|
|
if "error" in response:
|
|
error = response.get("error", {})
|
|
error_msg = error.get("message", "Unknown error")
|
|
logger.error(f"Tool call failed: {error_msg}")
|
|
return response
|
|
|
|
if "result" not in response:
|
|
logger.error("Invalid callTool response, missing 'result' field")
|
|
|
|
return response
|
|
|
|
def _get_next_request_id(self) -> int:
|
|
"""
|
|
Get the next request ID.
|
|
|
|
Returns:
|
|
The next request ID
|
|
"""
|
|
self._request_id += 1
|
|
return self._request_id |