Files
ssh-mcp-server/implementation/ssh-mcp-http-wrapper.py
Claude Code 7b98651e5a Initial commit: SSH-MCP server implementation
- SSH-MCP server with 7 infrastructure management tools
  * ssh_list_hosts - List all available hosts
  * ssh_exec - Execute commands on remote hosts
  * ssh_get_file / ssh_put_file - File operations
  * ssh_docker_ps - List Docker containers
  * lxc_list / lxc_exec - LXC container management

- HTTP/SSE transport wrapper for Claude Code integration
  * FastAPI-based HTTP server on port 8081
  * Server-Sent Events (SSE) support
  * MCP 2025 specification compliant

- Complete deployment on LXC 110 (10.50.0.110)
  * SSH key-based authentication (Ed25519)
  * Systemd service for automatic startup
  * Tested and verified on all infrastructure

- Comprehensive documentation
  * Complete deployment guide
  * Git collaboration workflow
  * API usage examples

Developed collaboratively by Claude Code and Agent Zero.

Infrastructure Access:
- photon.obnh.io (test target)
- proton.obr.sh (development server, hosts this repo)
- fry.obr.sh (migration target)
- Proxmox 10.50.0.72 (LXC/VM host)

🤖 Generated with Claude Code
Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-13 13:31:07 +00:00

177 lines
4.5 KiB
Python

#!/usr/bin/env python3
"""
HTTP/SSE Transport Wrapper for SSH-MCP Server
This provides an HTTP interface with Server-Sent Events (SSE) support
for Claude Code to connect to the SSH-MCP server.
"""
import asyncio
import json
import subprocess
from typing import Optional
from fastapi import FastAPI, Request, Response, HTTPException
from fastapi.responses import StreamingResponse
import uvicorn
app = FastAPI(title="SSH-MCP Server", version="1.0.0")
# In-memory storage for MCP server process
mcp_process: Optional[asyncio.subprocess.Process] = None
async def start_mcp_server():
"""Start the MCP server subprocess"""
global mcp_process
if mcp_process is None:
mcp_process = await asyncio.create_subprocess_exec(
"/usr/bin/python3",
"/opt/ssh-mcp-server/server.py",
stdin=asyncio.subprocess.PIPE,
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE
)
return mcp_process
async def send_mcp_request(request_data: dict) -> dict:
"""Send request to MCP server and get response"""
process = await start_mcp_server()
# Send request
request_json = json.dumps(request_data) + "\n"
process.stdin.write(request_json.encode())
await process.stdin.drain()
# Read response
response_line = await process.stdout.readline()
response_data = json.loads(response_line.decode())
return response_data
@app.get("/")
async def root():
"""Health check endpoint"""
return {
"name": "SSH-MCP Server",
"version": "1.0.0",
"status": "operational",
"description": "MCP server providing SSH-based infrastructure access",
"endpoints": {
"/": "Health check",
"/tools": "List available tools",
"/tools/call": "Call a tool",
"/sse": "Server-Sent Events endpoint"
}
}
@app.get("/health")
async def health():
"""Health check"""
return {"status": "healthy"}
@app.post("/tools")
async def list_tools():
"""List available MCP tools"""
request = {
"jsonrpc": "2.0",
"id": 1,
"method": "tools/list",
"params": {}
}
response = await send_mcp_request(request)
return response.get("result", {})
@app.post("/tools/call")
async def call_tool(request: Request):
"""Call an MCP tool"""
body = await request.json()
tool_name = body.get("name")
arguments = body.get("arguments", {})
mcp_request = {
"jsonrpc": "2.0",
"id": 2,
"method": "tools/call",
"params": {
"name": tool_name,
"arguments": arguments
}
}
response = await send_mcp_request(mcp_request)
return response.get("result", {})
@app.get("/sse")
async def sse_endpoint(request: Request):
"""
Server-Sent Events endpoint for real-time MCP communication
This is required for Claude Code's MCP connector
"""
async def event_generator():
# Send initial connection event
yield f"data: {json.dumps({'type': 'connected', 'server': 'ssh-mcp-server'})}\n\n"
# Keep connection alive
while True:
if await request.is_disconnected():
break
# Send periodic heartbeat
yield f"data: {json.dumps({'type': 'heartbeat', 'timestamp': asyncio.get_event_loop().time()})}\n\n"
await asyncio.sleep(30)
return StreamingResponse(
event_generator(),
media_type="text/event-stream",
headers={
"Cache-Control": "no-cache",
"Connection": "keep-alive",
}
)
@app.post("/message")
async def message(request: Request):
"""
MCP message endpoint for Claude Code integration
Accepts JSON-RPC 2.0 messages
"""
body = await request.json()
response = await send_mcp_request(body)
return response
# MCP Server Info endpoint (required by MCP spec)
@app.get("/mcp/info")
async def mcp_info():
"""MCP server information"""
return {
"name": "ssh-mcp-server",
"version": "1.0.0",
"description": "SSH-based MCP server for OBNH/OBR infrastructure management",
"capabilities": {
"tools": True,
"resources": False,
"prompts": False
},
"transport": "http+sse"
}
if __name__ == "__main__":
print("Starting SSH-MCP HTTP/SSE Server on port 8081...")
uvicorn.run(
app,
host="0.0.0.0",
port=8081,
log_level="info"
)