calculate — safe batch expression evaluator
Status: implemented (this PR)
Prior art: filliptm/ComfyUI_FL-MCPbackend/calc.py— a batch math evaluator with persistent assignments, seeded RNG, and a strict AST whitelist. Python got safety cheap viaast; our TypeScript equivalent is a tiny hand-rolled parser — noeval/new Function, ever.
Motivation
Agents constantly do arithmetic they get wrong token-by-token: SDXL-legal resolutions from an aspect ratio (floor(sqrt(1024*1024*ar)/64)*64), tile/grid layout math, cfg/denoise sweeps, reproducible seed batches. A connection-free calculate tool answers these exactly, in one call, and is one of the few tools that works even when ComfyUI is down.
Parser decision: zero-dep hand-rolled Pratt parser
Verified: runtime deps are 8 heavyweight-for-a-reason packages; nothing expression-related exists in deps orsrc/. Adding a dependency for ~150 lines of parser contradicts the repo’s lean-deps philosophy, and expr-eval is unmaintained with past prototype-pollution advisories. Recursive-descent/Pratt it is.
Tool API
calculate — category diagnostics, pure, no ComfyUI connection (usable in cloud mode):
calc.py splits on commas too, which breaks min(1, 2) unless the caller knows the quirk. We split only on newlines and semicolons; the parser handles commas inside call argument lists. Documented in the tool description.
Result (text + machine-readable):
results[i] = null plus an entry in errors: [{ line, expr, message }] — better for agents than FL-MCP’s fail-fast, since they see exactly which line broke. Total failure (nothing parseable) still returns isError via errorToToolResult.
Evaluator design — src/services/calc-evaluator.ts (pure, ~200 lines)
- Tokenizer: numbers (int/float/scientific), identifiers, operators
+ - * / // % **, comparisons< <= > >= == !=(returning 1/0 — useful for sweeps/clamps), parens, comma,=. - Pratt parser → direct evaluation (no retained AST): precedence
= (statement) < comparison < add < mul < unary < ** (right-assoc) < call/primary. Assignment only as a full statementname = expr(FL-MCP’s restriction; no chaineda = b = 1). - Environment:
Map<string, number>seeded fromvariables; constantspi, e, tau; function table (numbers only — no strings/objects):abs round min max pow sqrt floor ceil sin cos tan asin acos atan atan2 sinh cosh tanh exp log log10 log2 hypot radians degrees sign trunc clamp(x,lo,hi)plusrand() random() uniform(a,b) randint(a,b). Arity-checked; unknown identifier/function → line error. - Seeded RNG: mulberry32, instantiated per call from
seed >>> 0; when omitted, drawn fromcrypto.randomIntand echoed so runs are reproducible after the fact.randint(a,b)inclusive both ends (matches Pythonrandom.randint). - Safety: no property access, no strings, no arrays, no
eval; results arenumberonly; caps (≤1000 expressions, ≤10k chars each); NaN/Infinity passed through but flagged in the rendered text.
Implementation plan
src/services/calc-evaluator.ts— tokenizer, Pratt evaluator, mulberry32,evaluateBatch.src/tools/calculate.ts—registerCalculateTools(server): schema above, spec normalization (split/trim/drop-empty, minus commas), rendering,errorToToolResult. Motivating examples in the description (aspect-ratio snap-to-64, per-linerandint(0, 2**32-1)seed batches, cfg sweeps).src/tools/index.ts— append["diagnostics", registerCalculateTools]toTOOL_GROUPS(order-stability rule: append-only).
Test plan (vitest)
src/__tests__/services/calc-evaluator.test.ts (bulk of coverage, pure): precedence incl. right-assoc 2**3**2 = 512; unary minus -2**2; floordiv/mod; comparisons; assignments persisting and returned; input variables respected and not mutated; determinism — same seed twice → identical sequences, mulberry32 known-answer for seed 42; randint bounds inclusivity over many draws; per-line error isolation (later lines still evaluate); rejection of constructor, __proto__, property-access syntax, string literals; spec-size limits. Thin src/__tests__/tools/calculate.test.ts for schema/rendering.
Rollout / compat
Additive and connection-free. Registers throughTOOL_GROUPS, so the compact router catalog picks it up automatically. No gates (pure computation).