The full upstream README, mirrored here for reference. Install config, tool schemas, adoption signals, and an original overview live on the SageMath listing page.
A mathematics Model Context Protocol (MCP) server that gives LLM clients a sandboxed mathematical subset of SageMath --- one of the most comprehensive open-source mathematics systems available. Built on FastMCP 3.x, the server maintains a dedicated SageMath process for each MCP session so variables, functions, and assumptions persist across tool calls. Caller code is deny-by-default: the full breadth of Sage mathematics is reachable, but imports, the external CAS interfaces, and the file/display/persistence primitives are not (see Security Sandbox).
Whether the task is symbolic calculus, number theory, linear algebra, differential equations, plotting, combinatorics, graph theory, group theory, or basic arithmetic, the server provides 40 MCP tools --- the math tools backed by the SageMath engine, plus evaluate_sage_streaming (streaming wrapper) and HTTP /health and /ready endpoints.
| Category | Tools | Backend | Capabilities |
|---|---|---|---|
| Core execution | evaluate_sage, evaluate_sage_streaming | Sage | Run SageMath code (the mathematical subset the sandbox permits) with persistent state, LaTeX output, stdout capture, progress heartbeats, per-call timeouts, and line-by-line streaming |
| Calculus | differentiate_expression, integrate_expression, limit_expression, series_expansion | Sage | Derivatives of any order, indefinite & definite integrals, one-sided limits, Taylor/Laurent series |
| Algebra | solve_equation, simplify_expression, expand_expression, factor_expression, calculate_expression | Sage | Single equations & systems, symbolic simplification, expansion, factoring, numeric evaluation |
| Symbolic sums | symbolic_sum | Sage | Symbolic summation and products (finite and infinite series) |
| Linear algebra | matrix_multiply, matrix_operation | Sage | Matrix products, determinants, inverses, eigenvalues, rank, RREF, transpose |
| Differential equations | solve_ode | Sage | First- and higher-order ODEs via Sage's desolve() |
| Number theory | number_theory_operation | Sage | Primality testing, integer factorization, next prime, GCD, LCM |
| Combinatorics | combinatorics_operation | Sage | Binomial, permutations, combinations, partitions, factorial, Catalan, Fibonacci, Bell numbers |
| Graph theory | graph_operation | Sage | Named graphs including parameterised constructors (CompleteGraph(4)) and adjacency dicts; chromatic number, connectivity, planarity, diameter, shortest path |
| Group theory | group_operation | Sage | Symmetric, dihedral, cyclic, alternating groups; order, abelian/cyclic test, center, exponent |
| Elliptic curves | elliptic_curve_operation | Sage | Rank, torsion, discriminant, j-invariant, conductor, generators |
| Coding theory | coding_theory_operation | Sage | Hamming and generalized Reed-Solomon codes; length, dimension, minimum distance, generator matrix, rate |
| Polynomial rings | polynomial_ring_operation | Sage | Groebner bases, ideal dimension/variety, reduction, Groebner test |
| Boolean algebra | boolean_algebra_operation | Sage | Boolean polynomial ring, addressed as x, y, z or x0, x1, x2; evaluate, variables, degree, zero/one test |
| Geometry | geometry_operation | Sage | Distance, polygon area, polytope volume, convex hull, compactness via Polyhedron |
| Statistics | statistics_summary | Sage | Mean, median, population & sample variance/std dev, min, max |
| Probability | distribution_operation | Sage | Normal, exponential, Poisson, chi-squared, Student-t, uniform, beta, gamma; PDF, CDF, quantile, analytic mean/variance, sampling |
| Visualization | plot_expression, plot3d_expression, plot_multi_expression | Sage | 2D plots, 3D surface plots, multi-function overlays, returned as rendered images (PNG or SVG) the client displays |
| Numeric methods | find_root | Sage | Numeric root-finding in an interval via Sage's find_root(), from an expression or an equation |
| Verification | verify_claim | Sage | Independently re-check a stated claim through a proof ladder; answers proved, refuted, supported or undecided, always with its evidence |
| Vector calculus | vector_calculus_operation | Sage | Gradient, divergence, curl, Laplacian on scalar/vector fields |
| Session control | reset_sage_session, interrupt_sage_session, cancel_sage_session | Worker | Clear state, or stop a computation with or without keeping variables |
| Named workspaces | start_sage_session, list_sage_sessions, stop_sage_session | Worker | Several independent variable namespaces per client |
| Diagnostics | check_sage_health, lookup_sage_doc | Worker/Server | MCP-level readiness probe (evaluates 1+1, reports latency); doc links for a Sage name plus whether this server offers it to caller code |
| Infrastructure | /health and /ready endpoints, 3 MCP resources | Server | Liveness (process up) and readiness (evaluates 1+1 on the backend), session snapshots, aggregated metrics, documentation links |
Request flow: MCP client → a tool in tools/ → SageSessionManager.get_or_create() → SageSession.evaluate() → JSON request to _sage_worker.py subprocess → AST validation → exec() in persistent namespace → JSON response back.
Key design decisions:
If the command is not on your PATH, run python -m sagemath_mcp.server --help.
The server needs a SageMath runtime. Instead of the ~3 GB sagemath/sagemath
Docker image or a local Sage build, you can install passagemath
— a pip-installable, modularized fork of SageMath — as an extra. This path is
experimental (see the status caveats below):
from sage.all import * and the worker run unmodified on it. The server detects
the runtime at import (importlib.metadata) and loads the matching generated
security artifact set, so the application-level deny-by-default policy — the
allowlist, the import ban, the AST rules — is equivalent on both runtimes.
That equivalence is not a substitute for Docker's containment: the AST policy is the boundary inside the process, but a pip-installed passagemath runs with your user's privileges and file access, whereas the container image also gives you OS-level isolation (non-root user, read-only mounts, dropped capabilities). For an untrusted or multi-tenant deployment, run the container regardless of runtime; the passagemath extra trades that outer boundary for install convenience.
The version is pinned exactly (passagemath-standard==10.8.9) rather than
tracking latest, because passagemath's own release QA has shipped broken
backends — see docs/passagemath_evaluation.md
for the full measurements. Linux and macOS wheels only; native Windows does not
ship the pari/singular/maxima wheels this server's tools need.
Experimental status. There is no passagemath CI lane yet, and the full integration suite and doctest-corpus sweep against the pin are still outstanding (TODO), so treat correctness on passagemath as validated by spot checks, not by the same continuous gate the monolithic runtime has.
This container exists to run the test suite against a real Sage: it mounts your checkout writably and skips the read-only hardening the runtime paths apply, so treat it as a development fixture, not a deployment. For running the server, use the hardened Docker image or Compose paths below. To get a ready-to-use Sage runtime for the tests:
On Windows PowerShell:
Build a ready-to-run container with the MCP server baked in. The image's default command already serves streamable HTTP on the container side; the run flags below are the same hardening Docker Compose applies (read-only root, dropped capabilities, fork/memory ceilings), and the port is published on the loopback interface deliberately — this server executes code and authenticates nobody:
Prefer docker compose up --build (below) — it applies the same hardening from
one reviewed file. If you override the image's command, keep
--host 0.0.0.0: the container-side binding is what makes the published
loopback port reachable at all.
Released images are published to ghcr.io/xbp-europe/sagemath-mcp and signed with Cosign.
Verify a downloaded artifact with:
The compose service exposes port 8314 on both host and container and mounts the repository at /workspace. Containers run as the non-root sage user (UID/GID 1001) to match the base image. Tweak runtime settings by editing the environment block (for example, increase SAGEMATH_MCP_EVAL_TIMEOUT or adjust SAGEMATH_MCP_MAX_STDOUT) before launch.
evaluate_sage --- Open-Ended SageMath ExecutionExecutes SageMath code — the mathematical subset the sandbox permits (see Security Sandbox) — inside a persistent worker process. Variables, functions, classes, and assumptions defined in one call survive into subsequent calls within the same MCP session.
Its own tool description calls it a LAST RESORT, and for a single self-contained calculation a specialized tool is better: it validates arguments and returns a typed result. But that steer has one important exception. The specialized tools evaluate their input in a fresh namespace and cannot see variables you assigned with evaluate_sage — so any workflow that builds an object once and then explores it (a graph and its invariants, a number field, a polynomial ideal, a matrix decomposition) belongs in evaluate_sage, across as many calls as it takes. Persistent state is the reason to reach for it, not a reason to avoid it.
| Parameter | Type | Default | Description |
|---|---|---|---|
code | string | required | SageMath code to execute. Multi-line strings are supported. |
want_latex | bool | false | When true, the server generates a LaTeX representation of the final expression result (if one exists) via Sage's latex() function. Returned in the latex field. |
capture_stdout | bool | true | When true, any output from print() statements is captured and returned in the stdout field. Set to false for faster execution when stdout is not needed. |
timeout | float | null | Override the per-evaluation timeout in seconds. If omitted, the global default (SAGEMATH_MCP_EVAL_TIMEOUT, 30 s) applies. Must be > 0. |
Returns an EvaluateResult object:
| Field | Type | Description |
|---|---|---|
result_type | "expression" or "statement" | "expression" when the code ends with an expression whose value is captured; "statement" when it ends with an assignment or side effect. |
result | string or null | The repr() of the final expression value, or null for statement-type code. |
latex | string or null | LaTeX representation of the result (only when want_latex=true and the result is non-null). |
stdout | string | Captured stdout output (empty string if nothing was printed or capture_stdout=false). Truncated to SAGEMATH_MCP_MAX_STDOUT characters. |
elapsed_ms | float | Wall-clock execution time in milliseconds. |
Behavior details:
TimeoutError is raised. All session state from prior calls is lost.from sage.all import * by default) failed when the worker launched, every subsequent evaluate_sage call returns a clear StartupError instead of a confusing NameError.x, y, z and t are predefined, and evaluate_sage auto-declares any other symbol-shaped name (w, x_2, alpha) as a symbol the way SageMath's SR does — so w^2 + 1 just works. The shape is narrow and typo-guarded: a multi-letter name like sinn stays an error, and a name you assigned earlier keeps its value.Domain-specific examples (these are included in the tool description LLMs see):
| Domain | Example Sage code |
|---|---|
| Combinatorics | binomial(10, 3), Permutations(4).cardinality(), Combinations([1,2,3,4], 2).list() |
| Graph theory | G = graphs.PetersenGraph(); G.chromatic_number() |
| Number theory | prime_range(100), euler_phi(60), continued_fraction(pi, nterms=10) |
| Geometry | polytopes.cube().volume(), EllipticCurve([0,0,1,-1,0]).rank() |
| Probability | RealDistribution('gaussian', 1).cum_distribution_function(1.96) |
| Group theory | SymmetricGroup(5).order(), AlternatingGroup(4).is_abelian() |
| Polynomial rings | R.<a,b> = PolynomialRing(QQ); (a+b)^3 |
| Coding theory | codes.HammingCode(GF(2), 3).minimum_distance() |
Stateful multi-step workflow:
differentiate_expressionCompute the symbolic derivative of an expression. Calls Sage's diff(expr, var, order) internally.
| Parameter | Type | Default | Description |
|---|---|---|---|
expression | string | required | The expression to differentiate (e.g. "sin(x)*e^x", "x^3 + 2*x"). |
variable | string | "x" | The variable to differentiate with respect to. |
order | int (>= 1) | 1 | Differentiation order. 1 = first derivative, 2 = second derivative, etc. |
Returns: {"derivative": "...", "order": N}
integrate_expressionCompute indefinite or definite integrals. Calls Sage's integrate() function.
| Parameter | Type | Default | Description |
|---|---|---|---|
expression | string | required | The expression to integrate. |
variable | string | "x" | The integration variable. |
lower_bound | string or null | null | Lower bound for definite integrals. Accepts symbolic values like "0", "-oo" (negative infinity), or expressions like "-pi". |
upper_bound | string or null | null | Upper bound for definite integrals. Accepts "1", "oo" (infinity), "pi/2", etc. |
Bounds may also be free symbols, so upper_bound="a" integrates to a symbolic limit.
Names Sage already defines keep their meaning: e, pi and oo are the constants,
not new variables.
Both lower_bound and upper_bound must be provided together for a definite integral, or both omitted for an indefinite integral. Providing only one raises an error.
Returns: {"integral": "...", "definite": true/false}
limit_expressionCompute the limit of an expression as a variable approaches a point. Calls Sage's limit() function.
| Parameter | Type | Default | Description |
|---|---|---|---|
expression | string | required | The expression to take the limit of. |
variable | string | "x" | The variable approaching the point. |
point | string | "0" | The point to approach. Use "oo" for positive infinity, "-oo" for negative infinity, or any symbolic expression. |
direction | string or null | null | One-sided limit direction: "plus" (approach from the right, x -> a+), "minus" (approach from the left, x -> a-), or null for both sides. |
Returns: {"limit": "..."}
series_expansionCompute a Taylor or Laurent series expansion around a point. Calls Sage's .series() method.
| Parameter | Type | Default | Description |
|---|---|---|---|
expression | string | required | The expression to expand. |
variable | string | "x" | The expansion variable. |
point | string | "0" | Center of the expansion (Maclaurin series when "0"). |
order | int (>= 1) | 6 | Number of terms in the expansion. |
Returns: {"series": "...", "point": "...", "order": N}
solve_equationSolve a single equation or a system of simultaneous equations. Calls Sage's solve() function. Equations are parsed by splitting on =: the string "x^2 - 1 = 0" becomes the Sage equation x^2 - 1 == 0.
| Parameter | Type | Default | Description |
|---|---|---|---|
equation | string or list[string] | required | A single equation string (e.g. "x^2 - 1 = 0") or a list of equations for systems (e.g. ["x + y = 3", "x - y = 1"]). If no = is present, the expression is solved as expr = 0. |
variable | string or list[string] | "x" | Variable(s) to solve for. Use a list for systems (e.g. ["x", "y"]). |
Returns: {"solutions": [...]}
simplify_expressionApply Sage's simplify() function to reduce a symbolic expression to a simpler form.
| Parameter | Type | Default | Description |
|---|---|---|---|
expression | string | required | The expression to simplify. |
Returns: {"simplified": "..."}
expand_expressionExpand products, powers, and trigonometric/logarithmic identities using Sage's expand().
| Parameter | Type | Default | Description |
|---|---|---|---|
expression | string | required | The expression to expand. |
Returns: {"expanded": "..."}
factor_expressionFactor a symbolic expression or integer using Sage's factor().
| Parameter | Type | Default | Description |
|---|---|---|---|
expression | string | required | The expression to factor. Can be a polynomial (e.g. "x^2 - 1") or an integer (e.g. "60"). |
Returns: {"factored": "..."}
calculate_expressionEvaluate a symbolic expression and return both its string representation and numeric value (when possible). Uses Sage's sage_eval() internally with pre-declared variables x, y, z, t.
| Parameter | Type | Default | Description |
|---|---|---|---|
expression | string | required | The expression to evaluate. |
Returns: {"string": "...", "numeric": float} --- the numeric field is omitted when the expression cannot be converted to a float.
matrix_multiplyMultiply two matrices over the Symbolic Ring (SR). Input matrices are nested lists of numbers.
| Parameter | Type | Default | Description |
|---|---|---|---|
matrix_a | list[list[float]] | required | Left matrix (rows of numbers). |
matrix_b | list[list[float]] | required | Right matrix (rows of numbers). |
Returns: {"product": [[...], ...]} --- entries are floats when real, strings otherwise.
matrix_operationPerform a single matrix operation. Supports six operations on matrices over the Symbolic Ring.
| Parameter | Type | Default | Description |
|---|---|---|---|
matrix | list[list[float]] | required | Input matrix as nested list of numbers. |
operation | string | required | One of: "determinant", "inverse", "eigenvalues", "rank", "rref", "transpose". |
Returns: {"operation": "...", "result": ...} --- result type varies by operation:
| Operation | Result type | Description |
|---|---|---|
determinant | float or string | Scalar determinant value. |
inverse | list[list[float]] | The inverse matrix (error if singular). |
eigenvalues | list[float] | List of eigenvalues (with multiplicity). |
rank | int | Matrix rank. |
rref | list[list[float]] | Reduced row echelon form. |
transpose | list[list[float]] | Transposed matrix. |
solve_odeSolve an ordinary differential equation using Sage's desolve(). The equation is specified as a string using Sage's diff() notation. The solver returns a general solution with arbitrary constants (_C, _K1, _K2, etc.).
| Parameter | Type | Default | Description |
|---|---|---|---|
equation | string | required | The ODE as a string. Use diff(y(x),x) for y', diff(y(x),x,x) for y'', etc. Include = 0 or = rhs to specify the equation. |
function | string | "y" | Name of the dependent function being solved for. |
variable | string | "x" | Name of the independent variable. |
The dependent function may be written either applied (diff(y(x), x) + y(x)) or bare
(diff(y, x) + y). Both describe the same equation and return identical solutions.
Returns: {"solution": "..."}
number_theory_operationPerform common number-theoretic operations using Sage's built-in functions.
| Parameter | Type | Default | Description |
|---|---|---|---|
operation | string | required | One of: "is_prime", "factor_integer", "next_prime", "gcd", "lcm". |
a | int | required | Primary integer argument. |
b | int or null | null | Second integer. Required for gcd and lcm; ignored otherwise. |
Returns: {"operation": "...", "result": ...} --- result type varies:
| Operation | Result type | Sage function called | Description |
|---|---|---|---|
is_prime | bool | is_prime(a) | Whether a is a prime number. |
factor_integer | string | factor(a) | Prime factorization as a human-readable string (e.g. "2^3 * 3 * 5"). |
next_prime | int | next_prime(a) | The smallest prime greater than a. |
gcd | int | gcd(a, b) | Greatest common divisor of a and b. |
lcm | int | lcm(a, b) | Least common multiple of a and b. |
statistics_summaryCompute descriptive statistics for a numeric dataset using Sage's mean() and sqrt() functions.
| Parameter | Type | Default | Description |
|---|---|---|---|
data | list[float] | required | List of numeric values. Must contain at least 2 elements for variance/std dev. |
Returns: a dictionary with all of:
| Field | Description |
|---|---|
mean | Arithmetic mean. |
median | Median value. |
population_variance | Population variance (divides by N). |
sample_variance | Sample variance (divides by N-1). |
population_std_dev | Population standard deviation. |
sample_std_dev | Sample standard deviation. |
min | Minimum value. |
max | Maximum value. |
plot_expressionRender a 2D plot of an expression and return it as MCP image content (PNG by default, or SVG via image_format) the client displays inline. Calls Sage's plot(), renders to an in-memory buffer at a bounded size, and returns it as an image block rather than a base64 string.
| Parameter | Type | Default | Description |
|---|---|---|---|
expression | string | required | The expression to plot. |
variable | string | "x" | The plot variable. |
range_min | float | -10.0 | Lower bound of the plot range. |
range_max | float | 10.0 | Upper bound of the plot range. |
Returns: {"image_base64": "...", "format": "png"}
The returned base64 string can be rendered directly in any client that supports inline images (e.g., via an <img> tag or Markdown ).
reset_sage_sessionClear all variables, functions, and definitions in the current session. The underlying worker process continues running (fast). Equivalent to restarting a fresh Sage shell.
Returns: {"message": "Session cleared"}
evaluate_sage runs your code through Sage's preparser, exactly as the Sage
REPL does. 2^3 is 8, not 1; integer literals are Sage Integers; generator
syntax such as K.<a> = NumberField(x^3 - 2) parses; and x, y, z and t
are predefined. Use ^^ for XOR, as in Sage.
Sage's own REPL predefines x alone. This server predefines four, because the
specialised tools have always declared x, y, z, t in their prelude: with only
x, differentiate_expression("x^2*y^3") worked while the identical
mathematics through evaluate_sage failed.
In evaluate_sage, any other symbol needs var('w'), and the error message
says so. That is exactly SageMath's own rule: w + 1 typed as code is a
NameError there too.
The specialised tools declare a symbol on sight, because they take an
expression as a string and that is SageMath's other rule — SR("a*b + a")
creates a and b. So simplify_expression("w^2 + w^2") answers 2*w^2, and
expand_expression("(θ + φ)^2") answers in the letters you wrote.
Narrower than SR in the way that matters: SR invents any identifier, so
SR("sinn(x)") returns sinn(x) and a typo becomes a silent wrong answer.
Only symbol-shaped names are declared — a letter with an optional index (a,
w, x_2), a spelled-out Greek name (alpha), or a Greek letter (α, Ω) —
so sinn, foobar and pi2 are still errors. Names SageMath already defines
are never shadowed: e stays Euler's number, I the imaginary unit, and
gamma, zeta, π, σ, Γ and ψ stay the functions they are.
Only caller code is preparsed. The specialised tools build plain Python around
sage_eval, and preparsing those templates would change what they mean.
Mathematics produces integers that JSON numbers cannot carry. Above 2^53 a JSON
number stops being exact, and JavaScript-based MCP clients parse every number as
an IEEE double --- so bell(30) arrived in one CLI as 846749014511809388871680
instead of 846749014511809332450147. Nothing errored; the number was simply
wrong, which is the worst way for it to fail.
Both directions therefore speak decimal strings past that boundary:
Number.MAX_SAFE_INTEGER (2^53 - 1), not 2^53:
2^53 + 1 rounds to exactly 2^53, so those two arrive indistinguishable and
neither can be trusted.interrupt_sage_sessionStop a running computation while keeping every variable defined so far. The worker is signalled, abandons the current statement, and stays alive with its namespace intact. The interrupted call returns an Interrupted error.
Prefer this over cancel_sage_session — cancelling discards state that may have been expensive to build.
| Parameter | Type | Default | Description |
|---|---|---|---|
session | string | "default" | Named workspace to interrupt. |
Returns: {"message": "Interrupted session 'default'; state preserved"}
Interrupting when nothing is running is reported, not an error, and no signal is
sent: {"message": "No running computation in session 'default'"}. That matters
beyond tidiness — an idle worker is blocked reading its input, where a SIGINT has
no computation to abort, and signalling it anyway left real Sage workers unable
to answer the next request. POSIX only.
cancel_sage_sessionAbort any in-flight computation by killing the worker process and starting a new one. All session state is lost — reach for this only when the worker is wedged badly enough that interrupting does not help.
Returns: {"message": "Session cancelled and restarted"}
start_sage_session, list_sage_sessions, stop_sage_sessionOne client can hold several independent workspaces. Variables defined in one are invisible to the others, so a long-running exploration and a quick scratch calculation need not collide.
Portable workspace handles. start_sage_session also returns a
workspace_token — a server-issued, unguessable handle that addresses that one
workspace:
A plain name is scoped to your current MCP session, so it is lost if the
transport hands you a new session id (a reconnect, or a transport that rotates
the id per call). A handle is not: passed as the session argument it reaches
the same workspace regardless of the transport id, which is what keeps state
across a reconnect. It is a bearer credential, not authentication — it
identifies no one, and anyone who holds it can reach that workspace — so treat
it as a secret. An unknown or revoked handle is refused, never silently turned
into a fresh workspace, and stopping a workspace invalidates its handles. The
handle keeps its workspace only while that worker is alive (a server restart or
an idle cull ends it); it is not a cross-restart recovery token.
check_sage_healthThe MCP-level readiness probe, for stdio clients that cannot reach the HTTP
/ready route. It exercises the real path -- worker spawn, protocol round
trip, evaluation of 1+1 -- and reports failure in its result rather than
erroring, so an agent can always call it before committing to a workflow.
Returns: {"ok": true, "backend": "sagemath", "elapsed_ms": 412.3, "session": "default"}
lookup_sage_docDocumentation links for one SageMath name, plus the half the upstream manual
cannot answer: whether this server offers the name to evaluate_sage caller
code. Caller code is deny-by-default, so a name Sage documents may still be
withheld here; saying so up front saves the model a refused evaluation.
Returns: {"symbol": "EllipticCurve", "offered_to_caller_code": true, "links": {...}, "note": "..."}
verify_claimThe checking primitive for the dominant failure mode of models doing
mathematics: confident wrong algebra. The model states a claim -- an equality,
an inequality, anything that evaluates to True/False -- and the server
re-checks it independently through a ladder: Sage's symbolic prover, the exact
difference ((lhs-rhs).simplify_full().is_zero()), exact arithmetic over
QQbar/AA when the claim is constant, then certified interval arithmetic and
numeric sampling over the free variables.
Two rules keep the verdicts honest. The prover returning False means not
proved, never false -- refuted requires an exact decision or an exhibited
counterexample (interval evidence is always a certified enclosure, not a
floating-point comparison). And supported always carries its evidence --
sample count and precision -- never a bare confidence number.
Exactness is never assumed. Decimal literals are read as the exact rationals
they denote -- 0.1 means 1/10, so 0.1 + 0.2 == 0.3 is proved and
1.0 + 1e-20 == 1.0 is refuted, where deciding over 53-bit doubles would answer
both wrongly while claiming exactness. But a comparison whose operands are
genuine machine floats (RR(1), an .n() result, a session value in RR) is
reported as supported "over inexact machine numbers", never as an exact proof
-- RR(1) + RR(1)/10^20 == RR(1) is true only by rounding, and saying proved
there would be the false certainty this tool exists to prevent. And the
session's active assumptions are honored, domain declarations included: under
assume(x, 'integer') a sampled point of 1/2 is not admissible, so it is never
offered as a counterexample to x != 1/2; any verdict that leaned on an
assumption names it in the evidence.
Every tool that runs on a worker accepts the same optional session argument.
Omitting it uses the default workspace, which is the behaviour of every earlier
version.
What session does, precisely. It selects which worker process runs the
call, so a long computation in one workspace can be interrupted or cancelled
without disturbing another. It does not give the specialised tools access to
variables you defined with evaluate_sage: those tools evaluate their input in a
fresh Sage namespace, so calculate_expression("myvar") will not see a myvar
assigned earlier. Use evaluate_sage for anything that has to build on previous
state.
| Resource URI | Scope values | Description |
|---|---|---|
resource://sagemath/session/{scope} | all, or a specific session ID | Returns JSON with: session_id, live (bool), started_at, last_used_at, idle_seconds. |
resource://sagemath/monitoring/{scope} | metrics, all | Returns JSON with the process-wide aggregates only: attempts, successes, failures, security_failures, avg_elapsed_ms, max_elapsed_ms, last_run_at. Per-failure error text and stdout are not exposed here (they are shared process-global state); see the server logs instead. |
resource://sagemath/docs/{scope} | all, reference, tutorial | Returns documentation link objects with URLs to SageMath documentation. |
All code --- whether from evaluate_sage or generated internally by helper tools --- passes through an AST-based security validator before execution.
What this is, and is not. The validator is defence in depth against accidents and casual misuse. It is not a boundary against determined adversarial code, and it should not be the only thing standing between an untrusted caller and your host. The container is the security boundary --- run the server in one, and see Container hardening.
These are removed from the worker namespace as well as rejected by the validator, and by where they come from rather than by name: a list of names cannot keep up with a namespace thousands deep, and
cython(get_remote_file(url))was download, compile and execute in one expression.gp('system("id")')ran a shell command. Neither involved a name any rule mentioned.This section was previously inaccurate: it claimed
subprocess.*,pathlib.*andsocket.*were blocked when none of them were, because a rule required a module and a specific attribute name to match. Seven further bypasses were found and closed at the same time. It was inaccurate a second time, more subtly: the forbidden names were rejected only where they were called, sof = openfollowed byf("/etc/passwd")passed --- through the specialised tools as well asevaluate_sage. Forbidden names are now rejected wherever they are read, and the worker's namespace no longer contains them at all. The table below is covered by a test that fails if the code stops enforcing it, and that test now checks aliases, not just call sites.
The rule that comes first: an allowlist.
Caller code may read a name only if it is one this server offers --- the ~1900
mathematical names SageMath preloads, the safe builtins, and whatever the caller
defines itself (assignments, loop variables, function arguments, var('t'), and
anything created earlier in the same session). Everything else is refused.
That inversion is the point. Seven sandbox bypasses in two days had one shape
between them: a name nobody had thought to forbid --- cython, sh, gp,
get_remote_file, unpickle_global. A denylist over a namespace that size is
always one name behind. It does not retroactively catch something dangerous still
sitting in the namespace, but a helper added by a future SageMath release is
denied until someone looks at it, rather than reachable the day it lands. A test
run weekly, and on every push, fails when the two disagree.
The rules below still apply, and now serve as defence in depth behind it.
What is blocked:
Names in the first three rows are rejected anywhere they are read --- called,
assigned, aliased, defaulted into a lambda, placed in a list, or reached through
an attribute chain --- not only in call position. The last of those matters more
than it sounds: sage is an allowed import root, so
sage.misc.sage_eval.sage_eval("...") reached the same function that a bare
sage_eval could not.
| Category | Details |
|---|---|
| Dangerous builtins | eval(), exec(), compile(), __import__(), open(), input(), globals(), locals(), vars() |
| Attribute indirection | getattr(), setattr(), delattr() --- these defeat every attribute rule by naming the attribute at runtime |
| Runtime string evaluation | sage_eval(), preparse(), sage_input() --- these evaluate a string after the AST has been approved |
| Dunder access | Any __dunder__ name or attribute, which blocks ().__class__.__bases__[0].__subclasses__() and __builtins__ |
| Sage helpers that execute or fetch | cython(), cython_lambda(), fortran() (compile and run code), sh() (runs a shell), get_remote_file() (downloads), loads/dumps/save/db_save (pickle is code execution) |
| External CAS interfaces | gp, maxima, gap, singular, octave, magma, sage0 and everything else sage.interfaces.all exports --- each spawns the real program, and those have shell escapes of their own |
| Names that write, fetch or display | oeis (queries oeis.org), install_doc, show, view, animate, html, latex, search_src, search_doc, reference, Profiler --- each demonstrated a file written, a network request or the installation read. Plot tools are unaffected: they render through .savefig(BytesIO), and LaTeX output imports latex from sage.all directly rather than from the caller namespace |
| Sage loaders | load() and attach() execute whatever path they are given, and load() accepts a URL |
| String-path attribute access | attrgetter, methodcaller, itemgetter, and the operator module that carries them. Every attribute rule here is enforced on the AST, and these take the path as a runtime string the AST never sees: operator.attrgetter("misc.persist.unpickle_global")(sage) returned the real function, which is arbitrary code execution. getattr, setattr and vars were already refused, which left operator as the only way in |
| Forbidden modules | Every attribute of os, sys, subprocess, shutil, socket, pathlib, builtins, operator, warnings, pari, oeis --- at any depth, so sage.misc.temporary_file.os is caught too. pari is the PARI library interface, which the external-CAS scrub missed because it comes from sage.libs.pari; pari('system("id")') ran a shell command |
| Sage sub-packages that execute | cython, persist, remote_file, interfaces, inline_fortran, repl, package, temporary_file, attached_files, explain_pickle, edit_module, dev_tools, trace, sh --- at any depth. Blocked as a path, so sage.misc.trace.trace(...) is refused and A.trace() is not |
| Imports | Refused by default. An import is how you get back a helper the worker removed, and the namespace already has Sage loaded. Two narrow exceptions change nothing reachable: an import of names already offered, and from <module> import * for a curated set of internal SageMath modules whose public names are all ordinary mathematics --- screened clean as a whole and generated into star_exports.py, with any re-exported module object dropped so it cannot become a pivot. Nothing is added to the allowlist |
| Scope manipulation | global and nonlocal statements (configurable) |
| Namespace removal | The worker's __builtins__ omits open, eval, exec, compile, input, breakpoint, globals, locals, vars, memoryview, help, exit and quit outright --- a backstop for spellings the AST pass misses. __import__ deliberately stays, because Sage imports lazily during ordinary mathematics; it is unreachable from caller code, which cannot name any dunder. |
Caller-supplied expressions passed to the specialised tools are validated as
expressions in their own right before they are embedded in generated code.
Without that, calculate_expression("__import__('os').getuid()") reached the
operating system, because the validator saw only a string constant.
What is allowed:
Everything Sage preloads --- which is the whole library. factorial(5),
integrate(sin(x), x), matrix(...), EllipticCurve(...) and the rest need no
import, because the worker starts with from sage.all import * already done.
The import allowlist below applies only to the snippets this server generates.
Caller code gets a much narrower door --- the imports that would change nothing
(a name already offered, or from <curated module> import * expanded to its
screened names), and nothing else --- see the Imports row above:
| Import | Used by |
|---|---|
sage, sage.all | The generated prelude |
base64, io | The plot templates, for in-memory PNG encoding |
math, cmath, statistics | Helper templates |
The server has no authentication. Anyone who can reach the HTTP endpoint can
evaluate code, which is why every default here is loopback: --host defaults to
127.0.0.1, the default transport is stdio, the bundled compose file publishes
to 127.0.0.1:8314, and the Helm service is ClusterIP. Putting it on a network
means putting something that authenticates in front of it.
The validator narrows what caller code can express. The container is what
actually contains it, so docker-compose.yml sets:
| Setting | Why |
|---|---|
read_only: true | The root filesystem is immutable. Sage needs only a writable temp dir and its own dot-directory, supplied as the two tmpfs mounts below; without them it fails outright, which is how they were sized. |
tmpfs: /tmp, /home/sage/.sage | The only writable paths, in memory, capped at 512 MB and 256 MB. |
./:/workspace:ro | The server runs from the package installed in the image; an escaped process should not be able to edit the checkout it reads. |
cap_drop: [ALL] | No Linux capabilities are needed to do mathematics. |
security_opt: [no-new-privileges:true] | Blocks privilege escalation via setuid binaries. |
pids_limit: 256 | A fork bomb cannot exhaust the host. |
mem_limit: 4g | Neither can a runaway computation. |
An escape was measured reading all environment variables, reading the mounted
checkout and opening outbound sockets. If the server is exposed to untrusted
callers, also consider network_mode: none where the workload allows it, and
avoid passing secrets in the environment of this container.
The Helm chart applies runAsNonRoot, allowPrivilegeEscalation: false,
capabilities.drop: [ALL] and readOnlyRootFilesystem: true, with emptyDir
volumes for the same two writable paths and default CPU/memory requests and
limits. It is close but not identical: compose's pids_limit has no direct
chart equivalent (pod PID limits are a kubelet setting), so set one on the node
if you need it.
Enforced limits:
| Limit | Default | Env var |
|---|---|---|
| Max source code length | 131,072 chars | SAGEMATH_MCP_SECURITY_MAX_SOURCE |
| Max AST node count | 50,000 | SAGEMATH_MCP_SECURITY_MAX_AST_NODES |
| Max AST nesting depth | 75 | SAGEMATH_MCP_SECURITY_MAX_AST_DEPTH |
Error handling: When code violates the security policy, the server returns a clear error message identifying the violation (e.g., "Call to forbidden function 'eval' is blocked") and logs a warning. The session remains alive --- subsequent calls can succeed.
Clients connecting through MCP receive the following guidance automatically:
var('x'), f = ...) and reuse them across subsequent tool calls.solve_equation, differentiate_expression, etc.) for structured JSON output. Fall back to evaluate_sage for anything else.timeout parameter.eval/exec, and filesystem/process calls. Prefer Sage primitives; if a violation occurs, rewrite the workflow using supported APIs.Claude Desktop --- add to claude_desktop_config.json:
Claude Code --- add to .mcp.json in the project root:
Codex CLI:
Gemini CLI:
For HTTP transport, expose the endpoint first (sagemath-mcp --transport streamable-http --host 0.0.0.0 --port 8314) and point the client at http://HOST:8314/mcp.
Best for local LLM clients (Claude Desktop, Claude Code, Codex CLI). The client spawns the server as a subprocess and communicates over stdin/stdout.
Best for remote clients, browser-based tools, or shared environments. Supports streaming responses and cancellation.
Exposes http://127.0.0.1:8314/mcp. Runs as non-root sage user (UID/GID 1001). The compose file mounts the repository at /workspace and accepts environment variable overrides for all SAGEMATH_MCP_* settings.
Key values: service.port, env (map of environment overrides), args (CLI arguments), ingress.*. The chart enforces non-root execution (runAsUser/runAsGroup 1000). Review values.yaml for the full set of configurable knobs. The release workflow validates the chart with helm lint and helm template before publishing.
All configuration is done via environment variables. No config files are needed.
| Variable | Description | Default |
|---|---|---|
SAGEMATH_MCP_SAGE_BINARY | Path to the sage executable. | sage |
SAGEMATH_MCP_STARTUP | Sage code executed during session bootstrap. | from sage.all import * |
SAGEMATH_MCP_IDLE_TTL | Seconds of inactivity before a session is culled. | 900 |
SAGEMATH_MCP_EVAL_TIMEOUT | Per-evaluation timeout in seconds. | 30 |
SAGEMATH_MCP_MAX_STDOUT | Maximum characters of stdout returned per call. | 100000 |
SAGEMATH_MCP_MAX_SESSIONS | Ceiling on concurrently live sessions (workers); 0 means unbounded. A new session past the ceiling is refused; existing ones are always reachable. | 128 |
SAGEMATH_MCP_WARM_POOL_SIZE | Spare workers kept warm (Sage preloaded and lazy-init triggered) so a new session's first call is instant instead of paying ~1s; 0 disables it. Filled at startup, topped up in the background, never above SAGEMATH_MCP_MAX_SESSIONS. | 1 |
SAGEMATH_MCP_SHUTDOWN_GRACE | Grace period before a stuck worker is terminated. | 2 |
SAGEMATH_MCP_FORCE_PYTHON_WORKER | Use the pure-Python worker (helpful for tests/CI). | false |
SAGEMATH_MCP_PURE_PYTHON | When set to 1, load math stdlib instead of Sage modules. | unset |
| Variable | Description | Default |
|---|---|---|
SAGEMATH_MCP_SECURITY_ENABLED | Enable/disable AST-based code validation. | true |
SAGEMATH_MCP_SECURITY_MAX_SOURCE | Maximum source length in characters, measured after preparsing. | 131072 |
SAGEMATH_MCP_SECURITY_MAX_AST_NODES | Maximum AST node count allowed. | 50000 |
SAGEMATH_MCP_SECURITY_MAX_AST_DEPTH | Maximum AST depth allowed. | 75 |
SAGEMATH_MCP_SECURITY_ALLOW_IMPORTS | Permit import statements when set to true. | false |
SAGEMATH_MCP_SECURITY_FORBID_GLOBAL | Block global statements when true. | true |
SAGEMATH_MCP_SECURITY_FORBID_NONLOCAL | Block nonlocal statements when true. | true |
SAGEMATH_MCP_SECURITY_LOG_VIOLATIONS | Emit warnings when code is blocked. | true |
SAGEMATH_MCP_SECURITY_ALLOWED_IMPORTS | Comma-separated allowlist of importable modules. | math,cmath,statistics,base64,io,sage,sage.all |
SAGEMATH_MCP_SECURITY_ALLOWED_IMPORT_PREFIXES | Comma-separated prefixes treated as safe namespaces. | sage. |
| Argument | Description | Default |
|---|---|---|
--transport | Transport protocol: stdio, http, streamable-http, or sse. | stdio |
--host | Bind address for HTTP transports. | 127.0.0.1 |
--port | Listen port for HTTP transports. | 8314 |
--path | Custom HTTP path (e.g., /mcp) for streamable-http or sse transports. | auto |
--log-level | Python logging level (DEBUG, INFO, WARNING, ERROR). | INFO |
make allowlist is needed after a SageMath upgrade or any change to what the
worker namespace contains; an integration test and a weekly job fail when the
committed allowlist and the installed Sage disagree. It writes through a
temporary file, because the generator imports the module it replaces. Read the
diff: every added name is a name every caller can then use, and anything that
compiles, spawns, writes or fetches belongs in _DANGEROUS_BARE_NAMES instead.
Without a local SageMath installation you can still run the whole unit suite --- it replaces the Sage worker with a lightweight Python interpreter to validate session plumbing. Coverage is at 100% of statements and branches, enforced in CI by --cov-fail-under=100; that number is checked on every run, unlike a test count written into prose.
100% line and branch coverage proves the security policy's lines run, not that a test would notice if the policy were wrong. make mutation measures the latter: it drives cosmic-ray over src/sagemath_mcp/security.py, applying each deliberate weakening (a flipped comparison, a dropped not, a relaxed and) and re-running the security suite to see how many the tests catch.
Each worker mutates its own copy of the tree with PYTHONPATH shadowing the editable install, turning a ~35-minute serial sweep into ~2 minutes. The result is written to mutation-stats.md; a weekly, non-gating CI job (.github/workflows/mutation.yml) regenerates and uploads it. The Hypothesis property tests in tests/test_security_property.py — every forbidden name in every referencing position, any attribute on any forbidden module, any import at all — are what kill the behavioural mutants. This is never a required check: the score measures test quality, not correctness, and allowlist.py is out of scope because it is generated data guarded by the Sage-agreement integration test.
The doctest corpus sweep proves the guardrails do not refuse mathematics. This is the other claim: does a model get more mathematics right when it can run Sage? benchmarks/ holds a fixed, seeded case set (cases.json, 24 problems in five difficulty tiers, every gold answer verified in the Sage container) and a Workflow (outcome_benchmark.workflow.js) that runs it through the model twice — reasoning alone vs. with Sage compute — scoring every answer for mathematical equivalence in Sage, by an independent step, never string-matched.
First run (subject model haiku, scoring judge sonnet, full detail in benchmark-stats.md):
| Tier | Reasoning only | With Sage |
|---|---|---|
| arithmetic (GSM8K-style) | 4/4 | 4/4 |
| competition (MATH-style) | 5/5 | 5/5 |
| advanced (CAS-suited) | 5/5 | 5/5 |
| compute-heavy | 3/5 | 5/5 |
| infeasible (factoring, 8×8 det, partitions) | 0/5 | 5/5 |
| Total | 17/24 (71%) | 24/24 (100%) |
The lift is entirely in the last two tiers — arbitrary computation a model cannot do in-context — where reasoning alone refused six problems and answered one confidently wrong (the failure mode verify_claim exists for), while Sage got all ten. On problems the model already handles, the tool changes nothing: it does not help where it is not needed, and does not hurt. A stronger subject model closes the gap on its own, so this measures the model as much as the server; it is never CI-gated.
What this does and does not show. The compute arm runs Sage directly (docker exec … sage -c), so it establishes that Sage computation helps this model on these cases — not yet that this server's design (its tool selection, input validation, handles and verifier) improves outcomes, because that arm bypasses the server. Measuring through the actual MCP interface, with enforced tool permissions and the execution/scoring evidence retained, is the intended next step: a three-arm comparison (no-tools / evaluate_sage-only / full-catalogue) in the tests/cli_integration harness under the CLI nightlies. That harness is not built yet — it is tracked as a follow-up in TODO, not something that runs today.
Ruff with line-length 100, target Python 3.12. Rules: E, F, W, B, UP, ASYNC, RUF, I (import sorting). Run make lint before committing.
Configure Git hooks after cloning:
The pre-push hook runs ruff automatically.
The project includes a comprehensive end-to-end test suite that validates the MCP server through real LLM CLI invocations. Located in tests/cli_integration/.
claude --print) and Gemini CLI (gemini -p)| Domain | Cases | Tools tested |
|---|---|---|
| Calculus | 10 | differentiate_expression, integrate_expression, limit_expression, series_expansion |
| Algebra | 11 | solve_equation, simplify_expression, expand_expression, factor_expression, calculate_expression |
| Linear algebra | 5 | matrix_multiply, matrix_operation |
| ODEs | 2 | solve_ode |
| Number theory | 6 | number_theory_operation |
| Statistics | 2 | statistics_summary |
| Plotting | 2 | plot_expression |
| General | 3 | evaluate_sage |
| Session | 2 | reset_sage_session, cancel_sage_session |
| Component | Version | Purpose |
|---|---|---|
| FastMCP | >=3.4.7,<4 | MCP server framework (tools, resources, middleware); capped below 4, which breaks cross-client session isolation |
| MCP SDK | 1.27+ | Model Context Protocol implementation |
| Pydantic | 2.12+ | Data validation and serialization for all models |
| anyio | 4.13+ | Async runtime abstraction |
| SageMath | 10.x | Mathematics engine (subprocess worker) |
| Ruff | 0.15+ | Linting and import sorting |
| pytest | 9.0+ | Test framework |
| pytest-asyncio | 1.3+ | Async test support |
| pytest-cov | 7.0+ | Coverage reporting (100% statement and branch coverage, gated in CI) |
| pip-audit | 2.9+ | Dependency vulnerability scanning |
| Hatchling | 1.29+ | Build backend |
| Docker | --- | Containerization and CI integration testing |
| Helm | 3.15+ | Kubernetes deployment |
| GitHub Actions | --- | CI/CD (Node.js 24 compatible) |
| Cosign | --- | Container image signing |
Every released version, newest first. CHANGELOG.md carries the
full detail; this is the shape of each release.
The 2026-08-24 field survey and four rounds of external review. All additive — no breaking changes.
Added
verify_claim — re-checks a stated claim through a proof ladder (symbolic
prover, exact difference, exact algebraic arithmetic, certified intervals,
numeric sampling), reporting proved/refuted/supported/undecided with
evidence. Exactness is a prerequisite for a proof, and active assumptions are
named in the result.pip install "sagemath-mcp[passagemath]") — an
experimental ~1 GB pip alternative to the 3 GB Sage image; both runtimes work,
the security artifact set is chosen at import.Fixed
A security patch on 0.6.0.
Security
import * feature. The screen
vetted each export by its __module__, but a module object has none, so a
re-exported module (sage.modular.dims → dirichlet) passed and became a
pivot into the whole sage.* tree; dirichlet.free_module_element.sage.env.os.system('id')
ran a shell. The screen now drops module-object exports and the validator
refuses a terminal module name under any root.Added
from <module> import * for a curated set of clean internal
SageMath modules (generated into star_exports.py; nothing added to the
allowlist).evaluate_sage auto-declares symbol-shaped free names (w, x_2,
alpha) the way SageMath's SR does, instead of refusing them — narrow and
typo-guarded, and a session variable is never turned back into a symbol.Changed
set_verbose is offered as a no-op (it only sets a global verbosity level,
which has no surface over MCP); inject_shorthands is simulated so its names
are readable; a literal attrcall('method') is accepted after its name is
screened. Doctest-corpus acceptance rose 98.69% → 98.95%.Fixed
attrcall wrapper is reinstalled after every namespace reseal; it
had silently stopped working after the first specialised-tool call in a session.A security and correctness release, and the largest so far. Output changes,
so it is a minor bump rather than a patch: 2^3 is 8, x/y/z/t are
predefined, and callers lose imports, the external CAS interfaces and
show/view/latex/html.
Security
operator.attrgetter, and Sage's own attrcall/raw_getattr/getattr_debug);
pari and latex.has_file running shell commands; unpickle_global
reachable after a tool call re-imported sage.all; and bindings authorizing
names that already existed. See CHANGELOG.md and REVIEW_ACTIONS.md for the
full list with reproductions.sage module tree — the fragment path is validated independently of
where sage_eval resolves.Changed
evaluate_sage runs SageMath, not Python — the preparser is applied, so
2^3 is 8 and generator syntax parses.x, y, z and t are predefined, matching the specialised tools'
prelude; any other symbol needs var('w'), and the error says so.Fixed
f(x) = x^2 + 1 (the tutorial's first line), find_root taking an equation,
match statements and function('f') binding names, uniformly-indented code,
and refusal messages that now name an actionable fix.Added
test_math_coverage.py), the
research and physics sessions a user actually runs, and SageMath's own
doctests executed through the server.A correctness and hardening release. Output changes for large integers, so it is a minor bump rather than a patch.
Security
sage_eval-enabled generated code are now validated.
Four tool parameters (graph_operation.graph, group_operation.group,
coding_theory_operation.code_type, polynomial_ring_operation.base_ring) were
interpolated raw, which was demonstrated reading files, running shell commands
and opening outbound connections.f = open, a lambda default, a list literal, and the same for modules
(m = os, from sage.all import os as m).open, eval, exec, compile and
the rest of that family.Changed
bell(30) was reaching one CLI as
846749014511809388871680 instead of 846749014511809332450147.interrupt_sage_session reports No running computation when nothing is
running instead of claiming state was preserved. Signalling an idle worker
could leave it unable to answer, costing the namespace it was protecting.readOnlyRootFilesystem and resource defaults.Fixed
/health was never registered under FastMCP 3.x and returned 404 in every HTTP
deployment.is_convex returned true for concave and self-intersecting polygons.Internal
server.py split from 2327 lines into app.py, runtime.py, codegen.py and
a tools/ package; tool names, schemas and descriptions held identical by a
snapshot test.A correctness release: several tools returned wrong values or did not work at
all, so output differs from 0.3.1. Named workspaces
(start_sage_session/list_sage_sessions/stop_sage_session plus a session
argument), interrupt_sage_session, and the first exact-integer guards on
number_theory_operation.
Release-pipeline fixes only, no code change: Cosign lowercases the GHCR
reference, the build installs build first, PyPI trusted publishing via a pypi
environment.
18 tools to 33, all Sage-backed: symbolic_sum, combinatorics_operation,
plot3d_expression, distribution_operation, find_root,
plot_multi_expression and vector_calculus_operation.
The first substantial release: 18 MCP tools across calculus, algebra, linear algebra, ODEs, number theory, statistics and plotting; CLI integration suite; FastMCP 3.x migration; Docker pinned to SageMath 10.9; Helm health probes; Python 3.12 minimum.
Default HTTP port aligned to 8314 across code, docs and deployment artifacts; package published to GitHub Packages during release.
See ROADMAP.md for the full prioritized plan. Highlights:
Phase 1 — High-value tools:
symbolic_sum / symbolic_product --- symbolic summation and productscombinatorics_operation --- binomial, permutations, combinations, partitions, Catalan, Fibonacciplot3d_expression --- 3D surface plots for two-variable functionsPhase 2 — Medium-value tools:
distribution_operation --- probability distributions (PDF, CDF, sampling, quantiles)find_root --- numeric root-finding (complements symbolic solve_equation)vector_calculus_operation --- gradient, divergence, curl, LaplacianPhase 3 — Enrichment:
evaluate_sage examples (Fourier/Laplace transforms, modular arithmetic, recurrences)/health endpoint for Helm probesPATH (tested with Sage 10.x), or Docker.We welcome issues and pull requests! Review the Code of Conduct and Contributing Guide before opening a PR. For vulnerability disclosures, follow the steps in SECURITY.md. Ownership defaults are defined in .github/CODEOWNERS.
MIT