nrc-mcp
Provides a mesh handoff to Blender: nrc-mcp writes a brief and validates meshes that come back, without launching Blender itself.
Click on "Install Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@nrc-mcpValidate my map and show any errors or warnings."
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
nrc-mcp
An MCP server for designing, sculpting, optimizing and shipping levels for NetRadiant-custom, targeting Urban Terror.
nrc-mcp owns the .map file. It parses and writes it losslessly, derives geometry with exact
arithmetic, drives q3map2, and exposes all of it to an agent as 47 tools and 5 resources.
A human gets the same capabilities through a CLI and a set of mise tasks.
It is not an editor and not a plugin. It reads and writes the same files NetRadiant does, so you can keep the editor open and use both.
Contents
Reading the output — confidence, and what a warning means
Limits — what it will refuse to do
Related MCP server: onion-mcp-server
Requirements
Always needed
mise — the only build/run interface. It installs and pins everything else.
A POSIX shell. Linux, macOS or WSL. Native Windows is untested.
That is enough for parsing, validation, rendering, sculpting, analysis and the whole test suite. No GPU, no display, no game install, no editor.
Needed only to compile, package or read BSPs
q3map2— from a NetRadiant-custom install, or built from source withmise run vendor:build.A game installation, for
-fs_basepath. For Urban Terror that is the directory containingq3ut4/.
Optional
NetRadiant-custom itself, if you want to see the map in the editor. Its gamepack is also the source the Urban Terror profile was extracted from.
Blender, for the mesh handoff.
nrc-mcpnever launches it; it writes a brief and validates what comes back.
Install
git clone <this repo> && cd nrc-mcp
mise trust # approve this repo's mise config
mise install # rust, python 3.12, uv — pinned
mise run bootstrap # build the kernel, generate the corpus
mise run test # prove it works before trusting anythingmise run test should end with:
syntactic: 49/49 byte-identical (3343 brushes, 10 patches)
semantic: 6/6 compiled BSPs identical
GATE GREENAlongside 265 Rust tests and 248 Python tests. The semantic line only appears if a compiler is
configured; without one it is skipped, not failed.
If the gate is red, stop. Nothing downstream of the kernel is trustworthy while a map does not survive a round-trip. See Troubleshooting.
Configure
Machine-specific paths go in mise.local.toml, which is gitignored. Nothing in mise.toml
needs editing.
[env]
# Where the game lives. The directory that CONTAINS q3ut4/, not q3ut4 itself.
URT_BASEPATH = "/mnt/c/Program Files/UrbanTerror43"
URT_GAMEDIR = "/mnt/c/Program Files/UrbanTerror43/q3ut4"
# The compiler.
Q3MAP2 = "/mnt/c/Program Files/NetRadiant/NetRadiant-custom-20240309/q3map2.exe"
# Set to "windows" when q3map2 is a .exe reached from WSL. Omit on native Linux/macOS.
NRC_Q3MAP2_MODE = "windows"
# The gamepack .ent files — the only accepted source for the entity ontology.
URT_GAMEPACK = "/mnt/c/Program Files/NetRadiant/NetRadiant-custom-20240309/gamepacks/urt.game/q3ut4"
# Your own .map sources. Read-only: corpus:import copies out, never writes back.
URT_MAPSRC = "/mnt/c/Program Files/UrbanTerror43/q3ut4/maps"Variable | Required for | Notes |
| compiling, packaging | Passed as |
| shader auditing | Where |
| compiling, BSP reports | Defaults to the source build under |
| WSL only |
|
| re-extracting a profile | Not needed to use the shipped profile. |
|
| Optional. The corpus works without your maps. |
| — | Active game profile. Defaults to |
| — | Set to |
Check what resolved:
mise run infoIf q3map2 is a Windows .exe and you are in WSL
This is a supported and tested configuration, and the one thing worth knowing is that
q3map2.exe cannot read /home/... paths and rejects UNC paths like
\\wsl$\Ubuntu\home\... outright. tools/q3map2.py handles it: arguments are translated with
wslpath -w, and a map that lives on the Linux side is staged to a Windows-side directory before
compiling, then results are copied back. You do not have to do anything except set
NRC_Q3MAP2_MODE = "windows".
The consequence to expect: compiling a map under /home is slower than compiling one already on
/mnt/c, because of the staging copy.
Connect an MCP client
The server speaks JSON-RPC on stdio.
Claude Code
claude mcp add nrc -- mise -C /absolute/path/to/nrc-mcp run mcp:serveThe -C is what lets the server start from anywhere; drop it if you always launch from inside the
repo.
Any client with a JSON config (claude_desktop_config.json, .mcp.json, …)
{
"mcpServers": {
"nrc": {
"command": "mise",
"args": ["run", "mcp:serve"],
"cwd": "/absolute/path/to/nrc-mcp"
}
}
}cwd matters — it is how mise finds the config that supplies every path. If your client cannot set
a working directory, put it in the arguments instead:
"args": ["-C", "/absolute/path/to/nrc-mcp", "run", "mcp:serve"].
Verify without a client
mise run mcp:tools # print the whole surface, and the limits, and exit
mise run mcp:inspect # the official MCP inspector, in a browsermise run mcp:serve writes only protocol on stdout — diagnostics go to stderr — so it is safe to
point a client straight at it.
Your first session
A complete pass, in the order that works. Every step is a tool call; the shell equivalents are shown where one exists.
1 — Open a map. Everything else operates on the currently open map.
{"tool": "map_open", "path": "corpus/real/ut4_woolis.map"}2 — Look at the numbers.
{"tool": "map_stats", "grid": 8}Counts, bounds, a shader histogram, and grid alignment. grid is the authoring grid you want
alignment measured against — it changes the report, never the map.
3 — Look at the map. Sculpting blind fails, so do this early and often.
{"tool": "render_contact_sheet"}
{"tool": "render_topdown", "overlay": "structural"}The image comes back in the response, not as a file — there is no output path to choose. Counts,
dimensions, scale and warnings arrive as structured data alongside it, so you read an exact number
instead of reading your own render. Use the nrc render CLI when you want a PNG on disk.
Orthographic views are wireframe on purpose: a filled top-down of a sealed map shows you the
underside of its sky brush and nothing else. Wireframe gives a real floor plan, with rooms and stairs
legible through the ceiling. Perspective views render solid. Pass solid=true to a top-down only
when you know the geometry is open.
Overlays: structural separates structural from detail brushes, brush entities and patches; caulk
shows which surfaces are never drawn in game; off_grid marks vertices that miss the grid.
4 — Validate, in two passes. Geometry and file format first, then the game's own rules.
{"tool": "validate", "grid": 8}
{"tool": "validate_profile"}5 — Build something. Geometry is described as an intersection of half-spaces, so a non-convex brush is not expressible. Compile it, look at it, then commit it.
A room is a hollowed box, which is 6 brushes — floor, ceiling, four walls:
{"tool": "solid_compile", "ir": {
"op": "hollow",
"solid": {"op": "box", "min": [0, 0, 0], "max": [512, 512, 256]},
"thickness": 16}}A doorway is a subtraction, and it comes out as 3 brushes — left column, right column, lintel — which is what a mapper would draw by hand:
{"tool": "solid_compile", "ir": {
"op": "subtract",
"from": {"op": "box", "min": [0, 0, 0], "max": [512, 16, 256]},
"cut": [{"op": "box", "min": [224, -8, 0], "max": [288, 24, 112]}]}}The cutter deliberately overshoots the wall on both faces (y from -8 to 24 through a 16-thick
wall). If a doorway compiles to more pieces than you expect, that overshoot is usually what is
missing. Composing the two — cutting the same doorway out of the whole shell — gives 10 brushes
rather than 8, because the cutter also crosses the brushes adjoining that wall; solid_compile
reports the count before anything is committed, which is the point of running it first.
{"tool": "solid_preview", "ir": {"...": "..."}, "view": "sheet"}
{"tool": "solid_commit", "ir": {"...": "..."}, "label": "north_room",
"textures": {"default": "caulk", "faces": {"floor": "concrete_01"}}}
{"tool": "map_save"}label is required, and it is worth choosing well: it names the shape in the recorded sidecar and is
how you edit it later. Default textures.default to caulk and override only the faces a player can
see — the compiler discards caulk faces, so a caulked hidden face costs nothing.
solid_help is the full operator reference. solid_list and solid_edit_param then let you change a
committed shape by one field: solid_edit_param("north_room", "solid.max[1]", 640) makes the room
deeper and rebuilds it. It previews by default — pass preview_only=false to apply.
6 — Compile. Start with draft; it is the fastest thing that still tells you the truth about
whether the map seals.
{"tool": "compile_map", "path": "maps/mymap.map", "preset": "draft"}Presets are draft, iterate, quality, final. If it leaks, leak_trace reads the pointfile
and tells you where the hole is.
7 — Optimize, then ship.
{"tool": "structural_audit"}
{"tool": "ship_check", "target": "mymap"}
{"tool": "pack_pk3", "bsp": "out/mymap.bsp"}structural_audit is the biggest single lever on compile time and runtime visibility cost:
structural brushes generate portals, and most brushes do not need to be structural.
Tool reference
47 tools. mise run mcp:tools prints this list live.
The map
Tool | Does |
| Open a |
| Counts, bounds, shader histogram, grid alignment. |
| Write it back, byte-identically where untouched. |
| Entities, filterable by classname. |
| Exact vertices and derived properties of one brush. |
| Geometry and file-format findings. |
| Entities against the game profile's rules. |
| What the profile knows, and how much of it is verified. |
Seeing it
Tool | Does |
| Top-down (XY) view. |
| Perspective, or a front/side orthographic view. |
| Three orthographic views plus a perspective, in one image. |
| What a standing player sees from a floor position. |
Sculpting
Tool | Does |
| Every operator, its fields, and the on-grid caveat. |
| Compile geometry and report it, touching nothing. |
| Compile and render, without committing. |
| Compile and add the brushes to the open map. |
| Structure of a recorded shape; everything recorded. |
| Change one parameter and rebuild. |
Meshes — the Blender handoff
Tool | Does |
| Brush, patch or mesh? Decides, and says why. |
| A numerically complete brief plus a ready-to-send prompt. |
| Validate an exported mesh against the brief that asked for it. |
| Build the entity that puts it in the world. |
| Fit a convex collision hull, returned as geometry you can commit. |
Compiling and optimizing
Tool | Does |
| Run q3map2 with a named preset. |
| Read a compiled BSP; compare its entities to the source. |
| Brushes marked structural that need not be. |
| Hint brush planes, proposed from a portal file. |
| Where the map leaks, from the pointfile. |
| Shader references against the scripts on disk. |
| Compile two variants and diff what matters; every past comparison. |
Gameplay analysis — all constants read from the profile, never hardcoded
Tool | Does |
| Build the walkable grid; report size and coverage. |
| Per-team distance from each spawn group to each objective. |
| Sightline length distribution and power positions. |
| Clearances against verified movement constants. |
| Exits per spawn, and distance to the nearest enemy spawn. |
Shipping
Tool | Does |
| Naming, levelshot, arena file, package contents. |
| Build the release archive. |
| Every resource the BSP actually references. |
The toolchain itself
Tool | Does |
| Every capability, discovered from mise; run one. |
| The fitness suite. |
| The paths self-modification may never touch, and their hash pins. |
| Upstream drift; the contribution plan. |
Resources
URI | Contents |
| The live mise task list — capability discovery, always current. |
| A game profile as YAML. |
| Representation tiers, caulk, grid discipline, authoring order. |
| Claims from the design document that did not survive verification. |
| The open map. |
Read nrc://corrections before trusting a rule. The design document was written partly from
recollection; several of its claims were wrong, including three it listed as verified.
Reading the output
Findings carry a severity, and severity is earned. A rule may only fail your build if its
confidence is verified — meaning it was checked against the gamepack or the engine source.
Anything unverified is clamped to info and can never fail anything, however plausible it looks.
That mechanism is not bureaucratic. The design document asserted that Team Survivor needs dedicated
spawn entities; the gamepack says otherwise, and a validator built on that claim would have failed
correct maps. nrc://corrections records each one.
A warning is sometimes about the tool, not your map. BRUSH_OFF_GRID on a rotated prism is
unavoidable — a .map stores planes, and a brush's vertices are wherever three planes meet, which
for anything angled is not on the grid and cannot be made so. The same finding on axis-aligned
geometry is a real defect. validate reports the count; deciding needs to know which shape produced
it.
BRUSH_NOT_EXACT means "excluded", not "broken". See Limits.
Command line
Every capability is a mise task, and the task list is the agent's action surface, so anything an agent did is a command you can paste into a shell.
mise tasks # all 41
mise run info # resolved paths and versions
mise run render corpus/real/ut4_dofa.map out/dofa.png
mise run compile:draft maps/mymap.map
mise run test:diff # the round-trip gate alone
mise run bench # the fitness suite
mise run watch # re-run checks on changeThere is also a direct kernel binary, useful in scripts and pipes. mise run kernel:build puts it at
target/release/nrc; it is not installed onto PATH, so either call it by path or
export PATH="$PWD/target/release:$PATH".
nrc roundtrip <file.map>... # verify byte-identical load/save
nrc stats <file.map> [--grid N] # JSON
nrc validate <file.map> [--grid N] # JSON; exit 1 if findings
nrc normalize <file.map> --write # re-serialize in place (refuses without --write)
nrc render <file.map> --out x.png --view top --overlay structuralExit codes: 0 clean, 1 findings or did not round-trip, 2 tool error. Add --quiet for JSON
only, --pretty to indent it.
How it works
agent ──MCP──► nrc-mcp ──► nrc-core (Rust) .map I/O, exact geometry, validators
│
├──► mise run <task> ──► q3map2 / mbspc / cargo / uv
└──► profiles/*.yaml the only game-specific layerLossless before anything else. Load and re-save any .map byte-identically. mise run test:diff
checks it two ways: parse and re-serialize and require identical bytes, then compile both the
original and the re-serialized copy and compare the geometry lumps of the resulting BSPs. Current
state over 49 maps — 8 real Urban Terror sources, upstream's 18 pathological regression maps and 23
synthetic ones — is 49/49 byte-identical and 6/6 compiled BSPs identical.
Getting there meant discovering things the format is not documented with: the exact float formatting
(%10.10lf, trailing zeroes stripped, so -0 is a real literal), the leading newline every file
this fork writes begins with, and trailing whitespace preserved verbatim — one real map ends
}\r\n\r\n\r\n and was the last holdout. Numbers remember the text they were parsed from, so an
untouched map reproduces its own bytes and a modified one differs only where it was modified.
Comments, key order, duplicate keys, line endings, layer records and primitives whose syntax is
unrecognized all survive.
Exact predicates, or an honest refusal. Coplanarity, convexity, plane identity and grid
membership use integer and rational arithmetic, not epsilons. Brush vertices come from intersecting
every triple of face planes exactly and keeping the points that satisfy every half-space, so
convexity is guaranteed rather than checked. Where the input is off-grid the kernel reports
Indeterminate instead of guessing, because a guessed side is a sliver and a sliver is a leak three
weeks later.
Subtraction that a human would recognize. A \ B = ⋃ᵢ (A ∩ h₁ ∩ … ∩ hᵢ₋₁ ∩ ¬hᵢ) over B's
half-spaces: every term is convex by construction, and terms that would be slivers are exactly
empty and vanish. Adjacent pieces then merge where their union is genuinely convex, by an exact test
— for P and Q sharing plane h, the union is convex iff every other plane of P contains all
of Q and vice versa. That has to be exact, because merging wrongly fills the doorway back in.
mise as the action surface. The server never shells out to a raw command; it calls
mise run <task>. Capability discovery is therefore free, and new abilities need no server code.
One game-specific layer, enforced. Entity ontology, gametype ids, spawn rules and movement
constants live in profiles/*.yaml as data. The Urban Terror profile covers 95 entity classes and
carries an explicit confidence marker on 895 entries — 868 verified against the gamepack, 27 marked
unverified and therefore unable to fail a build.
mise run test:seam fails the build if a game-specific string appears in code, and it derives its
forbidden vocabulary from the profile itself, so it cannot fall behind.
Layout
crates/nrc-core/ the kernel: lex, parse, write, math, exact, winding, validate, stats
crates/nrc-solid/ half-space geometry: CSG, convex merge, brush emission
crates/nrc-render/ headless rasterizer: ortho and perspective, PNG out, no GPU
crates/nrc-cli/ `nrc` — roundtrip / stats / validate / normalize / render
crates/nrc-py/ PyO3 bindings; the server's in-process kernel
python/src/nrc_mcp/ the MCP server
tools/ corpus import, the differential harness, the q3map2 driver, the seam lint
profiles/ game profiles — the only game-specific layer
corpus/ real, upstream-regression and synthetic maps
contrib/mcpbridge/ an editor plugin for live editor state (never compiled — see docs/)
docs/ design notes and the spec correctionsLimits
Ask for any of these and you get an honest refusal rather than a plausible number.
Rotated geometry is partly invisible. This is the cost of the exact-predicate design and it shows
up on real maps. Any coordinate that is not an exact integer within world bounds is refused, and
everything downstream reports Indeterminate rather than picking a side. Geometry whose
plane-defining points are off-grid therefore cannot be evaluated at all, and rotated brushes are
the common case.
Measured: ut4_woolis and ut4_megastructunnel are 100% evaluable, while ut4_dofa has 478 of
1454 brushes the kernel declines. Those are absent from the navgrid, so every analysis report
carries the count, gives examples, and warns that a path may cross a wall. validate reports them as
BRUSH_NOT_EXACT.
The honest summary: fully precise about axis-aligned and 45° geometry, partly blind to arbitrarily
rotated geometry, and every report says which it is looking at. Closing the gap means either snapping
input — which changes the map — or adaptive floating-point predicates. crates/nrc-core/src/exact.rs
explains why the integer route came first.
No patch authoring. Patches are parsed, validated, tessellated and rendered, but cannot be created. Curved geometry is editor work.
No measured reference dimensions. There is no corpus of width/height/length distributions per space category, so sizing comes from the profile's verified constants. Related: no cover density or peek-angle analysis.
No traversal time. No player movement speed is verified, so every distance is reported in world units and never in seconds. Inventing a speed is exactly the failure this project is built to avoid — the design document assumed a 56-unit standing height, and the shipped gamepack says 69.375. A corridor sized from the wrong number passes every geometric check and still traps the player.
No live editor state. contrib/mcpbridge is a complete JSON-RPC plugin for NetRadiant-custom,
and it has never been compiled — there is no Qt5 environment here and the host compiler cannot
build that codebase at all. It is offered for review, not for use. docs/editor-bridge.md explains
the design; docs/pr-plan.md tracks readiness and reports this as unmet.
No kernel self-modification. The exact predicates, the differential harness, the fitness
definitions, the corpus and every verified rule are hash-pinned in bench/protected.json. The
opt-in self-tuning loop can only touch the prompt and resource layer, where a mistake cannot corrupt
a map. mise run selfdev:protected verifies the pins; if it fails, treat every fitness score as
meaningless until you know why.
Troubleshooting
Symptom | Cause and fix |
| The kernel could not reproduce that file's bytes. Nothing downstream is trustworthy. Run |
| No compiler configured. Set |
q3map2 exits immediately, or complains about the path | Almost always WSL path translation. Set |
|
|
MCP client shows no tools | The server needs to start in the repo. Set |
A rule fires that you believe is wrong | Check |
| Expected. A |
Analysis reports fewer brushes than the map has |
|
A top-down render is one flat grey rectangle | You forced |
| An |
| Something that defines correct changed. Treat all scores as meaningless and find out why before re-pinning. |
Contributing
mise run cimust be green. Iftest:diffis red, fix that first.The kernel has no dependencies, on purpose. A plane-intersection bug arriving through a transitive update is the failure mode this project can least afford.
Task names are an API. Renaming one breaks the agent's action surface.
Nothing in
python/srcparses.maptext. Twice a module reached for a second parser because an accessor was missing; both times the fix was to add the accessor to the kernel.Rules carry a
confidence, and onlyverifiedmay fail a build. Verify against the gamepack or the engine source — not against documentation, and not against recollection.mise run test:seamkeeps game-specific strings out of code. It reads its vocabulary from the profile, so it cannot fall behind.
Licence
GPL-2.0-or-later, matching NetRadiant-custom.
This server cannot be installed
Maintenance
Resources
Unclaimed servers have limited discoverability.
Looking for Admin?
If you are the server author, to access and configure the admin panel.
Related MCP Servers
- AlicenseBqualityDmaintenanceA comprehensive MCP server providing 30 tools for geocoding, routing, and OpenStreetMap data analysis. It enables AI assistants to search for locations, calculate travel routes, and perform quality assurance checks on map data.Last updated302MIT

onion-mcp-serverofficial
AlicenseAqualityCmaintenanceA feature-rich MCP server offering 30 tools across AI, code, text, data, web, and system categories, enabling tasks like chat, translation, code review, web scraping, and text processing.Last updated30MIT- Flicense-qualityDmaintenanceA highly configurable, deployment-ready MCP server with modular architecture for dynamic tool loading and external package support.Last updated1
- FlicenseAqualityCmaintenanceA robust MCP server with tools to search, install, configure, repair, and uninstall MCP servers, automating setup and maintenance across multiple AI and developer tools.Last updated417
Related MCP Connectors
A MCP server built for developers enabling Git based project management with project and personal…
An MCP server for deep research or task groups
This MCP server enables users to perform scientific computations regarding linear algebra and vect…
Latest Blog Posts
- Who's Calling? MCP Hosts Are an Identity Blind Spot (And the Spec Knows It)By Om-Shree-0709 on .mcpAgent IdentityOAuth 2.1
- Your AI Chatbot Just Exposed Your CEO's Salary to an InternBy Om-Shree-0709 on .Agent IdentityMCP SecurityOAuth Delegation
- Why MCP Servers Need Execution Sandboxing (And Why Your Current Stack Isn't Enough)By Om-Shree-0709 on .Agentic AiPrompt InjectionWebAssembly
MCP directory API
We provide all the information about MCP servers via our MCP API.
curl -X GET 'https://glama.ai/api/mcp/v1/servers/luukvhoudt/nrc-mcp'
If you have feedback or need assistance with the MCP directory API, please join our Discord server