Command Palette

Search for a command to run...

UnylyUnyly
Browse all

Se

FreeNot checked

Turn a running Space Engineers game into an MCP server

GitHubEmbed

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.1 only, rejects browser-origin (CSRF) and foreign Host headers, 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 server Update; SE2 a Harmony postfix on VRageCore.Update.
  • render lane (client only) — SE1 a Harmony postfix on MyRenderThread.RenderFrame; SE2 on Render12EngineComponent.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 port 9876, or 6789 on the SE2 client; if taken it climbs 9876→9885 — the live port shows in the dialog title and the log line listening 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 port 9000; same 9000→9009 climb if taken, with the bound port in the log.

Use localhost, not 127.0.0.1 — the Host header is checked and a mismatch returns 403.

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 (SE1 MySession.Static, SE2 GameAppComponent), not fully-qualified ones.
  • Compiled with Roslyn 5.3 against every loaded assembly (.NET + game + other plugins), with ignore-accessibility turned on: internal classes, methods, fields and properties are callable directly, no reflection — this works across the game's own assemblies and other loaded plugins (only truly private members still need reflection). unsafe and [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 render target runs on the render thread — use it only to inspect other plugins' Harmony hooks that execute there. The game API (SE1 MyAPIGateway, 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 — one HttpListener, single (non-batched) JSON-RPC, auth + CSRF/host checks, pre-rendered tools/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 one MoveNext per frame on its owning game thread. Each lane has its own executor and ScriptGuard, 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 reach internal members: MetadataImportOptions.Internal + BinderFlags.IgnoreAccessibility at 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 — injected Bail() on backward branches and catch→filter rewrites (so a catch can't swallow the abort), plus a stack-depth StackCheck at call sites. A background 1 s timer sets the Dead flag the injected checks read.

License

MIT — see LICENSE.

from github.com/lolifamily/se-mcp

Installing Se

This server has no published package — it is built from source. Open the repository and follow its README.

▸ github.com/lolifamily/se-mcp

FAQ

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

Compare Se with

Not sure what to pick?

Find your stack in 60 seconds

Author?

Embed badge for your README

Browse similar

All development MCPs