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

250 lines
8.0 KiB
Python

#!/usr/bin/env python3
# SPDX-License-Identifier: AGPL-3.0-or-later
"""
STDIO server adapter for MCP testing.
This module defines a server adapter that starts a subprocess and
communicates with it over standard input/output using JSON-RPC.
"""
import asyncio
import logging
import os
import shlex
import signal
import subprocess
import sys
from typing import Dict, Any, List, Optional, Tuple, Union
from ..transports.stdio import StdioTransportAdapter
from .base import MCPServerAdapter
logger = logging.getLogger(__name__)
class StdioServerAdapter(MCPServerAdapter):
"""
Adapter for MCP servers that communicate over standard input/output.
This adapter starts a server as a subprocess and communicates with it
using the STDIO transport.
"""
def __init__(
self,
server_command: str,
protocol_version: str,
debug: bool = False,
env: Optional[Dict[str, str]] = None,
):
"""
Initialize a STDIO server adapter.
Args:
server_command: The command to start the server
protocol_version: The MCP protocol version to use
debug: Whether to enable debug logging
env: Additional environment variables to set when running the server
"""
super().__init__(protocol_version, debug)
self.server_command = server_command
self.env = env or {}
self.process = None
self.transport = None
async def start(self) -> bool:
"""
Start the server as a subprocess.
Returns:
True if started successfully, False otherwise
"""
if self.process is not None:
logger.warning("Server is already running")
return True
# Create environment for the subprocess
env = os.environ.copy()
env.update(self.env)
if self.debug:
logger.debug(f"Starting server with command: {self.server_command}")
logger.debug(f"Environment: {env}")
try:
# Start the server process
args = shlex.split(self.server_command)
self.process = subprocess.Popen(
args,
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
env=env,
text=False, # We want binary I/O
)
# Create a transport adapter for communicating with the server
self.transport = StdioTransportAdapter(
self.process.stdin,
self.process.stdout,
self.debug,
)
# Start a background task to log stderr output
if self.debug:
asyncio.create_task(self._log_stderr())
return True
except Exception as e:
logger.error(f"Failed to start server: {str(e)}")
return False
async def stop(self) -> bool:
"""
Stop the server.
Returns:
True if stopped successfully, False otherwise
"""
if self.process is None:
logger.warning("Server is not running")
return True
try:
if self.debug:
logger.debug("Stopping server")
# Try to send a shutdown request first
await self.shutdown()
# Close the transport
if self.transport:
await self.transport.close()
self.transport = None
# If the process is still running, terminate it
if self.process.poll() is None:
logger.debug("Server still running, sending SIGTERM")
self.process.terminate()
# Wait for the process to terminate
try:
self.process.wait(timeout=5)
except subprocess.TimeoutExpired:
logger.warning("Server didn't terminate, sending SIGKILL")
self.process.kill()
# Clean up
self.process = None
return True
except Exception as e:
logger.error(f"Failed to stop server: {str(e)}")
return False
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
"""
if self.process is None or self.transport is None:
raise RuntimeError("Server is not running")
if params is None:
params = {}
request_id = self._get_next_request_id()
request = {
"jsonrpc": "2.0",
"id": request_id,
"method": method,
"params": params,
}
if self.debug:
logger.debug(f"Sending request: {request}")
try:
await self.transport.send_message(request)
response = await self.transport.receive_message()
if self.debug:
logger.debug(f"Received response: {response}")
if not isinstance(response, dict):
raise RuntimeError(f"Expected dict response, got {type(response)}")
if "id" not in response or response["id"] != request_id:
raise RuntimeError(f"Response ID mismatch: expected {request_id}, got {response.get('id')}")
return response
except Exception as e:
logger.error(f"Failed to send request: {str(e)}")
raise RuntimeError(f"Failed to send request: {str(e)}")
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
"""
if self.process is None or self.transport is None:
raise RuntimeError("Server is not running")
if params is None:
params = {}
notification = {
"jsonrpc": "2.0",
"method": method,
"params": params,
}
if self.debug:
logger.debug(f"Sending notification: {notification}")
try:
await self.transport.send_message(notification)
except Exception as e:
logger.error(f"Failed to send notification: {str(e)}")
raise RuntimeError(f"Failed to send notification: {str(e)}")
async def _log_stderr(self) -> None:
"""
Log the stderr output from the server process.
"""
if self.process is None or self.process.stderr is None:
return
while True:
try:
line = await asyncio.get_event_loop().run_in_executor(
None, self.process.stderr.readline
)
if not line:
break
try:
stderr_line = line.decode('utf-8').rstrip()
logger.debug(f"Server stderr: {stderr_line}")
except UnicodeDecodeError:
logger.debug(f"Server stderr (binary): {line}")
except Exception as e:
logger.error(f"Error reading stderr: {str(e)}")
break