Files
integration/Plugins/UEWingman/ue-wingman-mcp.py

166 lines
4.4 KiB
Python
Raw Normal View History

#!/usr/bin/env python3
"""
2026-03-23 19:34:42 -04:00
MCP stdio-to-TCP bridge for UE Wingman.
2026-03-07 19:32:19 -05:00
Exposes a single MCP tool "unreal" that forwards JSON commands to the
2026-03-23 19:34:42 -04:00
UE Wingman TCP server in the Unreal Editor.
"""
import sys
import json
import socket
HOST = "localhost"
2026-03-23 19:34:42 -04:00
PORT = 9851
CONNECT_TIMEOUT = 2
READ_TIMEOUT = 120
2026-03-07 19:32:19 -05:00
TOOL_DESCRIPTION = (
2026-03-23 19:34:42 -04:00
"Send a command to the Unreal Editor's UE Wingman plugin. "
2026-03-07 19:32:19 -05:00
"The 'command' field specifies which operation to perform; "
"additional fields are command-specific parameters. "
2026-04-04 02:58:23 -04:00
'Use {"command": "Documentation_Manual"} to get an overview. '
2026-03-07 19:32:19 -05:00
"If the editor is not running, the call will return an error; "
"just ask the user to start the editor and try again."
)
TOOL_SCHEMA = {
"name": "unreal",
"description": TOOL_DESCRIPTION,
"inputSchema": {
"type": "object",
"properties": {
"command": {"type": "string", "description": "The command to execute"},
},
"required": ["command"],
"additionalProperties": True,
},
}
sock = None
def connect():
"""Try to connect to the editor. Returns True on success."""
global sock
if sock is not None:
return True
try:
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.settimeout(CONNECT_TIMEOUT)
s.connect((HOST, PORT))
s.settimeout(READ_TIMEOUT)
sock = s
return True
except (ConnectionRefusedError, socket.timeout, OSError):
return False
def disconnect():
global sock
if sock is not None:
try:
sock.close()
except Exception:
pass
sock = None
def send_and_receive(message):
2026-03-08 21:28:47 -04:00
"""Send a JSON message to the editor and return the null-terminated response."""
data = json.dumps(message) + "\0"
sock.sendall(data.encode())
result = b""
while True:
chunk = sock.recv(65536)
if not chunk:
raise ConnectionError("Connection closed")
result += chunk
2026-03-08 21:28:47 -04:00
if b"\0" in result:
break
return result[:result.index(b"\0")].decode()
2026-03-07 19:32:19 -05:00
def forward_to_editor(arguments):
"""Forward arguments to the editor, return the result dict."""
if not connect():
return {"error": "Unreal Editor is not running. Start the editor and try again."}
try:
return send_and_receive(arguments)
except Exception:
disconnect()
# Retry once in case the connection was stale
if connect():
try:
return send_and_receive(arguments)
except Exception:
disconnect()
return {"error": "Lost connection to Unreal Editor."}
def make_jsonrpc(msg_id, result):
return {"jsonrpc": "2.0", "id": msg_id, "result": result}
def handle_message(msg):
"""Handle one JSON-RPC message from Claude Code."""
msg_id = msg.get("id")
method = msg.get("method", "")
2026-03-07 19:32:19 -05:00
# Notifications don't get responses
if msg_id is None:
return None
if method == "initialize":
2026-03-07 19:32:19 -05:00
return make_jsonrpc(msg_id, {
"protocolVersion": "2024-11-05",
"capabilities": {"tools": {}},
2026-03-23 19:34:42 -04:00
"serverInfo": {"name": "ue-wingman", "version": "1.0.0"},
2026-03-07 19:32:19 -05:00
})
if method == "tools/list":
2026-03-07 19:32:19 -05:00
return make_jsonrpc(msg_id, {"tools": [TOOL_SCHEMA]})
if method == "tools/call":
2026-03-07 19:32:19 -05:00
params = msg.get("params", {})
arguments = params.get("arguments", {})
result = forward_to_editor(arguments)
2026-03-12 00:44:17 -04:00
if isinstance(result, dict) and "error" in result:
content = [{"type": "text", "text": result["error"]}]
2026-03-12 00:44:17 -04:00
else:
try:
content = json.loads(result)
except json.JSONDecodeError:
content = [{"type": "text", "text": "Malformed response from editor."}]
2026-03-07 19:32:19 -05:00
return make_jsonrpc(msg_id, {
"content": content,
2026-03-07 19:32:19 -05:00
})
return {
"jsonrpc": "2.0",
"id": msg_id,
2026-03-07 19:32:19 -05:00
"error": {"code": -32601, "message": f"Method not found: {method}"},
}
def main():
for line in sys.stdin:
line = line.strip()
if not line:
continue
try:
msg = json.loads(line)
except json.JSONDecodeError:
continue
response = handle_message(msg)
if response is not None:
sys.stdout.write(json.dumps(response) + "\n")
sys.stdout.flush()
if __name__ == "__main__":
main()