Jake Shore f3c4cd817b Add all MCP servers + factory infra to MCPEngine — 2026-02-06
=== 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.
2026-02-06 06:32:29 -05:00

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