UE Wingman getting ready for release
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2026 BlueprintMCP Contributors
|
||||
Copyright (c) 2026 UE Wingman and BlueprintMCP Contributors
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
||||
@@ -1,72 +1,177 @@
|
||||
= History
|
||||
# UE Wingman
|
||||
|
||||
This plugin was originally vibe-coded by Claude Code. It
|
||||
was then posted to David Gundry's github account, here:
|
||||
UE Wingman is a tool that allows an AI to control the unreal
|
||||
editor. When you're using it, it feels like the AI is right
|
||||
there looking at the editor with you. You'll be able to
|
||||
watch as it creates graph nodes and wires them together,
|
||||
you'll see it add components to your blueprints, you'll see
|
||||
it design widget hierarchies for you, and you'll see it write
|
||||
shaders for your materials.
|
||||
|
||||
https://github.com/mirno-ehf/ue5-mcp
|
||||
The tool is not complete, not by a long shot. There are
|
||||
tons of Unreal Editor functions that the AI just can't
|
||||
access yet. Even so, I think it's useful: it has pretty
|
||||
comprehensive support to allow the AI agent to help create
|
||||
blueprints, widget blueprints, and materials.
|
||||
|
||||
He released it under the MIT license.
|
||||
## How Does it Work?
|
||||
|
||||
How do I know it was vibe coded? Because he says so:
|
||||
This tool adds a command interpreter plugin to the Unreal
|
||||
Editor. You can type commands, and the plugin in the editor
|
||||
will execute them. You can actually type commands directly
|
||||
from the command-line. Here's an example of what you might
|
||||
see:
|
||||
|
||||
> For Humans:
|
||||
>
|
||||
> You're welcome here, but probably not in the way you'd
|
||||
> expect. This project is built and maintained entirely by
|
||||
> AI coding agents — Claude Code, Cursor, Copilot Workspace,
|
||||
> and the like. We don't accept human-written code or
|
||||
> human-opened issues.
|
||||
```
|
||||
$ ue-wingman.py Graph_Dump Graph=/Game/Testing/BP_Test,graph:EventGraph
|
||||
|
||||
node K2Node_Event_0: Event BeginPlay
|
||||
output-pins OutputDelegate
|
||||
|
||||
node K2Node_Event_2: Event Tick
|
||||
output-pins OutputDelegate, DeltaSeconds
|
||||
```
|
||||
|
||||
Huh, interesting. I (Josh Yelon) downloaded the code, and
|
||||
evaluated it. I was curious what vibe coding could do.
|
||||
Here's what I found:
|
||||
There are tons of commands built in: Graph_Dump,
|
||||
GraphNode_Create, GraphPin_Connect,
|
||||
BlueprintComponent_Create, Widget_Create, and so forth.
|
||||
Using these commands, it's possible to examine and modify
|
||||
blueprints, widgets, and materials.
|
||||
|
||||
* The code, overall, had issues, but in the end, it wasn't
|
||||
that bad. It did work, mostly - and that matters. I wanted
|
||||
my own working MCP server, and starting from a place where
|
||||
you have working code is a lot easier than starting from
|
||||
a blank slate.
|
||||
But, of course, these commands aren't really intended for humans.
|
||||
They're intended for an AI agent. The AI is given access to these
|
||||
commands using what's called "Model Context Protocol," which is
|
||||
a goofy name for "a mechanism that an AI can use to send
|
||||
commands to other software."
|
||||
|
||||
* Claude really doesn't build any mechanisms to enforce
|
||||
consistency. For example: there were all the handlers, and
|
||||
then in a different file, declarations of all the handlers,
|
||||
then in another file registrations of all the handlers with
|
||||
the webserver, then in another place a list of all the
|
||||
handlers that require undo support, then in an entirely
|
||||
other directory, a list of all the handlers and their
|
||||
parameters. If any of these lists got out of sync, it would
|
||||
break.
|
||||
## Why Choose this Particular Unreal Engine MCP?
|
||||
|
||||
* Claude doesn't seem to care at all about extra layers upon
|
||||
layers, or about dependencies. Rather than building an MCP
|
||||
server in an unreal plugin, it built a *web* server in an
|
||||
unreal plugin, then built an MCP-to-web translation program
|
||||
in *javascript*. Requests to the server were getting
|
||||
translated from json into URL parameters and then back into
|
||||
json. Ouch. The javascript required installation of about
|
||||
a thousand javascript libraries, some of which were not easy
|
||||
to install. Why not just build an MCP server in the Unreal
|
||||
plugin directly, skipping all the dependencies and
|
||||
translation layers?
|
||||
There are a *lot* of Unreal Engine MCPs out there. Some of
|
||||
them are, shall we say, not carefully engineered. I'm a
|
||||
reasonably skilled software engineer and I've designed
|
||||
this plugin to be robust and capable of sustained development.
|
||||
|
||||
* Claude will repeat code over and over. I found endless
|
||||
places where there were 10 copies of the same function, with
|
||||
trivial variations, that could have all been merged into one
|
||||
function with a parameter or two. When it does repeat code,
|
||||
the repetitions are not generally consistent with each other:
|
||||
one variant might have a bug, whereas another is fine.
|
||||
This MCP is also designed to be as broadly general as
|
||||
possible. I've seen many Unreal Engine MCPs that claim "can
|
||||
create 22 different kinds of graph nodes!" This makes me
|
||||
ask: why not just provide the *entire catalog* of all
|
||||
possible graph nodes? I've seen MCPs claim "you can edit 15
|
||||
different material expression properties!" Why not provide
|
||||
access to *all* editable material expression properties?
|
||||
I've tried to make every tool in this MCP as capable as
|
||||
possible, with as few limits as possible.
|
||||
|
||||
* There were lots of little edge-case bugs throughout the
|
||||
code. Claude is actually not bad about noticing edge cases,
|
||||
but it misses some.
|
||||
This MCP is very extensible. Adding a new command requires
|
||||
a relatively small amount of code. I'm hoping some others
|
||||
in the community will eventually start contributing new
|
||||
commands.
|
||||
|
||||
Despite all this, it was much easier to start from something
|
||||
than to start from nothing.
|
||||
## Installation
|
||||
|
||||
So, I undertook a massive refactoring effort. I did use
|
||||
Claude Code extensively - in fact, I'd say Claude did 70% of
|
||||
the work. But I monitored every step, and constantly pushed
|
||||
Claude hard to use better software engineering practices.
|
||||
There are three parts to UE Wingman:
|
||||
|
||||
* The Unreal Plugin, which does 99% of the work.
|
||||
|
||||
* The python program "ue-wingman.py" which a human can
|
||||
use to send commands to the plugin.
|
||||
|
||||
* The python program "ue-wingman-mcp.py", which an AI
|
||||
can use to send commands to the plugin.
|
||||
|
||||
If you build Unreal from source, the best way to install the
|
||||
plugin is to drop the entire UEWingman source folder into
|
||||
your Plugins folder. Then do a build. Restart the editor, and
|
||||
go into your plugins configuration. Enable the UE Wingman
|
||||
plugin. You're done.
|
||||
|
||||
If you don't build from source, then unfortunately, you're
|
||||
out of luck. Precompiled plugins must be built for every
|
||||
different OS, for every different engine version. I just
|
||||
don't have the means to do that right now.
|
||||
|
||||
After installing the plugin, you need to install the two
|
||||
python programs. They are both short and simple: all they
|
||||
do is establish a network connection to the plugin, and then
|
||||
send the command you typed. They require python 3.6 or later,
|
||||
and no other dependencies.
|
||||
|
||||
To install the human version, ue-wingman.py, just drop it into
|
||||
a folder on your PATH.
|
||||
|
||||
To install the AI version, ue-wingman-mcp.py, you have to
|
||||
usually set up some config file for your AI agent. I use
|
||||
Claude Code, for that, you have to create a file ".mcp.json"
|
||||
in your project folder, and it needs to have this inside it:
|
||||
|
||||
```
|
||||
{
|
||||
"mcpServers": {
|
||||
"ue-wingman": {
|
||||
"command": "python3",
|
||||
"args": ["Plugins/UEWingman/ue-wingman-mcp.py"]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
You can usually ask your AI agent for help creating this
|
||||
config file.
|
||||
|
||||
## The "User Manual"
|
||||
|
||||
You might be interested in seeing the "user manual" for the
|
||||
plugin. To get that, you type this:
|
||||
|
||||
```
|
||||
$ ue-wingman.py UserManual
|
||||
```
|
||||
|
||||
Of course, you're not the intended user: your AI agent is.
|
||||
When the AI agent starts up ue-wingman-mcp, it is
|
||||
automatically told to read the user manual. From there, the
|
||||
User Manual says, among other things, that the AI agent can
|
||||
get a listing of built-in commands. You can see that too:
|
||||
|
||||
```
|
||||
$ ue-wingman.py ShowCommands
|
||||
```
|
||||
|
||||
With these two commands at your disposal, you'll have a better
|
||||
understanding of what exactly your AI agent is doing with this
|
||||
plugin, and how it all works.
|
||||
|
||||
## Fun things to Try
|
||||
|
||||
I really started enjoying this plugin when I asked my agent
|
||||
to make me a "cool looking material, something psychedelic
|
||||
and weird." It made a neat kaleidoscope thing. I then
|
||||
asked it to make me an animated rendering of the mandelbrot
|
||||
set. It's fun to watch it do things like that.
|
||||
|
||||
## History and Credits
|
||||
|
||||
When I myself needed an MCP for unreal development, I did a
|
||||
survey of the plugins out there. I ended up choosing a
|
||||
plugin called "Blueprint MCP" by David Gundry:
|
||||
|
||||
<https://github.com/mirno-ehf/ue5-mcp>
|
||||
|
||||
It was not bad, but it had some limitations, and I started
|
||||
doing work to improve it. Incrementally, I ended rewriting
|
||||
pretty much the whole thing. So this whole project is actually
|
||||
a fork of Blueprint MCP. There's very little of the original
|
||||
code remaining. However, you will find snippets here and there.
|
||||
|
||||
Even though I ended up rewriting most of the code, it really
|
||||
was useful to have a functioning starting point. It meant I
|
||||
could improve one thing at a time, without having to try to
|
||||
get everything working all at once. So I'm quite grateful
|
||||
to David Gundry and his work.
|
||||
|
||||
## Software License
|
||||
|
||||
UE Wingman is licensed under the MIT license, a copy of which
|
||||
is enclosed. Its predecessor (of which it is a fork) was also
|
||||
under the MIT license, so everything works out.
|
||||
|
||||
The result, I think, is a pretty clean MCP server.
|
||||
|
||||
|
||||
@@ -83,7 +83,7 @@ private:
|
||||
|
||||
// ----- TCP server -----
|
||||
FSocket* ListenSocket = nullptr;
|
||||
int32 Port = 9847;
|
||||
int32 Port = 9851;
|
||||
bool bRunning = false;
|
||||
|
||||
// ----- Client connections -----
|
||||
|
||||
162
Plugins/UEWingman/ue-wingman-mcp.py
Normal file
162
Plugins/UEWingman/ue-wingman-mcp.py
Normal file
@@ -0,0 +1,162 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
MCP stdio-to-TCP bridge for UE Wingman.
|
||||
|
||||
Exposes a single MCP tool "unreal" that forwards JSON commands to the
|
||||
UE Wingman TCP server in the Unreal Editor.
|
||||
"""
|
||||
|
||||
import sys
|
||||
import json
|
||||
import socket
|
||||
|
||||
HOST = "localhost"
|
||||
PORT = 9851
|
||||
CONNECT_TIMEOUT = 2
|
||||
READ_TIMEOUT = 120
|
||||
|
||||
TOOL_DESCRIPTION = (
|
||||
"Send a command to the Unreal Editor's UE Wingman plugin. "
|
||||
"The 'command' field specifies which operation to perform; "
|
||||
"additional fields are command-specific parameters. "
|
||||
'Use {"command": "UserManual"} to get an overview. '
|
||||
"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):
|
||||
"""Send a JSON message to the editor and return the null-terminated response."""
|
||||
data = json.dumps(message) + "\n"
|
||||
sock.sendall(data.encode())
|
||||
|
||||
result = b""
|
||||
while True:
|
||||
chunk = sock.recv(65536)
|
||||
if not chunk:
|
||||
raise ConnectionError("Connection closed")
|
||||
result += chunk
|
||||
if b"\0" in result:
|
||||
break
|
||||
|
||||
return result[:result.index(b"\0")].decode()
|
||||
|
||||
|
||||
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", "")
|
||||
|
||||
# Notifications don't get responses
|
||||
if msg_id is None:
|
||||
return None
|
||||
|
||||
if method == "initialize":
|
||||
return make_jsonrpc(msg_id, {
|
||||
"protocolVersion": "2024-11-05",
|
||||
"capabilities": {"tools": {}},
|
||||
"serverInfo": {"name": "ue-wingman", "version": "1.0.0"},
|
||||
})
|
||||
|
||||
if method == "tools/list":
|
||||
return make_jsonrpc(msg_id, {"tools": [TOOL_SCHEMA]})
|
||||
|
||||
if method == "tools/call":
|
||||
params = msg.get("params", {})
|
||||
arguments = params.get("arguments", {})
|
||||
result = forward_to_editor(arguments)
|
||||
if isinstance(result, dict) and "error" in result:
|
||||
text = result["error"]
|
||||
else:
|
||||
text = result
|
||||
return make_jsonrpc(msg_id, {
|
||||
"content": [{"type": "text", "text": text}],
|
||||
})
|
||||
|
||||
return {
|
||||
"jsonrpc": "2.0",
|
||||
"id": msg_id,
|
||||
"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()
|
||||
66
Plugins/UEWingman/ue-wingman.py
Executable file
66
Plugins/UEWingman/ue-wingman.py
Executable file
@@ -0,0 +1,66 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Human-friendly MCP test client.
|
||||
|
||||
Usage: ue-wingman.py UserManual
|
||||
"""
|
||||
|
||||
import sys
|
||||
import json
|
||||
import socket
|
||||
|
||||
HOST = "localhost"
|
||||
PORT = 9851
|
||||
TIMEOUT = 120
|
||||
|
||||
|
||||
def main():
|
||||
args = sys.argv[1:]
|
||||
if not args:
|
||||
print("Usage: ue-wingman.py ShowCommands [key=value ...]")
|
||||
sys.exit(1)
|
||||
|
||||
msg = {"command": args[0]}
|
||||
for arg in args[1:]:
|
||||
key, _, value = arg.partition("=")
|
||||
if value.lower() == "true":
|
||||
value = True
|
||||
elif value.lower() == "false":
|
||||
value = False
|
||||
else:
|
||||
try:
|
||||
value = int(value)
|
||||
except ValueError:
|
||||
pass
|
||||
msg[key] = value
|
||||
|
||||
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
sock.settimeout(TIMEOUT)
|
||||
try:
|
||||
sock.connect((HOST, PORT))
|
||||
except (ConnectionRefusedError, socket.timeout, OSError) as e:
|
||||
print(f"Cannot connect to {HOST}:{PORT} — is the editor running?")
|
||||
sys.exit(1)
|
||||
|
||||
sock.sendall((json.dumps(msg) + "\n").encode())
|
||||
|
||||
result = b""
|
||||
while True:
|
||||
chunk = sock.recv(65536)
|
||||
if not chunk:
|
||||
break
|
||||
result += chunk
|
||||
if b"\0" in result:
|
||||
break
|
||||
|
||||
sock.close()
|
||||
result = result[:result.index(b"\0")].decode() if b"\0" in result else result.decode()
|
||||
|
||||
try:
|
||||
parsed = json.loads(result)
|
||||
print(json.dumps(parsed, indent=2))
|
||||
except json.JSONDecodeError:
|
||||
print(result)
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user