A model writing Qu from memory gets it subtly wrong — a half-open range, a function that does not exist, a keyword spelled the Python way — and nobody finds out until a person runs the script. Qu ships an MCP server so the assistant can check instead: parse it, run it, look up the name, draw the figure and read the labels back.
Model Context Protocol
qu mcpOne command. The server speaks JSON-RPC over stdin and stdout, the
standard MCP stdio transport, and it is part of the qu binary — there
is nothing else to install, and no separate process to keep in step with the
engine you are actually running.
Claude Code, in one line:
claude mcp add qu -- qu mcp
Or the configuration file directly — the same mcpServers block
every MCP client uses. For Claude Desktop that is
%APPDATA%\Claude\claude_desktop_config.json on Windows and
~/Library/Application Support/Claude/claude_desktop_config.json on
macOS:
{
"mcpServers": {
"qu": {
"command": "qu",
"args": ["mcp"]
}
}
}
If qu is not on the path, give the full path to the binary
instead. Restart the client; the tools appear under qu.
| Tool | What it settles |
|---|---|
qu_eval |
Runs Qu source and returns everything it printed. Printing the shape or the first few values of a result costs one call and settles the question that would otherwise be answered by assertion. |
qu_check |
Parses without executing and returns diagnostics with line and column.
Cheaper than running, and safe on code with side effects. It catches the
structural mistakes — a missing end for, a Python-style colon —
but not undefined names, which are a runtime error in Qu. |
qu_builtins |
Searches the engine's own dispatch table. This is the authority on whether a function exists: the table is the one the interpreter dispatches on, not a list in a document. A model that checks here does not hallucinate an API. |
qu_docs |
Reads a documentation page, optionally filtered to the sections that mention a term. Sixteen pages are compiled into the binary, so this works nowhere near a checkout. |
qu_figure |
Runs Qu that draws and returns the figure as SVG. The axis labels, tick values and legend text come back as text, so a model can verify a plot contains what it intended rather than hoping. |
qu_run_file |
Runs a .qu file from disk, with arguments reaching the
script as argv(). |
The documentation is also exposed as MCP resources under
qu://docs/…, for clients that browse resources rather than call
tools.
A model will write while true sooner or later. The
server is built so that this is uninteresting when it happens.
Isolated
Evaluating in-process would save a few milliseconds and give up the three
properties that matter: a hard wall-clock limit, a hard memory limit, and
surviving a script that aborts. qu run already implements all
three as flags, so the server spends a process and gets them.
Sandboxed by default
Calls run under --sandbox, which denies the network,
process-spawning and file-writing builtins at the moment they are called —
not by static analysis, which can be talked around.
Bounded
Both adjustable. A run that exceeds either is killed with a message naming which limit it hit, so the model learns something rather than waiting.
Figures come back without lifting the sandbox: qu_figure is
written by the CLI rather than by the script, so a sandboxed script that never
calls savefig still produces one.
| Flag | Effect |
|---|---|
| --allow-write | Lifts the sandbox. Needed only if you want scripts to save their own files. |
| --timeout <s> | Wall-clock limit per call. Default 30. |
| --memory <MB> | Memory limit per call. Default 2048. |
| --root <dir> | Confines qu_run_file to one directory. A path outside it is
refused by name rather than clamped — silently running a different file than
the one asked for is worse than saying no. |
qu mcp --root ./analysis --timeout 60 --allow-write
The server hands these to the client during initialize,
so a model has them before it writes a line. They are not the interesting parts of
the language — they are the places Qu differs from what you would assume from
Python or MATLAB, which is a different list.
0 to 9 is ten
values, and so is a slice: v[0:4] is five elements,
M[0:1, :] is two rows. v[0:n] is n+1
elements; the first n are v[0:n - 1].v[999].* and ^ are matrix operations.
Element-by-element is .* and .^. On two vectors of
the same length * is an error, not a product.as matrix(r, c) fills by column. This one is
silent: you get a matrix, just not the one you meant.colour= does not quietly do nothing.end if,
end for, end function. Indentation carries no
meaning.Two more worth knowing: assignment inside a function is local unless the
function says global name, and () is the empty list.
Three files, in the order they are worth reading.
The important one
Every place Qu differs from what a model would assume, each with the error message the wrong assumption produces. It is the set of mistakes actually made porting scientific code to Qu, in the order they cost time.
Authoritative
Generated from the interpreter's own table. If a name is not there, the engine does not have it — whatever any other document says.
/llms.txt
Follows the llms.txt convention: a short description and an
ordered set of links, so a model can find the right document without crawling
the site.
These were decided for people. They happen to matter more for a model, which cannot look at a figure and notice it is wrong.
No silent drop
Tracked from the code itself rather than a table, so it cannot drift. A guessed argument name is refused immediately, not three figures later.
No spooky action
A function cannot write to its caller's variables. Generated helper code cannot corrupt the surrounding script by reusing a common name.
No hallucinated API
The list of functions comes from the interpreter's own table, and
available("part") searches it from inside a running program.
| Assumption | What Qu does | What you see |
|---|---|---|
| Ranges are half-open | 0 to 9 is ten values, both ends |
index 100000 is outside 0..99999 |
| Slices are half-open | they include both ends too | no error — one element too many |
as matrix fills by rows | it fills by columns | no error — a different matrix |
* is elementwise | * is matmul,
.* is elementwise | no error — a blank field |
| An unknown keyword is ignored | it is an error | unknown keyword argument `colour=` |
"\tau" is a backslash | it is a tab | a label reading au |
Three of those six produce no error at all. That is the argument for running the code rather than reading it.