amcli mcp serves AngouriMath to an LLM agent over the Model Context Protocol:
newline-delimited JSON-RPC 2.0 on stdin and stdout, protocol revision 2024-11-05.
The point is verification rather than calculation. A model is confident it can do algebra, so a tool that only offers to do it goes unused; a tool that checks the model's work gets called. So every integral is checked by differentiating it back, a decline is reported as a decline rather than dressed up as an answer, and every response echoes what was parsed.
amcli is the terminal's dotnet tool:
dotnet tool install --global AngouriMath.Terminal
amcli mcp --selftestThe selftest checks a dozen identities, Euler's and Machin's among them, and that each library defect the server's documents describe still reproduces. A defect that no longer reproduces is reported as drift, since the documents then need editing.
Register it with a client by naming amcli and the argument mcp. For Claude Code:
claude mcp add angourimath --scope user -- amcli mcpFor Claude Desktop, in claude_desktop_config.json, and the same shape for any other client:
{
"mcpServers": {
"angourimath": { "command": "amcli", "args": ["mcp"] }
}
}There is no network access, no file access, no configuration and no secret. Every tool is
annotated readOnlyHint and openWorldHint: false, so a client can approve calls without asking,
which matters: a maths tool that costs a click per call does not get used.
To check the protocol without a client:
echo '{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{}}' | amcli mcp| Tool | |
|---|---|
am_parse |
the parse as understood, its LaTeX, its free variables, and warnings |
am_simplify |
alternatives: true returns several candidate forms |
am_solve |
a list of constraints, combined with and, so ['x^2 = 4', 'x > 0'] narrows to 2 |
am_differentiate |
any order |
am_integrate |
always verified by differentiating back. With from and to, a declined integral still gets a numeric value for that interval, to the digits two step counts agree on |
am_limit |
one-sided with side; tells "no limit" from failure |
am_evaluate |
the exact form and a decimal, after optional substitutions |
am_verify_equal |
did your own algebra change the meaning? Names the difference |
am_check_steps |
checks a chain of working and names the step that broke |
am_truth_table |
the table and the satisfying assignments |
am_solve_system |
takes x + y = 3 as well as x + y - 3 |
am_domain_check |
domain conditions, structural hazards, and where it stops being real |
am_represent |
encodings: bases 2–36, Q-format fixed point, IEEE 754 bits, polar form |
am_matrix |
determinant, inverse, transpose, rank, RREF, trace, multiply, tensor product, power |
am_eigenvalues |
exact, via the characteristic polynomial; symbolic entries allowed |
am_compare_numeric |
worst and RMS error of an approximation over an interval, and where |
am_substitute |
plug in without evaluating: the shape, not a number |
am_expand, am_factor |
brackets out, or back into a product |
am_series |
Taylor or Maclaurin, to a given degree |
am_number_theory |
factorisation, totient, gcd, divisor count, primality |
am_to_sympy |
a runnable SymPy program, for cross-checking |
Five prompts surface as slash commands in a client: verify-derivation, check-formula,
derive-jacobian, analyse-approximation and solve-with-constraints. A model rarely reaches for
a maths tool on its own; a prompt is the user reaching for it instead, and it costs nothing in
tool-list context.
Three resources: angourimath://syntax, the grammar and its two silent traps;
angourimath://reliability, when to trust a result and when to expect a decline; and
angourimath://curiosities, famous results with the call that reproduces each. Point a model at
the first two at the start of a session.
A preliminary tool works as described, but its arguments and answers may change in a minor release,
without an entry in BREAKING-CHANGES.md. A server offers one only when
it is started with --preliminary:
claude mcp add angourimath --scope user -- amcli mcp --preliminaryIt is marked where a model reads it: its description begins Preliminary:, and each of its answers
carries "preliminary": true. Called on a server started without the flag, it answers failed and
names the flag.
| Tool | |
|---|---|
am_classify |
which field a formula comes from, read off its symbols: a guess from naming conventions, not a computation |
A tool is preliminary while its name is in Tools.Preliminary. Taking it out makes it stable, and
from then on a change to what it answers is a breaking change.
Every response echoes the parse. The parser is permissive in two silent ways, and silence is the dangerous part: a valid parse of a different expression, with a plausible answer.
- A number after a name is an exponent:
x2is x², and2(g+e)3is 2(g+e)³. A variable namedx2ort0is silently raised to a power. - An unknown name before a bracket is a product:
beta(z)isbeta * z.
Both raise a warning, and the parsed field always shows what was understood. Whether a name is a
function is asked of the parser itself, by parsing name(x), so the warning cannot fall behind the
grammar.
The status is explicit: solved, unchanged, declined, suspect, timeout, failed or
conflict. declined means the library has no rule, whether it left the expression unevaluated or
raised NotSufficientlySupportedException. unchanged means no progress, not "already simplest".
A printed NaN is screened. An answer that exists has none in it.
An approximate answer says how approximate it is. The numeric fallback for a declined definite
integral, Entity.DefiniteIntegral, returns the digits its rule knows, and only those are reported,
with the error they leave.
Every call has a budget and its own stack. Each runs under the library's cancellation token on a thread with a 64 MB stack, abandoned rather than killed on timeout, so neither a runaway search nor a deep recursion takes the server down.
- LaTeX is output only. Convert
\frac{a}{b}toa/bbefore calling. am_verify_equaldecides on positive real points, then checks the negatives separately: forsqrt(x^2)againstxit answersequal: truewith a note that the two agree only on the positives. The note is part of the answer.am_solve'ssolutions[]is tidied root by root and its rawresultis not, so the two can read differently. Prefersolutions[].- A nonlinear system can come back with no solution when one exists.
- Requests are served one at a time. Settings scopes follow the call, so this is not needed for correctness; a stdio client sends one request at a time anyway.
dotnet test Sources/Tests/McpUnitTestsThe tests drive the server as a client would. They include the selftest, which fails here on a
library change that fixes a documented defect, and a check that angourimath://syntax names every
function the grammar has. .github/workflows/InteractiveTest.yml runs them.
Each of these looks arbitrary and is not:
- Nothing but JSON-RPC goes to the output. Diagnostics go to the error stream; one stray line corrupts the stream, and the client reports the server as failed.
- Every library call goes through
Guard.Run. It is what keeps a stack overflow or a hang inside one request. - A tool's answer comes from the library's catalogue of operations,
Core/Operations/Catalogue.cs, andTools.Offerssays which entries each tool offers. An entry added there failsCatalogueTestuntil a tool offers it orTools.NotOfferedsays why none does, so the server keeps up with the library by construction. The server's own checks of an answer, such as differentiating an antiderivative back, call the library directly. The catalogue is internal, which is why this assembly is signed with the library's key. - Decline detection reads the raw result, before any
Simplify. An unevaluatedlimit(...)simplifies toNaN, which would turn an honest decline into what looks like a wrong answer. - Simplify before stripping a
providedguard, never after. The guard is what licenses the cancellation: stripprovided not a = 0first anda*(d*a - c*b)/ano longer reduces. - A warning that fires on correct input is worse than none, because it teaches the caller to ignore the channel. Check a new warning against the correct spelling as well as the broken one.
Each tool costs context and dilutes the descriptions a model routes on, so prefer a parameter on an existing tool, then a prompt, and add a tool last. A tool's description is a routing prompt, not documentation: say when to call it, and where a model would wrongly trust itself, say so.
This is an adapter, not a second computer algebra system. Mathematics belongs in the library, where the terminal and the notebooks get it too. Where the server has to work around the library, the workaround links the library's issue in a comment beside it, and the fix for that issue removes it.