NAMEFRAME Frame the world. Name the frames.

MCP server and AI control

NameFrame ships a Model Context Protocol server, so an assistant such as Claude can read your scene, propose a scenario, dry-run it and, only with your explicit approval, start a generation task. Reading is free. Writing is deliberately hard.

The safety model in one paragraph#

The MCP server delegates to the public NameFrame Python SDK and API. It never calls Unreal Remote Control directly, never runs shell commands, never evaluates Python, and never passes filesystem paths through. Every write tool defaults to a dry run and requires an explicit approved flag plus an idempotency key. When an approval store is configured, the request is parked in a durable queue that a human clears from inside the Unreal Studio panel. List responses are capped, planning inputs are size-limited, and plans expire.

Install and run#

python -m pip install -e ".[mcp]"
nameframe api-serve --scene-manifest _local/scene_manifest.json
nameframe mcp-serve --api-url http://127.0.0.1:8010

mcp-serve speaks STDIO by default, so stdout is reserved for protocol messages and diagnostics go to stderr. It uses the official Python MCP SDK v2 and the 2026-07-28 protocol revision.

Connecting a client

Point your MCP client at the command. A typical STDIO client entry:

{
  "mcpServers": {
    "nameframe": {
      "command": "nameframe",
      "args": ["mcp-serve", "--api-url", "http://127.0.0.1:8010"]
    }
  }
}

Inside Unreal, the Copy Setup button on the Settings page puts token-free client setup instructions on your clipboard, with secrets represented only as an environment variable name.

Scopes#

The server starts with safe read-only scopes. Write scopes have to be granted explicitly, one --scope flag at a time.

Granted by defaultMust be granted explicitly
nameframe:read
nameframe:scene:read
nameframe:config:read
nameframe:preview
nameframe:audit:read
nameframe:task:read
nameframe:metadata:read
nameframe:analytics:read
nameframe:coverage:read
nameframe:model:read
nameframe:experiments:read
nameframe:scene:write
nameframe:config:write
nameframe:task:write
nameframe:generation:write
nameframe mcp-serve --api-url http://127.0.0.1:8010 --scope nameframe:config:write --output-root D:/NameFrame/_local/runs

--disable-writes forces safe read-only scopes regardless of what was granted, which is the fastest way to make a running endpoint harmless. --output-root allowlists where generation may write. It takes absolute paths and can be repeated.

Environment variables

VariableEffect
NAMEFRAME_API_URLLocal API base URL (loopback HTTP only)
NAMEFRAME_MCP_SCOPESComma-separated extra scopes
NAMEFRAME_MCP_ACTORBounded actor identity recorded in the audit
NAMEFRAME_MCP_OUTPUT_ROOTSComma-separated allowed output roots
NAMEFRAME_MCP_TIMEOUTAPI read timeout, 0.05 to 300 seconds
NAMEFRAME_MCP_LOG_PATHAbsolute path for the bounded JSONL request log

Authenticated HTTP#

For anything beyond a single local client, use the Streamable HTTP transport. It requires a control-plane project and a bearer token, and adds RBAC, project binding, quotas and durable audit.

nameframe control-init _local/control.json
nameframe auth-user-create _local/control.json --user analyst --role operator
nameframe auth-token-create _local/control.json --user analyst

nameframe mcp-serve --transport streamable-http --control-store _local/control.json --project-id project_xxxx --port 8020 --max-requests-per-minute 120 --max-generation-tasks-per-day 20

Tokens are verified either against the local control store or via RFC 7662 OAuth introspection, using --token-verifier oauth-introspection with --oauth-introspection-url and --oauth-client-id. The host defaults to loopback.

The approval queue#

With --approval-store <absolute path>, generation requests do not run when an assistant asks. They get recorded as durable pending requests and wait for a human.

From the command line:

nameframe mcp-approval-list _local/approvals.json --status pending
nameframe mcp-approval-decide _local/approvals.json <request-id> --approve
nameframe mcp-approval-decide _local/approvals.json <request-id> --deny

From inside Unreal, the AI / MCP section on the Studio Settings page shows the queue with Inspect Plan, Approve and Deny. Approving one request approves exactly that request. The client still has to retry, and nothing else is unlocked.

Panic buttons

Disable MCP Writes restarts a running endpoint with read-only scopes, or forces the next start to be read-only. Disconnect Clients stops the endpoint and closes every current session. Both are on the Studio Settings page.

