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

449 lines
16 KiB
Python

"""
MCP STDIO Tester module for testing STDIO server implementations.
"""
import json
import logging
import shlex
import subprocess
import time
from typing import Any, Dict, List, Optional, Tuple, Union
from mcp_testing.stdio.utils import check_command_exists, verify_python_server
# Configure logging
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
logger = logging.getLogger("MCPStdioTester")
class MCPStdioTester:
"""Tester for MCP STDIO server implementations."""
def __init__(self, server_command: str, args: List[str] = None, debug: bool = False):
"""Initialize the tester.
Args:
server_command: Command to run the server
args: Additional arguments to pass to the server command
debug: Enable debug output
"""
self.server_command = server_command
self.args = args or []
self.debug = debug
self.protocol_version = "2025-03-26"
self.server_process = None
self.client_id = 1
self.session_id = None
# Configure logging based on debug flag
if debug:
logger.setLevel(logging.DEBUG)
# Only log if debug is enabled and using the instance args (which is guaranteed to be a list)
logger.debug(f"Initialized tester with command: {server_command} {' '.join(self.args)}")
def start_server(self) -> bool:
"""Start the server process.
Returns:
True if server started successfully, False otherwise
"""
try:
# Check if the command exists
cmd_parts = shlex.split(self.server_command)
if not check_command_exists(cmd_parts[0]):
logger.error(f"Command not found: {cmd_parts[0]}")
return False
# If it's a Python server, verify it exists and is valid
if cmd_parts[0] in ["python", "python3"] and len(cmd_parts) > 1:
server_script = cmd_parts[1]
if not verify_python_server(server_script):
return False
# Build command
cmd = cmd_parts + self.args
logger.debug(f"Starting server with command: {' '.join(cmd)}")
# Start server process with pipes for stdin/stdout
self.server_process = subprocess.Popen(
cmd,
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
bufsize=1 # Line buffered
)
# Wait a short time for server to start
time.sleep(0.5)
if self.server_process.poll() is not None:
# Server exited prematurely
returncode = self.server_process.poll()
stderr = self.server_process.stderr.read()
logger.error(f"Server exited with code {returncode}. Error: {stderr}")
return False
logger.info("Server started successfully")
return True
except Exception as e:
logger.error(f"Failed to start server: {e}")
return False
def stop_server(self) -> None:
"""Stop the server process."""
if self.server_process:
try:
logger.debug("Sending shutdown request to server")
# Send shutdown request if possible
try:
self._send_request("shutdown", {})
except Exception:
pass
logger.debug("Terminating server process")
self.server_process.terminate()
# Wait for process to terminate
try:
self.server_process.wait(timeout=2)
except subprocess.TimeoutExpired:
logger.warning("Server did not terminate within timeout, killing forcefully")
self.server_process.kill()
# Close pipes
self.server_process.stdin.close()
self.server_process.stdout.close()
self.server_process.stderr.close()
logger.info("Server stopped")
except Exception as e:
logger.error(f"Error stopping server: {e}")
self.server_process = None
def _send_request(self, method: str, params: Dict[str, Any], request_id: Optional[int] = None) -> Tuple[bool, Dict[str, Any]]:
"""Send a request to the server and receive a response.
Args:
method: Method name
params: Method parameters
request_id: Request ID (generated if None)
Returns:
Tuple of (success, response)
"""
if not self.server_process:
logger.error("Cannot send request - server not running")
return False, {"error": "Server not running"}
# Generate request ID if not provided
if request_id is None:
request_id = self.client_id
self.client_id += 1
# Build request object
request = {
"jsonrpc": "2.0",
"method": method,
"params": params,
"id": request_id
}
# Add session ID if we have one
if self.session_id and method != "initialize":
request["sessionId"] = self.session_id
# Convert to JSON and send
request_json = json.dumps(request)
if self.debug:
logger.debug(f"Sending request: {request_json}")
try:
# Send request with newline
self.server_process.stdin.write(request_json + "\n")
self.server_process.stdin.flush()
# Read response
response_json = self.server_process.stdout.readline()
if not response_json:
logger.error("Server closed connection without sending a response")
return False, {"error": "No response received"}
# Parse response
response = json.loads(response_json)
if self.debug:
logger.debug(f"Received response: {response_json}")
# Check for errors
if "error" in response:
logger.error(f"Server returned error: {response['error']}")
return False, response
return True, response
except Exception as e:
logger.error(f"Error communicating with server: {e}")
return False, {"error": str(e)}
def initialize(self) -> bool:
"""Initialize the server.
Returns:
True if initialization successful, False otherwise
"""
params = {
"protocolVersion": self.protocol_version,
"clientInfo": {
"name": "MCP STDIO Tester",
"version": "1.0.0"
},
"capabilities": {
"tools": {
"asyncSupported": True
}
}
}
logger.info("Initializing server")
success, response = self._send_request("initialize", params)
if success and "result" in response:
# Store session ID if provided
if "sessionId" in response["result"]:
self.session_id = response["result"]["sessionId"]
logger.info(f"Server initialized with session ID: {self.session_id}")
else:
logger.info("Server initialized (no session ID provided)")
return True
else:
logger.error("Failed to initialize server")
return False
def list_tools(self) -> Tuple[bool, List[Dict[str, Any]]]:
"""List available tools.
Returns:
Tuple of (success, tools_list)
"""
logger.info("Listing tools")
success, response = self._send_request("tools/list", {})
if success and "result" in response and "tools" in response["result"]:
tools = response["result"]["tools"]
logger.info(f"Server reported {len(tools)} available tools")
return True, tools
else:
logger.error("Failed to list tools")
return False, []
def test_echo_tool(self) -> bool:
"""Test the echo tool.
Returns:
True if test passed, False otherwise
"""
test_message = "Hello, MCP STDIO server!"
logger.info(f"Testing echo tool with message: '{test_message}'")
success, response = self._send_request("tools/call", {
"name": "echo",
"arguments": {
"message": test_message
}
})
if success and "result" in response and "content" in response["result"]:
content = response["result"]["content"]
if isinstance(content, dict) and "echo" in content and content["echo"] == test_message:
logger.info("Echo tool test passed")
return True
else:
logger.error(f"Echo tool returned unexpected result: {content}")
return False
else:
logger.error("Failed to invoke echo tool")
return False
def test_add_tool(self) -> bool:
"""Test the add tool.
Returns:
True if test passed, False otherwise
"""
logger.info("Testing add tool with numbers 5 and 7")
success, response = self._send_request("tools/call", {
"name": "add",
"arguments": {
"a": 5,
"b": 7
}
})
if success and "result" in response and "content" in response["result"]:
content = response["result"]["content"]
if isinstance(content, dict) and "sum" in content and content["sum"] == 12:
logger.info("Add tool test passed")
return True
else:
logger.error(f"Add tool returned unexpected result: {content}")
return False
else:
logger.error("Failed to invoke add tool")
return False
def test_async_sleep_tool(self) -> bool:
"""Test the async sleep tool.
Returns:
True if test passed, False otherwise
"""
sleep_duration = 1 # 1 second
logger.info(f"Testing async sleep tool with duration: {sleep_duration}s")
# Start async tool call
success, response = self._send_request("tools/call-async", {
"name": "sleep",
"arguments": {
"duration": sleep_duration
}
})
if not success or "result" not in response or "id" not in response["result"]:
logger.error("Failed to invoke async sleep tool")
return False
# Get tool call ID
tool_call_id = response["result"]["id"]
logger.debug(f"Async tool call started with ID: {tool_call_id}")
# Poll for completion
start_time = time.time()
max_wait = sleep_duration + 3 # Add buffer
while time.time() - start_time < max_wait:
success, response = self._send_request("tools/result", {
"id": tool_call_id
})
if not success or "result" not in response:
logger.error("Failed to get tool call result")
return False
result = response["result"]
if "status" in result:
status = result["status"]
logger.debug(f"Tool call status: {status}")
if status == "completed" and "result" in result:
logger.info("Async sleep tool completed successfully")
return True
if status == "failed":
logger.error("Async sleep tool failed")
return False
elif "content" in result:
# Tool completed and returned result
logger.info("Async sleep tool completed successfully")
return True
# Wait before polling again
time.sleep(0.2)
logger.error("Timed out waiting for async tool to complete")
return False
def run_all_tests(self) -> bool:
"""Run all tests.
Returns:
True if all tests passed, False otherwise
"""
logger.info("Starting MCP STDIO server tests")
try:
# Start server
if not self.start_server():
logger.error("Failed to start server, aborting tests")
return False
# Initialize server
if not self.initialize():
logger.error("Failed to initialize server, aborting tests")
return False
# List tools
success, tools = self.list_tools()
if not success:
logger.error("Failed to list tools, aborting tests")
return False
# Check available tools
tool_names = [tool["name"] for tool in tools]
logger.debug(f"Available tools: {', '.join(tool_names)}")
# Test the first available tool
if tools:
test_tool = tools[0]
tool_name = test_tool["name"]
# Generate test parameters based on schema
test_params = {}
if "parameters" in test_tool: # 2025-03-26
schema = test_tool["parameters"]
if "properties" in schema:
for prop_name, prop_details in schema["properties"].items():
prop_type = prop_details.get("type", "string")
if prop_type == "string":
test_params[prop_name] = "test_value"
elif prop_type in ["number", "integer"]:
test_params[prop_name] = 42
elif prop_type == "boolean":
test_params[prop_name] = True
elif prop_type == "array":
test_params[prop_name] = []
elif prop_type == "object":
test_params[prop_name] = {}
elif "inputSchema" in test_tool: # 2024-11-05
schema = test_tool["inputSchema"]
if "properties" in schema:
for prop_name, prop_details in schema["properties"].items():
prop_type = prop_details.get("type", "string")
if prop_type == "string":
test_params[prop_name] = "test_value"
elif prop_type in ["number", "integer"]:
test_params[prop_name] = 42
elif prop_type == "boolean":
test_params[prop_name] = True
elif prop_type == "array":
test_params[prop_name] = []
elif prop_type == "object":
test_params[prop_name] = {}
# Test the tool
success, response = self._send_request("tools/call", {
"name": tool_name,
"arguments": test_params
})
if not success or "result" not in response:
logger.error(f"Failed to test tool {tool_name}")
return False
logger.info(f"Successfully tested tool: {tool_name}")
return True
except Exception as e:
logger.exception("Error during tests")
return False
finally:
# Clean up
self.stop_server()