Recursive AI

Let an AI assistant inspect a simulation

OneScope is a free, open-source viewer for deterministic libraries from Recursive AI. MIT or Apache-2.0.

Inspect OnePath or OnePhysics recordings through the Model Context Protocol®. This example records a OnePath crowd.

Terminal
dotnet tool install --global RecursiveAI.OneScope.Tool

Requires .NET 10 (net10.0); .NET 9 and earlier aren’t supported.

Unzip the example, then dotnet run.

This writes crowd.oprp; then start the shell:

Terminal
onescope open crowd.oprp --headless --live /tmp/onescope.sock

Serves the example’s recorded crowd on a local socket. Keep this process running while the client connects.

Connect a client

In a second terminal, from the same folder, start the stdio bridge:

Terminal
onescope mcp --socket /tmp/onescope.sock

In your client’s configuration:

JSON
{ "mcpServers": { "onescope": { "command": "onescope", "args": ["mcp", "--socket", "/tmp/onescope.sock"] } } }

The JSON structure is from the OneScope README.

Run an offline inspection

The download creates its own input: an 8 × 8 walkable floor with one character moving from (1, 0, 1) toward (6, 0, 6), recorded for 30 steps. It restores a local OneScope tool pinned to 1.0.0, opens a headless shell, starts the stdio bridge, and sends two tools/call requests. RecursiveAI.OnePath 1.2.0 creates the replay. No model, account, or service key is used; the replies shown here come from that offline protocol client.

For a new console project, install the recording library and create the local tool manifest used by the program:

Terminal
dotnet add package RecursiveAI.OnePath --version 1.2.0
dotnet new tool-manifest
dotnet tool install RecursiveAI.OneScope.Tool --version 1.0.0
C#
using System.Diagnostics;
using System.Text.Json;
using RecursiveAI.OneMath;
using RecursiveAI.OnePath;
using RecursiveAI.OnePath.Replay;
 
// An 8 by 8 floor; true means walkable. No external input files.
var cells = Enumerable.Repeat(true, 64).ToArray();
var mesh = NavMesh.Build2D(cells, 8, 8, 1,
    new Vector2D(0, 0), NavSettings.Humanoid with { AgentRadius = 0 });
var recorder = new CrowdRecorder(mesh.CreateCrowd(), 1.0 / 30, keepSteps: true);
var agent = recorder.Add(new Vector3D(1, 0, 1), Agent.Small);
recorder.SetDestination(agent, new Vector3D(6, 0, 6));
for (int i = 0; i < 30; i++) recorder.Step();
File.WriteAllBytes("crowd.oprp", recorder.ToReplay().ToBytes());
 
// Restore the pinned local tool; no model or service key is needed.
using (var restore = Start("tool", "restore"))
{
    await restore.StandardOutput.ReadToEndAsync();
    await restore.WaitForExitAsync();
    if (restore.ExitCode != 0) throw new InvalidOperationException("Tool restore failed.");
}
string socket = Path.Combine(Path.GetTempPath(), $"scope-{Guid.NewGuid():N}.sock");
using var shell = Start("tool", "run", "onescope", "--", "open", "crowd.oprp",
    "--headless", "--live", socket);
_ = shell.StandardError.ReadToEndAsync();
try
{
    var deadline = DateTime.UtcNow.AddSeconds(20);
    while (!File.Exists(socket) && !shell.HasExited && DateTime.UtcNow < deadline)
        await Task.Delay(20);
    if (!File.Exists(socket)) throw new InvalidOperationException("Shell did not start.");
    using var bridge = Start("tool", "run", "onescope", "--", "mcp", "--socket", socket);
    try
    {
        // These are the same JSON-RPC messages a client sends over stdio.
        Console.WriteLine(await Call(bridge, 1, "seek", new { step = 30 }));
        Console.WriteLine(await Call(bridge, 2, "entities", new { kind = "agent", limit = 1 }));
    }
    finally { if (!bridge.HasExited) bridge.Kill(entireProcessTree: true); }
}
finally { if (!shell.HasExited) shell.Kill(entireProcessTree: true); }
 