Diagnosing a connection#

nameframe mcp-doctor

Read-only end-to-end diagnosis: whether the API answers, whether Unreal is reachable, whether the analytics index exists, and which scopes are in effect. Run it first when a client reports a problem.

Tool reference#

Grouped by what they are allowed to do. Read-only tools need no approval. Controlled writes default to a dry run and require both approved and an idempotency key.

38 tools are exposed by this build.

Read only Read only

Never writes. No approval, safe to call at will.

ToolParametersWhat it does
nameframe.server.healthnoneInspect NameFrame MCP, API, Unreal, and analytics health.
nameframe.enterprise.statusnoneInspect project-scoped quota, workers, shards, jobs, and recent audit.
nameframe.observability.statusnoneRead bounded payload-free MCP request metrics.
nameframe.scene.getdetail_level: str = "summary"Read the current scene at summary or full detail.
nameframe.scene.scandetail_level: str = "summary"Request a fresh read-only scene scan through the NameFrame API.
nameframe.scene.validategeneration_ready: bool = True, strict: bool = FalseValidate the current scene and return blockers, warnings, and repairs.
nameframe.class.listlimit: int = 50, offset: int = 0List a bounded page of registered semantic classes.
nameframe.run.getrun_id: strRead one existing run by identifier.
nameframe.analytics.summaryrun_id: str | None = NoneRead bounded aggregate analytics, optionally with one run.
nameframe.metadata.search_framesfilters: dict[str, Any] | None = None, limit: int = 50, offset: int = 0Search indexed frames through fixed filters; arbitrary SQL is not accepted.
nameframe.analytics.compare_runsbaseline_run_id: str, candidate_run_id: str, representative_limit: int = 12Compare two indexed runs in memory without writing report artifacts.
nameframe.analytics.annotation_issuesrun_id: str = "", limit: int = 100Read bounded, measured annotation-health issues from the metadata index.
nameframe.model.evaluateresults: list[dict[str, Any]], metric: str = "map50", pass_threshold: float = 0.5, slice_keys: list[str] | None = NoneEvaluate measured model-result rows over bounded declared slices.
nameframe.model.failuresevaluation: dict[str, Any], max_slices: int = 10Mine underperforming slices from measured evaluation output.
nameframe.model.compare_synthetic_realreal_profile: dict[str, Any], synthetic_profile: dict[str, Any]Compare bounded measured real and synthetic dataset profiles.
nameframe.experiment.regression_campaignname: str, evaluations: list[dict[str, Any]], required_pass_rate: float = 1.0,Aggregate measured evaluations into a bounded regression campaign gate.
nameframe.scenario.validatescenario: dict[str, Any], strict: bool = FalseValidate an inline ScenarioConfig against the current scene.
nameframe.coverage.evaluateaxes: dict[str, list[Any]], observations: list[dict[str, Any]], target_per_cell: int = 1Evaluate a bounded coverage matrix without reading raw dataset arrays.
nameframe.coverage.gapsmatrix: dict[str, Any], limit: int = 100Rank missing and under-covered cells from a NameFrame coverage matrix.
nameframe.task.listlimit: int = 50, status: str = ""List up to 100 durable NameFrame tasks, optionally filtered by status.
nameframe.task.gettask_id: strRead one durable task and its latest capture progress.

Computed Computed

Derives a result in memory. Risk R1, no persistent write.

ToolParametersWhat it does
nameframe.model.domain_gapcomparison: dict[str, Any], threshold: float = 0.1Derive labelled recommendations from measured profile deltas.
nameframe.scenario.dry_runscenario: dict[str, Any], samples: int = 8, start_index: int = 0Resolve deterministic representative samples without generation or writes.
nameframe.coverage.correction_planmatrix: dict[str, Any], limit: int = 100, max_additional_frames: int = 100_000Draft corrections from measured deficits; never approve or start generation.

Plan Plan

Produces an immutable session-local plan. Never starts generation.

ToolParametersWhat it does
nameframe.model.hard_case_planfailure_report: dict[str, Any], samples_per_slice: int = 1000Draft an immutable evidence-linked hard-case plan without generation.
nameframe.dataset.planscenario: dict[str, Any], requested_frames: int, annotation_tasks: list[str] | None = None, exporters: list[str] | None = None, split_policy: dict[str, float] | None = None, quality_profile: str = "balanced", output_destination: str | None = None, coverage_targets: dict[str, Any] | None = NoneCreate an immutable session-local plan; never start dataset generation.
nameframe.dataset.validatescenario: dict[str, Any] | None = None, idempotency_key: str = ""Start a durable validation task without generation.

