126 lines
3.8 KiB
Python
126 lines
3.8 KiB
Python
"""opencode <-> Serial Studio MCP bridge.
|
|
|
|
Serial Studio exposes a native MCP server on TCP port 7777 speaking
|
|
JSON-RPC 2.0 (newline-delimited over a raw TCP socket), NOT over HTTP.
|
|
|
|
opencode's "remote" MCP type expects HTTP (streamableHttp / SSE), so it
|
|
cannot talk to Serial Studio directly. This script is a thin stdio
|
|
bridge: opencode launches it as a "local" MCP server, reads newline-
|
|
delimited JSON-RPC from stdin, forwards each request over TCP to Serial
|
|
Studio on port 7777, and writes the response back to stdout.
|
|
|
|
Usage:
|
|
python serial_studio_bridge.py [host] [port]
|
|
Defaults: host=127.0.0.1, port=7777
|
|
"""
|
|
|
|
import json
|
|
import socket
|
|
import sys
|
|
|
|
DEFAULT_HOST = "127.0.0.1"
|
|
DEFAULT_PORT = 7777
|
|
|
|
|
|
def main() -> int:
|
|
if len(sys.argv) > 1:
|
|
host = sys.argv[1]
|
|
else:
|
|
host = DEFAULT_HOST
|
|
if len(sys.argv) > 2:
|
|
port = int(sys.argv[2])
|
|
else:
|
|
port = DEFAULT_PORT
|
|
|
|
sock = socket.create_connection((host, port), timeout=10)
|
|
sock.settimeout(30)
|
|
buf = b""
|
|
|
|
def send_json(obj) -> None:
|
|
line = (json.dumps(obj, separators=(",", ":")) + "\n").encode("utf-8")
|
|
sock.sendall(line)
|
|
|
|
def read_response(target_id) -> dict:
|
|
nonlocal buf
|
|
while True:
|
|
while b"\n" in buf:
|
|
line, buf = buf.split(b"\n", 1)
|
|
line = line.strip()
|
|
if not line:
|
|
continue
|
|
try:
|
|
resp = json.loads(line.decode("utf-8"))
|
|
except Exception:
|
|
continue
|
|
if resp.get("id") == target_id or target_id is None:
|
|
return resp
|
|
sock.settimeout(30)
|
|
data = sock.recv(65536)
|
|
if not data:
|
|
raise ConnectionError("Serial Studio closed connection")
|
|
buf += data
|
|
|
|
try:
|
|
for raw in sys.stdin:
|
|
raw = raw.strip()
|
|
if not raw:
|
|
continue
|
|
try:
|
|
req = json.loads(raw)
|
|
except Exception as e:
|
|
err = {
|
|
"jsonrpc": "2.0",
|
|
"id": None,
|
|
"error": {"code": -32700, "message": f"Parse error: {e}"},
|
|
}
|
|
sys.stdout.write(json.dumps(err) + "\n")
|
|
sys.stdout.flush()
|
|
continue
|
|
|
|
if isinstance(req, list):
|
|
results = []
|
|
for sub in req:
|
|
try:
|
|
send_json(sub)
|
|
results.append(read_response(sub.get("id")))
|
|
except Exception as e:
|
|
results.append(
|
|
{
|
|
"jsonrpc": "2.0",
|
|
"id": sub.get("id"),
|
|
"error": {"code": -32000, "message": str(e)},
|
|
}
|
|
)
|
|
sys.stdout.write(json.dumps(results) + "\n")
|
|
sys.stdout.flush()
|
|
continue
|
|
|
|
if req.get("jsonrpc") == "2.0" and not req.get("id"):
|
|
send_json(req)
|
|
continue
|
|
|
|
try:
|
|
send_json(req)
|
|
resp = read_response(req.get("id"))
|
|
sys.stdout.write(json.dumps(resp) + "\n")
|
|
sys.stdout.flush()
|
|
except Exception as e:
|
|
err = {
|
|
"jsonrpc": "2.0",
|
|
"id": req.get("id"),
|
|
"error": {"code": -32000, "message": str(e)},
|
|
}
|
|
sys.stdout.write(json.dumps(err) + "\n")
|
|
sys.stdout.flush()
|
|
finally:
|
|
try:
|
|
sock.close()
|
|
except Exception:
|
|
pass
|
|
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|