DecisionMatrix MCP
A transparent, 100% deterministic Model Context Protocol (MCP)
server that gives LLM agents a reliable multi-criteria decision analysis (MCDA) engine.
Agents are great at gathering options but unreliable at weighing them: they lose
precision, apply inconsistent weights, and can't show their work. DecisionMatrix
offloads the scoring to an exact, explainable engine. You provide options and
weighted criteria (plus a score matrix); it returns a fully scored, ranked, and
explained result — with per-criterion breakdowns, the methodology used, the weights
applied, and 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.
🌐 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, 15 calls/day per IP). Paid plans
(Starter $12/mo · 5,000/day, Pro $39/mo · 50,000/day) are live 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://decisionmatrix-mcp.pages.dev.
What it does
Six tools, all returning a uniform, agent-parseable envelope:
Scoring methods
Each criterion has a direction: benefit (higher is better — quality, speed) or
cost (lower is better — price, latency, risk). Weights are relative; they are
normalized to sum to 1 internally.
Consistent response envelope
Every successful response contains: status, method, winner, ranking
(with per-criterion breakdown), methodology, weights_used, inputs_used,
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: score is a deterministically-rounded number (6 dp)
for easy consumption; score_exact / raw_score are full-precision strings so no
precision is lost in JSON. Rankings are computed on the exact values, with input order
as a stable tie-break.
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.
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
Any Streamable-HTTP MCP client
Point it at https://decisionmatrix-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
create_decision(options, criteria, scores, method="weighted_sum")
- options — array of names (
["Vendor A","Vendor B"]) or objects
([{"name":"Vendor A","scores":{...}}]). Minimum 2, names unique.
- criteria — array of
{ "name", "weight" (>=0), "direction": "benefit"|"cost" }.
At least one weight must be > 0.
- scores — the option×criterion matrix. Accepted shapes:
- object map:
{ "Vendor A": { "Price": 100, "Quality": 8 }, ... }
- array:
[ { "option": "Vendor A", "scores": { ... } }, ... ]
- inline on each option object.
- method —
weighted_sum (default) · weighted_product · topsis (aliases like
saw, wpm, ideal also resolve).
score_options(options, criteria, scores, method)
Same inputs as create_decision; returns the full scored matrix (per-option,
per-criterion normalized scores + totals) without the winner narrative.
sensitivity_analysis(options, criteria, scores, method, variation=0.2, steps=10)
Sweeps each criterion's weight from -variation to +variation (fractional, e.g.
0.2 = ±20%) in steps increments (2–100), renormalizing the others, and recomputes
the winner each time. Returns a robustness_score (share of scenarios the baseline
winner stays #1), the fragile_criteria, and per-criterion flip points.
compare_two(option_a, option_b, criteria, scores, method)
Head-to-head between exactly two options (pass option_a/option_b names, or a
2-element options array). Returns the winner, score margin, criteria_wins, and a
per_criterion breakdown showing which option each criterion favours.
list_methods() / health_check()
Discovery + status. No parameters.
Example tool-call payloads
Choose a laptop (price & weight are cost criteria):
Test how robust the winner is:
Head-to-head:
Deploy on Cloudflare Pages
Same pattern as PrecisionCalc — 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
scoring engine is stateless and the server fails open (free tier, quota disabled).
Enabling billing (already live on the hosted server)
The hosted server uses these — replicate them for your own paid deployment:
- KV namespace for API keys + daily usage counters, bound as
DECISIONMATRIX_KV
in wrangler.toml.
- 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.
- Quota —
consumeQuota() is a KV daily counter (resets 00:00 UTC); the single
gating point in handleRpc where method === "tools/call".
- 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.
DecisionMatrix 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
scoring logic.
Design decisions & assumptions
- Deterministic by construction. 40-digit decimal math,
ROUND_HALF_UP
everywhere, and stable input-order tie-breaking. No floats, no randomness, no clocks
in the result.
- Normalization is per-criterion and direction-aware.
weighted_sum uses min-max
(best→1, worst→0); if a criterion is identical across all options it's treated as
neutral (normalized to 1) and noted. weighted_product uses ratio normalization and
requires strictly positive scores (clear error otherwise). topsis uses vector
normalization and ranks by closeness to the ideal/anti-ideal.
- Weights are relative — normalized to sum to 1, so
[3,2,1] and [30,20,10]
give identical results.
- Scores are relative to the option set — they measure standing within the
provided alternatives, not an absolute grade. This is stated in
notes.
- Errors are data, not exceptions — every tool returns
status:"error" with a
machine type and an actionable hint. Validation covers duplicate names, missing
cells (listing exactly which), non-numeric scores, bad weights/directions, and
unknown methods.
- Stateless & side-effect-free — trivially cacheable, horizontally scalable, and
safe to run anywhere (Cloudflare, Node, Deno, Bun).
Testing
The suite pins the hand-verifiable weighted_sum arithmetic, checks determinism,
weight-relativity, direction handling, ties, all three methods, compare_two,
sensitivity_analysis, the multiple score-input shapes, and every error path.
Roadmap (post-MVP)
- More methods: AHP (pairwise weight elicitation), ELECTRE, PROMETHEE, Borda count.
- Group decisions: aggregate multiple stakeholders' weight/score sets.
- Monte-Carlo sensitivity (perturb all weights jointly) alongside one-at-a-time.
- Per-key usage dashboard + Redis/Durable-Object quotas for stronger consistency.
- Published npm package + a hosted multi-tenant tier.
License
MIT — see LICENSE.