Preview Preview

Writes temporary preview state only.

ToolParametersWhat it does
nameframe.preview.generatescenario: dict[str, Any], count: int = 1, layer: str = "metadata", representative_mode: str = "scene_layout"Generate up to eight temporary metadata scene-layout previews.

Controlled write Controlled write

Risk R2. Defaults to a dry run; a real write needs approved plus an idempotency key.

ToolParametersWhat it does
nameframe.scenario.updatepatch: dict[str, Any], expected_config_hash: str, dry_run: bool = True, approved: bool = False, idempotency_key: str = ""Patch the active scenario with hash conflict checks; dry-run is the default.
nameframe.spawner.updatepatch: dict[str, Any], expected_config_hash: str, dry_run: bool = True, approved: bool = False, idempotency_key: str = ""Patch only the active scenario spawn section; dry-run is the default.
nameframe.camera.updatepatch: dict[str, Any], expected_config_hash: str, dry_run: bool = True, approved: bool = False, idempotency_key: str = ""Patch only the active scenario camera section; dry-run is the default.
nameframe.environment.updatepatch: dict[str, Any], expected_config_hash: str, dry_run: bool = True, approved: bool = False, idempotency_key: str = ""Patch only the active scenario environment section; dry-run is the default.
nameframe.scene.helper.createhelper: dict[str, Any], expected_config_hash: str, dry_run: bool = True, approved: bool = False, idempotency_key: str = ""Create one allowlisted NameFrame helper; dry-run is the default.
nameframe.dataset.generateplan_id: str, approved: bool = False, idempotency_key: str = ""Start one approved immutable plan and immediately return a durable task.
nameframe.dataset.canceltask_id: str, approved: bool = False, idempotency_key: str = ""Request safe cancellation and preserve completed frames.
nameframe.model.generate_hard_caseshard_case_plan_id: str, case_id: str, dataset_plan_id: str, approved: bool = False, idempotency_key: str = "",Start one approved dataset task linked to measured hard-case evidence.

Task, non-idempotent Task, non-idempotent

Changes durable task state and cannot be safely repeated.

ToolParametersWhat it does
nameframe.dataset.retrytask_id: str, approved: bool = False, idempotency_key: str = ""Retry a failed or cancelled immutable task as a new archived attempt.
nameframe.dataset.resumetask_id: str, approved: bool = False, idempotency_key: str = ""Resume a restart-paused immutable task after snapshot checks.

Resources#

Alongside tools, the server exposes read-only resources for clients that prefer to browse rather than call:

nameframe://server/health, nameframe://capabilities, nameframe://project/current, nameframe://scene/current, nameframe://scene/current/validation, nameframe://scenario/current, nameframe://classes, nameframe://runs, nameframe://runs/latest, nameframe://runs/{run_id}, nameframe://plans/{plan_id}, nameframe://audit/recent

Limits you should know about#

LimitValue
List responsescapped at 100 records
Dry-run sample rowsat most 100
Dataset plan lifetime60 minutes, session-local, immutable
Representative samples in a planat most 8
Previews per requestat most 8, metadata/scene-layout layer only
Coverage evaluation4 axes, 1,000 cells, 10,000 observations
Planning input size256 KB

Capability discovery states explicitly that arbitrary shell, Python, Unreal console and filesystem passthrough are not available. That is a contract, not an oversight.

A realistic assistant workflow#

  1. Read

    nameframe.scene.get and nameframe.scene.validate, to see what is in the level and what is blocking.

  2. Propose

    nameframe.scenario.validate then nameframe.scenario.dry_run, which checks a scenario and shows its deterministic samples with no side effects.

  3. Preview

    nameframe.preview.generate, up to eight temporary layout previews.

  4. Plan

    nameframe.dataset.plan returns an immutable plan document. Planning never starts generation.

  5. Ask

    nameframe.dataset.generate with the plan id. Without approval it gets parked in the queue.

  6. Approve

    You inspect and approve it in Studio. The client retries and gets a durable task id.

  7. Watch

    nameframe.task.get until it completes, then nameframe.analytics.summary.