About
Turn a running Space Engineers game into an MCP server
README
Turn a running Space Engineers game into an
MCP server — SE1 (client or dedicated
server) and SE2 (client). An LLM connects over local HTTP and executes C#
directly inside the live engine, with full .NET and game API access — right
down to internal types and members.
⚠️ This is remote code execution by design
The bearer token is the root password. Anyone holding it can run arbitrary C# in the game process: file I/O, spawning processes, native
[DllImport]— the lot. There is no sandbox.
- Never share, screenshot, or commit the token.
- Never port-forward the listener or expose it past
localhost.- The server binds
127.0.0.1only, rejects browser-origin (CSRF) and foreignHostheaders, and uses constant-time token comparison. In multiplayer the client additionally gates execution (SE1: Admin/Owner promote level; SE2: host / local-server sessions only).- The per-script watchdog (1 s/frame, 700 KB stack) exists to stop runaway loops from hanging the game thread. It is not a security boundary — a token holder already has full RCE.
What it is
MCP client (Claude, etc.)
│ HTTP · JSON-RPC · Bearer token (127.0.0.1 only)
▼
┌──────────────────────────────┐
│ McpServer (Shared) │ HttpListener · JSON-RPC · auth · sessions
│ ITool dispatch │
│ Executor · Compiler · Guard │ Roslyn compile → Cecil IL guard → coroutine
└──────────────────────────────┘
│ runs your C# on a game thread
┌─────┴───────────────┐
▼ ▼
main lane render lane ← client only
per-frame pump postfix on the render
(game / API state) thread (hook per host)
Three hosts share one MCP core (Shared): SE1 ClientPlugin (Pulsar,
in-game), SE1 ServerPlugin (Magnetar, dedicated server), and SE2
Client2Plugin (Pulsar Modern, in-game — ships as SeMcp2). Code runs on the
game's main thread or, on either client, on the render thread. The
per-frame pump differs by host:
- main lane — SE1 client
IPlugin.Update, SE1 serverUpdate; SE2 a Harmony postfix onVRageCore.Update. - render lane (client only) — SE1 a Harmony postfix on
MyRenderThread.RenderFrame; SE2 onRender12EngineComponent.RenderFrame.
execute_code works on all three; take_screenshot and the render lane are
client-only.
Connecting
On first launch the plugin auto-generates a token. Where to find it and the URL:
- Client — open the in-game settings dialog and click Copy URL. You get
http://localhost:9876/?token=<token>on the clipboard. (Default port9876, or6789on the SE2 client; if taken it climbs9876→9885— the live port shows in the dialog title and the log linelistening on :<port>.) - Server — on Quasar-managed servers, open the Plugin configuration page
to view the token and edit the port. Standalone servers without Quasar can read
the token from
<UserDataPath>/SeMcp.cfg. Default port9000; same9000→9009climb if taken, with the bound port in the log.
Use
localhost, not127.0.0.1— theHostheader is checked and a mismatch returns403.
Transport is MCP Streamable HTTP (not SSE): POST JSON-RPC to
http://localhost:<port>/. Auth is either an Authorization: Bearer <token>
header or a ?token=<token> query parameter (not both). Point any standard
MCP client at it — it will initialize, pick up the Mcp-Session-Id, and manage
the session for you:
{
"mcpServers": {
"se-mcp": {
"type": "streamable-http",
"url": "http://localhost:9876/",
"headers": { "Authorization": "Bearer <token>" }
}
}
}
Driving the raw protocol by hand (note: every call after initialize must echo
the session id):
# 1. initialize — the Mcp-Session-Id comes back in the response headers
curl -i http://localhost:9876/ \
-H 'Authorization: Bearer <token>' -H 'Content-Type: application/json' \
-d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{}}'
# 2. call a tool
curl http://localhost:9876/ \
-H 'Authorization: Bearer <token>' -H 'Mcp-Session-Id: <id>' \
-H 'Content-Type: application/json' \
-d '{"jsonrpc":"2.0","id":2,"method":"tools/call",
"params":{"name":"execute_code",
"arguments":{"code":"Console.WriteLine(MySession.Static?.Name);"}}}'
execute_code
Your input maps 1:1 onto three C# layers, which are spliced into a wrapper:
// <usings> ← extra "using" lines (defaults already imported)
public class __REPL__
{
// <class_body> ← methods, fields, nested types, [DllImport] — class-level
public IEnumerable<object> Run(TextWriter Console)
{
// <code> ← statements only; this is the entry point
yield break;
}
}
| field | required | what goes in it |
|---|---|---|
code |
yes | Statements only. Output via Console.WriteLine(). Pause until the next frame with yield return null. |
class_body |
no | Class-level declarations — anything that can't live in a method body, e.g. [DllImport] P/Invoke. |
usings |
no | Extra namespace imports — bare paths like "System.Runtime.InteropServices", no using keyword, no ;. |
target |
no | "main" (default) or "render" (client only). |
- A large set of namespaces is pre-imported — SE1:
System.*,VRageMath,VRage.*,Sandbox.*,SpaceEngineers.Game.*; SE2:System.*,Keen.VRage.*,Keen.Game2.*. Use short type names (SE1MySession.Static, SE2GameAppComponent), not fully-qualified ones. - Compiled with Roslyn 5.3 against every loaded assembly (.NET + game +
other plugins), with ignore-accessibility turned on:
internalclasses, methods, fields and properties are callable directly, no reflection — this works across the game's own assemblies and other loaded plugins (only trulyprivatemembers still need reflection).unsafeand[DllImport]are allowed. - Compile errors come back per field with corrected line numbers:
code (3,9): error CS0103: .... - Scripts are coroutines and run in parallel; each step is bounded by the watchdog above.
Examples
Read game state on the main thread:
var s = MyAPIGateway.Session;
Console.WriteLine($"World: {s.Name}");
Console.WriteLine($"You are at: {s.Player?.GetPosition()}");
The same on SE2 — the API root is GameAppComponent, reached through the
engine singleton, not MyAPIGateway:
var engine = Singleton<VRageCore>.Instance.Engine;
var session = engine.Get<GameAppComponent>().ClientSession;
Console.WriteLine($"in a session: {session != null}");
P/Invoke via class_body + usings (usings: ["System.Runtime.InteropServices"]):
// class_body
[DllImport("user32.dll", CharSet = CharSet.Unicode)]
static extern int MessageBox(IntPtr hWnd, string text, string caption, uint type);
// code
MessageBox(IntPtr.Zero, "Hello from Space Engineers", "SeMcp", 0);
Console.WriteLine("shown");
Spread work across frames:
for (int i = 3; i > 0; i--)
{
Console.WriteLine($"tick {i}");
yield return null; // resume next frame
}
Console.WriteLine("done");
The
rendertarget runs on the render thread — use it only to inspect other plugins' Harmony hooks that execute there. The game API (SE1MyAPIGateway, SE2 session / scene access) asserts off the main thread.
Both clients also expose take_screenshot (captures the current frame as an
image; optional ignore_sprites to drop the HUD). Full parameters are in the
tool's inputSchema.
Client vs. server
| SE1 client (Pulsar) | SE1 server (Magnetar) | SE2 client (Pulsar Modern) | |
|---|---|---|---|
| Default port | 9876 (9876–9885) |
9000 (9000–9009) |
6789 (6789–6798) |
render lane |
✅ | ❌ | ✅ |
take_screenshot |
✅ | ❌ | ✅ |
| Multiplayer gate | Admin/Owner required | none — token is full access | host / local-server only¹ |
| Settings GUI | ✅ (MyGui) | ✅ (Quasar Plugin config) | ✅ (Avalonia) |
| Config file | <UserDataPath>/Storage/SeMcp.cfg |
<UserDataPath>/SeMcp.cfg |
Pulsar Data\SeMcp2\SeMcp2.cfg |
| API root | MyAPIGateway / MySession |
same | GameAppComponent (Keen.*) |
¹ SE2 has no multiplayer yet; the gate pre-emptively blocks a pure client (one with no local authoritative server), so in practice it only ever runs single-player today.
Configuration
Port and SecretKey persist to the .cfg (auto-saved). On either client both are
editable in the settings dialog, with Regenerate Token and Copy URL buttons;
on the server they are editable through Quasar's Plugin configuration page.
Changing the port requires a restart.
How it works
For anyone reading or extending the code:
McpServer— oneHttpListener, single (non-batched) JSON-RPC, auth + CSRF/host checks, pre-renderedtools/list. Sessions namespace in-flight request ids but carry no server state.Executor— compilation runs on the thread pool; the compiled coroutine is then stepped oneMoveNextper frame on its owning game thread. Each lane has its own executor andScriptGuard, so finally-blocks and thread-affine state stay on the right thread.Compiler— drives Roslyn entirely through reflection (no compile-time binding, so it works against both the game's ancient Roslyn and the NuGet 5.3 one), references every loaded assembly, and enables ignore-accessibility so scripts can reachinternalmembers:MetadataImportOptions.Internal+BinderFlags.IgnoreAccessibilityat compile time, plus an injected[assembly: IgnoresAccessChecksTo]per referenced assembly at runtime (its attribute is self-declared — source wins over the copies Harmony and other plugins ship, so no ambiguity). It then rewrites the emitted IL with Mono.Cecil to inject the guard.ScriptGuard— injectedBail()on backward branches andcatch→filter rewrites (so acatchcan't swallow the abort), plus a stack-depthStackCheckat call sites. A background 1 s timer sets theDeadflag the injected checks read.
License
MIT — see LICENSE.
Installing Se
This server has no published package — it is built from source. Open the repository and follow its README.
▸ github.com/lolifamily/se-mcpFAQ
Is Se MCP free?
Yes, Se MCP is free — one-click install via Unyly at no cost.
Does Se need an API key?
No, Se runs without API keys or environment variables.
Is Se hosted or self-hosted?
Self-hosted: the server runs locally on your machine via the install command above.
How do I install Se in Claude Desktop, Claude Code or Cursor?
Open Se on unyly.org, pick your client tab (Claude Desktop, Claude Code, Cursor) and press Install — the config is generated automatically, no JSON editing.
Related MCPs
GitHub
PRs, issues, code search, CI status
by GitHubFilesystem
Secure file operations with configurable access controls.
Memory
Knowledge graph-based persistent memory system.
Template MCP Server
A CLI tool to create a new Model Context Protocol server project with TypeScript support, dual transport options, and an extensible structure
by mcpdotdirectCompare Se with
Not sure what to pick?
Find your stack in 60 seconds
Author?
Embed badge for your README
Browse similar
All development MCPs
