---
title: "Let an AI assistant inspect a simulation"
url: https://recursiveai.org/onescope/cookbook/let-an-ai-agent-inspect-a-simulation/
description: "Expose OneScope simulation inspection through the Model Context Protocol: open a live shell, connect a stdio client, and seek and inspect recorded state."
version: "1.0.0"
---

# 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.

[NuGet](https://www.nuget.org/packages/RecursiveAI.OneScope.Tool) · [GitHub](https://github.com/recursive-ai-oss/onescope-csharp) · [Docs](https://recursiveai.org/onescope/docs/) · [Download example (.zip)](https://recursiveai.org/onescope/cookbook/let-an-ai-agent-inspect-a-simulation/let-an-ai-agent-inspect-a-simulation.zip)

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

```shell
dotnet tool install --global RecursiveAI.OneScope.Tool
```

Requires [.NET 10](https://dotnet.microsoft.com/download/dotnet/10.0) (net10.0); .NET 9 and earlier aren’t supported.

Unzip [the example](https://recursiveai.org/onescope/cookbook/let-an-ai-agent-inspect-a-simulation/let-an-ai-agent-inspect-a-simulation.zip), then `dotnet run`.

This writes `crowd.oprp`; then start the shell:

```shell
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:

```shell
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](https://github.com/recursive-ai-oss/onescope-csharp/blob/d681be56420836ea0d8874416367bc8181235813/README.md#user-content-mcp).

## 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:

```shell
dotnet add package RecursiveAI.OnePath --version 1.2.0
dotnet new tool-manifest
dotnet tool install RecursiveAI.OneScope.Tool --version 1.0.0
```

```csharp
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:

```shell
onescope seek --step 30 --socket /tmp/onescope.sock
```

It returns the same seek JSON shown above.

## Related recipes

- [Visualize a navigation mesh and crowds](https://recursiveai.org/onescope/cookbook/visualize-navmesh-and-crowds/)
- [Attach to a running physics game](https://recursiveai.org/onescope/cookbook/attach-to-a-running-game/)

## 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](https://recursiveai.org/onescope/docs/shell/) 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.