static Process Start(params string[] arguments)
{
    var info = new ProcessStartInfo("dotnet")
    {
        RedirectStandardInput = true, RedirectStandardOutput = true,
        RedirectStandardError = true, UseShellExecute = false
    };
    foreach (string argument in arguments) info.ArgumentList.Add(argument);
    return Process.Start(info) ?? throw new InvalidOperationException("Could not start dotnet.");
}
 
static async Task<string> Call(Process bridge, int id, string name, object arguments)
{
    string message = JsonSerializer.Serialize(new
    {
        jsonrpc = "2.0", id, method = "tools/call",
        @params = new { name, arguments }
    });
    await bridge.StandardInput.WriteLineAsync(message);
    await bridge.StandardInput.FlushAsync();
    string line = await bridge.StandardOutput.ReadLineAsync()
        ?? throw new IOException("Bridge closed without a reply.");
    using var reply = JsonDocument.Parse(line);
    var result = reply.RootElement.GetProperty("result");
    if (result.TryGetProperty("isError", out var error) && error.GetBoolean())
        throw new InvalidOperationException(result.ToString());
    return result.GetProperty("content")[0].GetProperty("text").GetString()!;
}
JSON
{"step":30,"start":0,"steps":30,"sources":[{"source":0,"step":30,"hash":"8565f30e87defd2f"}]}
{"source":0,"step":30,"entities":[{"id":4611686018427387904,"parent":13835058055282163713,"kind":"agent","name":"agent 0"}],"count":1,"truncated":false}

The program throws if the shell does not start, the bridge closes without a reply, or an operation reports an error; it stops the subprocesses it started when finished.

Variations

For a shell you already have running, call the same operation directly from another terminal:

Terminal
onescope seek --step 30 --socket /tmp/onescope.sock

It returns the same seek JSON shown above.

The short answer

Open a simulation recording with onescope open --live, then configure your Model Context Protocol client to run onescope mcp --socket against that shell. The bridge forwards requests from stdio to the shell’s local socket. Tools can seek through a recording, list entities, inspect properties, and request captures without using the window’s controls.

The protocol supplies access to a deterministic simulation viewer; it does not supply a model or decide what to investigate. The example deliberately uses a small offline client to demonstrate the requests and replies without making claims about any assistant’s reasoning.

How does Model Context Protocol work with a simulation?

There are three separate pieces. The shell owns the opened sources and the time cursor. The stdio bridge connects a client to that shell. The client chooses an operation and consumes its JSON result. A single shell serves the same operations whether they originate in the window, on the command line, or over the protocol.

Start the shell first. The --live option means “serve operations,” not “attach to a live physics process.” You can serve a saved replay, as this example does. To inspect a running host instead, first attach the shell to a probe-equipped program. File and live sources then expose the same inspection operations, with the live source additionally reporting the received step range.

The sample’s Call method sends tools/call with a name and arguments. It reads the response’s text content as the operation result and checks isError before treating it as a successful reply. This is an MCP server (Model Context Protocol®) bridge for the viewer, not a separate simulation implementation. The replay still runs through its library module.

How can an AI assistant inspect a simulation step?

Ask for a particular step before asking about an entity. Otherwise a result can refer to whatever cursor position a previous operation left active. A useful sequence is: identify the source, seek, list entities of the relevant kind, then inspect an ID returned by that list. The program demonstrates the seek and entity-list portion, with a small limit on the latter so the reply stays bounded.

Keep entity IDs exact. OneScope accepts a decimal string where an operation expects an entity ID. That avoids rounding through a client that represents all JSON numbers as floating-point values. For multiple recordings, specify the source as well rather than relying on the active-source default.

A returned hash names the module’s reconstructed state at that step; it is not an assessment of whether the behavior is desirable. Findings and properties are separate results. Ask for those if you need a module’s diagnostics or a character’s actual state rather than guessing from a capture alone.

Changes also have an explicit boundary: declared pokes apply to forks. A client can explore a different impulse or destination on a fork without changing the original recording or the running host. The shell reference lists the operation arguments and error shapes. Keep those distinctions in the client’s instructions so an explanation of the original run is not accidentally based on a modified branch.

All onescope recipes