12. Tools and agents
What this is for
An agent writes the code now. Two of this language’s commands exist because of that, and they are the two things in Burxt no other language can currently do.
The first is about calling in. An MCP tool ships a JSON Schema describing what a caller may pass it. The function behind that tool also checks what it was passed. Those are one fact written twice, and everywhere in the industry they drift:
schema says: { "client_id": { "type": "integer", "minimum": 1 } }
handler says: if client_id <= 0 { ... } # someone remembered
handler says: # ...or nobody did
The schema is the copy that rots. It keeps a bound the code relaxed a year ago, or says a field is optional after the code started requiring it. The client sends a request that is valid by the schema, the tool refuses it, and the failure arrives as a 500 rather than as a validation message — which is the worst possible place to learn about it, because the schema was the thing meant to prevent exactly that.
The second is about changing out. An agent could not satisfy a rule, so it deleted the rule. One line, in a diff of forty. Every test still passes — more of them pass than before, because whatever was failing was failing on purpose. In every other language an assertion in a body is just another line in a body, and nothing in that diff looks different from any other deleted line.
That is the single most dangerous change anyone can make to a program.
Think of a sign printed by the machine
A parking meter has a sign saying which coins it takes, and a slot that actually takes them.
On most meters a person typed the sign. It was right the day it went up. Then the mechanism was serviced, and now the sign says one thing and the slot does another — and you find out standing in the rain with a coin that will not go in.
The other kind of meter prints its sign from the mechanism. Not carefully kept in step with it: printed from it. There is no second thing to update, so there is nothing to forget.
burxt mcp-schema prints the second from the first, so forgetting to update it is
not a thing you can do — there is no second artifact to forget.A step closer
The reason no other language can do this is not tooling. It is that the precondition lives in the signature.
function line_total(unit: Decimal<2> [> $0.00], quantity: Int [> 0, <= 100000]) -> Decimal<2> {
return unit * quantity;
}
[> $0.00] and [> 0, <= 100000] are contracts — the bracket spelling from
contracts. They are checked at run time and they are part of the declaration, so
a tool that reads only declarations can see them.
In Python or TypeScript the same information is a decorator, or a separate schema object, or a validation library’s call inside the body. Keeping those in step with the checks is a code review — which is precisely the review this language exists to remove.
And the clauses are read structurally, from the parsed condition, not from the text. So the
bracket form and a written requires unit > $0.00 produce identical schema. That is what makes them
the same sentence rather than two spellings that happen to agree this week.
In code
Ask the compiler for the manifest:
burxt mcp-schema examples/mcp/tools.bx
and the schema comes back derived from those clauses — [> $0.00] became exclusiveMinimum,
[<= 100000] became maximum:
{"name":"line_total","inputSchema":{"type":"object","properties":{
"unit": {"type":"string","description":"Decimal<2>","exclusiveMinimum":"0.00"},
"quantity": {"type":"integer","description":"Int","exclusiveMinimum":"0","maximum":"100000"}},
"required":["unit","quantity"]}}
Money crosses as a quoted string, throughout. A JSON number reaches a JavaScript consumer as a
double and loses the cent; a string reaches every consumer intact. That is the same wall as scaled
puts at the C boundary and the same one lib/json.bx puts at the wire — three edges,
one idea.
The other command: what a change did to what a program promises
burxt review compares two versions of a program and answers one question: did any promise get
weaker? Not what changed in the text — what changed in what the code guarantees.
burxt review before.bx after.bx
Given an Account that lost a precondition and had a private field opened up:
WEAKENED Account.balance no longer `private` — anything may now read it
WEAKENED Account.withdrawn lost `requires amount <= self.balance`
2 weakened promise(s). A weakened contract is the one change that passes every test — the tests were failing BECAUSE of it.
It exits non-zero when a promise gets weaker, so it works as a gate rather than as a report. Put it in CI and nothing can quietly promise less than it did yesterday — not a deleted precondition, not a function that started reaching the network, not a field that stopped being private.
Why it is built this way
Both commands work for one reason, and it is the reason for every other decision in this language: everything that matters is in the signature.
An agent reasons one function at a time. You scan. Neither of you has the whole program in view. So a tool that reads only declarations can still answer the only question a review is really asking — and that is only true because the declarations were made load-bearing on purpose.
That gives the schema a property a hand-written one cannot have. It is not checked against the implementation, and it is not generated from a comment. It is derived from the same clause the compiler enforces inside the body, so the agent calling the tool is bounded by the same rule the function is, checked at both ends.
The doubled validation in examples/mcp/server.bx is deliberate for a related reason. The server
checks each argument and answers -32602 before calling a tool; the tool also carries the
contract, which aborts the process if violated. A server must not die on a bad request, so the polite
check has to exist — and if the two ever disagreed, the contract would take the process down
loudly rather than letting a bad value through quietly.
What it costs
This is the honest part, and it is the most useful part of the page.
A clause mcp-schema cannot express is skipped and reported — never guessed at. JSON Schema has
no way to say that one parameter relates to another, so this:
function withdraw(balance: Decimal<2>, amount: Decimal<2> [<= balance]) -> Decimal<2> {
return balance - amount;
}
produces schema for balance and amount with no bound on amount at all, and says so on
stderr:
note: 1 precondition(s) could not be expressed as JSON Schema and were left out. A clause relating two parameters — `requires amount <= balance` — has no key in JSON Schema, and the function still enforces it.
Emitting something approximate there would be the drift this tool exists to remove. So the count goes to stderr, and a schema that covers less than the function does says so out loud. The function still enforces it — the caller simply learns about it as a refusal rather than as a validation message.
The MCP server is an example, not a framework. examples/mcp/server.bx handles initialize,
tools/list and tools/call. No resources, no prompts, no notifications, no batching. Enough to
be a real server and small enough to read in one sitting.
burxt review reads promises, not behaviour. It will tell you a precondition disappeared. It will
not tell you the body started computing the wrong total — that is what tests are for. The two are
complementary and neither replaces the other.
Both are single-file today. They read one program and its use graph, not a package.
When you reach for it
| Situation | Reach for |
|---|---|
| exposing functions to an agent as MCP tools | burxt mcp-schema, and put the bounds in the signature so there is nothing else to write |
| a CI gate on what the code guarantees | burxt review old.bx new.bx — non-zero means a promise got weaker |
| reviewing an agent’s diff by hand | read the declarations; that is where the changes that matter are |
| a tool that returns money | Decimal<2> in, a quoted string out — never a JSON number |
| a bound that relates two parameters | write it anyway. The function enforces it; the schema will tell you it could not carry it |
The one habit worth forming: write the bound on the value, in the signature. Everything on this page is downstream of that, and it costs one bracket.
Examples
A real exchange with the server in examples/mcp/, recorded by building it and piping requests in.
Two of them succeed and two are refused, and the refusals are the interesting half.
{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"line_total","arguments":{"unit":"19.99","quantity":3}}}
{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"tax_on","arguments":{"subtotal":"59.97","rate":"0.0825"}}}
{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"line_total","arguments":{"unit":"0.00","quantity":3}}}
{"jsonrpc":"2.0","id":4,"method":"tools/call","params":{"name":"line_total","arguments":{"unit":"19.999","quantity":1}}}
{"jsonrpc":"2.0","id":1,"result":{"content":[{"type":"text","text":"59.97"}]}}
{"jsonrpc":"2.0","id":2,"result":{"content":[{"type":"text","text":"4.95"}]}}
{"jsonrpc":"2.0","id":3,"error":{"code":-32602,"message":"`unit` must be greater than 0.00"}}
{"jsonrpc":"2.0","id":4,"error":{"code":-32602,"message":"`unit` must be an exact amount, as digits"}}
Four things to notice.
19.99 × 3 is 59.97, not 59.96999999999999. The money went out and came back with all its
digits, as a string, because that is what a Decimal is.
4.95 is a rounding that named itself. tax_on returns
Decimal<2, RoundHalfEven>, so an agent reading the schema can see how the half-cent goes.
Request 3 violated a precondition and got a JSON-RPC error — and the process survived to answer request 4. That is the doubled check doing its job.
Request 4 sent 19.999 for a Decimal<2> and was refused, not rounded. The caller sent a third
decimal place for a reason, and no default here can know what it was. This is the case that makes the
whole design worth it: rounding it silently would be the one behaviour that produces a plausible
wrong number.
Build it yourself:
burxt build examples/mcp/server.bx -o /tmp/mcp
echo '{"jsonrpc":"2.0","id":1,"method":"tools/list"}' | /tmp/mcp
And there is nowhere for the schema to have drifted, because there is nowhere else for it to live.
the_mcp_schema_follows_the_contracts in tests/runner.rs proves that the only way it can:
it tightens a clause and watches the schema move. A test that compared the output to a recorded
string would pass forever while the derivation quietly stopped reading the clauses at all.
Next
Practices — where a contract belongs and where it does not, and the traps this project has already paid for. It is the shortest page here and the one that saves the most time.
Then two places to go from there.
The reference has every keyword, builtin, command and standard-library function, with a search box — and it is generated by reading the compiler, so it cannot fall behind the language.
The examples page has whole programs, including the point-of-sale till written four times: once in Burxt, and once each in PHP, Python and Rust. Reading those side by side is the shortest version of everything above.