ScenarioSim MCP
A transparent, 100% deterministic Model Context Protocol (MCP)
server that gives LLM agents a reliable what-if / scenario simulation engine.
Agents are good at describing a plan but unreliable at projecting it: they drift on
multi-period arithmetic, mishandle compounding, and can't show their work. ScenarioSim
offloads the simulation to an exact, explainable engine. You provide assumptions
(growth rates, churn, pricing, costs, starting metrics, a time horizon); it returns
projected outcomes over time, key metrics, the exact assumptions used,
plus sensitivity analysis and break-even solving — each with a plain-language
explanation.
Every number flows through decimal.js at
40-digit precision (never floats), so identical inputs always produce
byte-identical output. The server is stateless — no database, no sessions,
no clocks or randomness in the result.
This is the third product in a suite built to the same engineering standard as
PrecisionCalc MCP (deterministic high-precision finance/business math) and
DecisionMatrix MCP (transparent multi-criteria decision analysis): identical
project structure, output philosophy, and Cloudflare Pages deployment.
🌐 Live hosted server (free, no install)
A public remote MCP server runs on Cloudflare's edge — point any Streamable-HTTP
MCP client at it:
It runs in open mode on the free tier (no key, 20 calls/day per IP). Paid plans
(Starter $12/mo · 5,000/day, Pro $39/mo · 50,000/day) are available via Stripe
Checkout — buy a plan, get an API key instantly, and send it as X-API-Key. Self-host
for unlimited calls with no keys. Landing page + pricing: https://scenariosim-mcp.pages.dev.
What it does
Six tools, all returning a uniform, agent-parseable envelope:
Scenario templates
Every template accepts horizon (number of periods, 1–1200) and period_label
(day/week/month/quarter/year, which also sets annualization). Inputs you don't
provide fall back to documented defaults; unknown inputs are ignored and reported in notes.
Call list_templates for the full input/output catalog.
Consistent response envelope
Every successful response contains: status, scenario, period_label, horizon,
key_results (+ key_results_detail with units and full-precision value_exact),
projections, assumptions_used, methodology, notes, and a natural-language
explanation.
Errors never cross the tool boundary as exceptions — they come back as a
structured, actionable envelope:
Design note — exact numbers: headline numbers in key_results are
deterministically rounded (6 dp) for easy consumption; key_results_detail[].value_exact
and assumptions_used carry full-precision strings so no precision is lost in JSON.
All internal math is exact 40-digit decimal.
Project structure
Separation of concerns: engine.mjs is pure and transport-agnostic (import it
directly in tests or any Node/Deno/edge runtime); index.mjs only handles the MCP
JSON-RPC wiring, HTTP, CORS, and the auth/metering seam; server.mjs re-uses the same
engine over stdio.
Requirements
- Node 18+ (for the build, tests, and local dev). Only two dev/runtime deps:
decimal.js (math) and esbuild (bundler).
- A Cloudflare account (free tier is fine) to deploy the hosted version.
Run it locally
Quick manual call:
Install via npm (stdio, no hosting)
Run the server locally over stdio with a single command — nothing to deploy:
Claude Desktop / any stdio MCP client (claude_desktop_config.json):
This is the same deterministic engine as the hosted server, running on your machine.
Client configuration
Cursor — ~/.cursor/mcp.json
Claude Desktop — claude_desktop_config.json
Claude Desktop launches stdio servers, so bridge to the HTTP endpoint with mcp-remote:
VS Code — .vscode/mcp.json
Windsurf — ~/.codeium/windsurf/mcp_config.json
Any Streamable-HTTP MCP client
Point it at https://scenariosim-mcp.pages.dev/mcp (or your self-hosted URL). If
you enable auth, add X-API-Key (or Authorization: Bearer <key>) in the client's
headers.
Tools & parameters
run_scenario(template?, inputs?, metrics?, horizon?, period_label?)
- template — one of the template ids above (aliases like
saas, pricing, runway,
ltv, funnel also resolve). Omit it (or pass "custom") to run a free-form model.
- inputs — the assumptions object for the template, e.g.
{ "churn_rate": 0.03, "arpu": 60 }. Also accepted as assumptions, or spread at the
top level. Missing keys use documented defaults.
- metrics — (custom mode) array of
{ name, start, growth_rate?, mode? } where
mode is "compound" (default, x·(1+r)ⁿ) or "linear" (x·(1+r·n)).
- horizon — number of periods to project (1–1200). Default per template (usually 12).
- period_label —
day/week/month/quarter/year (default month).
sensitivity_analysis(template, variable|variables, target_metric?, variation?, steps?, values?, min?, max?, inputs?, horizon?)
Sweeps each listed input across a range (default ±variation=0.2 around the baseline,
steps=5) while all others stay at baseline, recomputing target_metric (defaults to the
template's primary output) at each point. Returns per-variable sweep rows, an
elasticity_estimate, the output_range, and a most_influential ranking. You can also
give explicit values: [...] or a min/max grid instead of variation.
break_even(template, solve_for, target_metric?, target_value, bounds?, inputs?, horizon?)
Solves for the value of solve_for (an input name) that makes target_metric equal
target_value, via deterministic bisection with automatic bracket expansion. Returns
required_input, change_from_baseline, achieved_metric, and residual. Assumes the
metric is monotonic in the solved input over the search range; if the target can't be
bracketed it returns a clean no_solution error with the achievable range. Pass explicit
bounds: [lo, hi] to constrain (or fix) the search.
compare_scenarios(scenarios, compare_metric?, goal?, horizon?, include_projections?)
Runs 2–3 scenarios ({ name?, template, inputs }, or { name?, metrics } for custom)
and aligns their key_results, differencing each against the first (baseline). Pass
compare_metric + goal (max default | min) to rank and pick a winner. Set a shared
horizon at the top level, or per-scenario.
list_templates() / health_check()
Discovery + status. No parameters.
Example tool-call payloads
Project 12 months of SaaS growth:
Which lever moves ending MRR the most?
What churn keeps 90% of customers after a year?
→ required_input ≈ 0.008742 (about 0.87%/month).
Compare growth strategies:
Free-form (custom) model:
Deploy on Cloudflare Pages
Same pattern as PrecisionCalc / DecisionMatrix — one build step bundles worker-src/
into site/_worker.js (Pages "advanced mode" Function), then Wrangler deploys the site/
directory.
Or wire it to Git: create a Pages project, set the build command to npm run build
and the output directory to site. Every push deploys automatically. The
compatibility_date and project name live in wrangler.toml.
To run fully free / private, you need no bindings, secrets, or env vars — the
simulation engine is stateless and the server fails open (free tier, quota disabled).
Enabling billing (optional)
Replicate these for a paid deployment:
- KV namespace for API keys + daily usage counters, bound as
SCENARIOSIM_KV
in wrangler.toml (wrangler kv namespace create SCENARIOSIM_KV).
- Stripe products/prices (subscription) — put the price IDs in
[vars]
(PRICE_STARTER, PRICE_PRO) and the daily limits (FREE_DAILY, STARTER_DAILY,
PRO_DAILY).
- Stripe secrets (never in the repo):
- Webhook → create a Stripe webhook endpoint at
https://<your-domain>/webhook
for customer.subscription.updated + customer.subscription.deleted.
Routes wired up: /checkout?plan=starter|pro → Stripe Checkout, /success provisions
and shows the API key (idempotent), /portal opens the Stripe billing portal,
/webhook handles subscription lifecycle (revoke/restore), /metrics reports usage.
Auth & rate limiting
The hosted server enforces tiered quotas in worker-src/billing.mjs:
- Identity —
identify() reads X-API-Key / Authorization: Bearer, looks the key
up in KV, and falls back to per-IP free tier. (To add JWT/mTLS/per-org keys, change
extractKey + identify only — the engine and transport are untouched.)
- Quota —
consumeQuota() is a KV daily counter (resets 00:00 UTC); the single
gating point in handleRpc where method === "tools/call". (Swap for a
sliding-window / token-bucket in a Durable Object or Redis for per-minute limits — see
the NOTE (rate limiting) comment.)
- Paywall response — over-quota / invalid / revoked keys get a structured
upsell
envelope with pricing + checkout URLs (agents can read and act on it).
- Usage metering — in-memory counters at
/metrics.
ScenarioSim has no paid-only tools — every tool works on every tier; paid plans
only raise the daily quota. To make a tool paid-only, add its name to PAID_ONLY_TOOLS
in index.mjs. Because the engine is pure and stateless, none of this touches the
simulation logic.
Design decisions & assumptions
- Deterministic by construction. 40-digit decimal math,
ROUND_HALF_UP everywhere,
period-by-period iteration (not float**n), and no clocks/randomness in results.
- Period 0 is the starting state; periods
1..horizon are projected. period_label
sets the annualization factor (month → 12/yr, etc.), which is used for ARR/payroll.
- Assumptions are echoed back in full (
assumptions_used) with defaults filled in, so
a caller always knows exactly what was simulated.
- Counts stay fractional for precision (e.g. 233.6 customers); round to integers in
your presentation layer if needed. This is stated in
notes.
- Elasticity/growth models are intentionally simple and transparent (constant
elasticity, constant per-period rates). They're honest first-order estimates, not
econometric forecasts — the methodology block says so.
- break_even uses bisection with automatic bracket expansion and a fixed iteration
budget → deterministic. It assumes monotonicity of the metric in the solved input over
the range; non-monotonic/ratio metrics (with poles) return a clean
no_solution rather
than a wrong root. sensitivity_analysis/break_even operate on named templates (not
the free-form custom model) and say so if misused.
- Errors are data, not exceptions — every tool returns
status:"error" with a machine
type and an actionable hint. Validation covers unknown templates/inputs/metrics,
non-numeric values, bad horizons/period labels, unreachable targets, and more.
- Stateless & side-effect-free — trivially cacheable, horizontally scalable, and safe
to run anywhere (Cloudflare, Node, Deno, Bun).
Testing
The suite pins hand-verifiable arithmetic (compound growth, LTV/CAC, elasticity, runway),
checks determinism, the multiple assumption-input shapes, period-label annualization,
custom free-form models, the sensitivity sweep + influence ranking, the break-even solver
(including the unreachable-target path), scenario comparison with goal=min, and every
error path.
Roadmap (post-MVP)
- More templates: LBO/DCF, inventory & cash-conversion cycle, ad-spend ROAS, cohort retention.
- Monte-Carlo mode: distributions on inputs → confidence bands on outcomes (seeded, still deterministic).
- Multi-variable (grid) sensitivity and tornado charts alongside one-at-a-time.
- Break-even on the free-form
custom model and on multiple simultaneous inputs.
- Per-key usage dashboard + Durable-Object quotas for stronger consistency.
License
MIT — see LICENSE.