@polygraphso/litmus
v0.36.0
Published
Behavioral litmus harness for MCP servers — grade a server A–F (tool-output injection, egress, sensitive-data, adversarial-input) with reproducible, content-addressed evidence. Ships a CLI and an MCP server with a run_litmus tool for AI agents.
Maintainers
Readme
@polygraphso/litmus
The behavioral litmus harness for MCP servers, from polygraph.so.
It connects to an MCP server the way an agent would, fingerprints its exact tool surface, and runs four probe categories — C-01 tool-output injection (static, dynamic, and second-order — one tool's output weaponized as another's input), C-02 permission/egress (in a hardened default-deny Docker sandbox, matched host and port), C-03 sensitive-data handling (planted canaries), C-04 adversarial-input handling (malformed/oversized and jailbreak inputs) — then grades the server A–F and produces a deterministic, content-addressed evidence bundle.
A passing grade is a measurement, not a guarantee. The methodology and its disclosed limits live at polygraph.so.
Install
npm i -g @polygraphso/litmus
# …or run without installing (note the -p flag — the package ships two bins,
# `polygraphso-litmus` and `polygraphso-litmus-mcp`, so npx needs to be told which):
npx -y -p @polygraphso/litmus polygraphso-litmus litmus npm/@modelcontextprotocol/server-filesystemRequires Node ≥ 18. Docker is optional — without it, C-02 (egress) is skipped and the grade is capped at B for that run.
Gate your CI (GitHub Action)
Fail a build when an MCP server — or an Agent Skill it ships — grades D/F.
The polygraph MCP gate
on the GitHub Marketplace wraps the harness as polygraphso/litmus@v1:
# .github/workflows/mcp-gate.yml
name: mcp-gate
on: [pull_request]
permissions:
contents: read
jobs:
gate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: polygraphso/litmus@v1
with:
# Auto-discovers MCP servers (.mcp.json / .vscode/mcp.json / .cursor/mcp.json)
# and skills (SKILL.md dirs). Or name them explicitly:
servers: |
npm/@modelcontextprotocol/server-filesystem
# min-grade: B # stricter than the default D/F gate
# strict: "true" # also fail on targets that cannot be gradedInputs: servers · skills · discover (default true) · min-grade · strict ·
working-directory · version · bearer. Outputs: result · failed · report.
Not on GitHub? The gate is a plain command —
npx -y -p @polygraphso/litmus polygraphso-litmus ci — so it runs in any CI or as a
pre-commit hook.
CLI
polygraphso-litmus litmus <registry-ref | https-url | path-to-mcp> # grade a server
polygraphso-litmus litmus --json <ref> # machine-readable evidence bundle
polygraphso-litmus litmus --timeout <seconds> <ref> # cap the whole run (default 900s)
polygraphso-litmus litmus --no-deps-audit <ref> # skip the dependency advisory scan
polygraphso-litmus check <ref> # look up a published gradeExamples:
# a remote https target runs no local code — graded directly
polygraphso-litmus litmus https://example.com/mcp
# a registry ref or local file launches the TARGET's own code. Grade it sandboxed:
LITMUS_STDIO_ISOLATION=docker polygraphso-litmus litmus npm/@modelcontextprotocol/server-filesystem
# …or, without Docker, opt in to running it on this host:
polygraphso-litmus litmus --unsafe-host-exec npm/@modelcontextprotocol/server-filesystemHost-execution safety. Grading a registry ref (npm/…, pypi/…) or a local
path launches the target's own code. By default the CLI refuses to do that on
your host: set LITMUS_STDIO_ISOLATION=docker to run the target only inside the
hardened sandbox, or pass --unsafe-host-exec to accept host execution. Remote
https:// targets run no local code and need neither.
Dependency advisories. Below the grade, an npm/… target's dependency tree is
checked against the osv.dev vulnerability database and any
vulnerable dependencies are listed. This is advisory only and point-in-time: it
never affects the A–F grade and is not part of the evidence bundle (vulnerability
data changes over time, so it stays out of the reproducible verdict). Other target
kinds report it as skipped. The scan resolves the tree with
npm install --package-lock-only --ignore-scripts — no tarballs are downloaded and
no package code runs. Opt out with --no-deps-audit or LITMUS_DEPS_AUDIT=0. From
the run_litmus MCP tool it is returned as a separate dependencyAudit field.
Token-gated servers. If a target is a token-gated https:// server and you pass no
--bearer / --header / LITMUS_BEARER, litmus — on the auth failure — looks for a token you
already configured for that server (matched by URL in your MCP client config: project
.mcp.json / .cursor/mcp.json / .vscode/mcp.json, or your Claude Code / Claude Desktop /
Cursor config) and offers to reuse it. It is read-only, asks before sending, sends only to the
target origin, and never prints the token. In non-interactive use, pass --use-discovered-auth
to opt in without a prompt.
If the server uses OAuth (no static token to reuse), litmus opens your browser to authorize,
captures the token via a single-use 127.0.0.1 callback, and grades with it — used for that run
only, never stored. This happens automatically on an interactive terminal; use --oauth /
--no-oauth to force or skip it. From the run_litmus MCP tool, set interactive_auth: true.
The litmus command exits non-zero on a failing grade (D/F), so it scripts in CI.
To dispute a published grade, just re-run litmus against the same server: the harness is
open and deterministic, so a re-run reproduces the grade — or refutes it.
Use it from an AI agent (MCP server)
For grade lookups, point any MCP client at polygraph's hosted endpoint, no install:
claude mcp add --transport http polygraph https://polygraph.so/api/mcpor the raw config:
{ "mcpServers": { "polygraph": { "url": "https://polygraph.so/api/mcp" } } }This serves the lookup tools only (check_server, list_servers, request_grade); grading a
server yourself (run_litmus, run_skill_litmus) executes its code, so it needs the local
stdio install below.
The package also ships a stdio MCP server, polygraphso-litmus-mcp, with the full toolset, so
it works in any MCP-capable client. For MCP servers it exposes:
check_server— read a server's published grade from polygraph.so in under a second (no execution). The default first move before recommending or installing a server. On a miss it says what to do next:request_gradeorrun_litmus.list_servers— servers with a published grade, A first; paged (default 25 per call, withgrade/limit/offsetfilters and a full-corpus summary).request_grade— record a grade request with polygraph.so. Recording is free; grading starts once the request's $1 one-time fee is paid (the response carries the payment link — web checkout in $POLYGRAPH, or x402/USDC for agents) and the grade publishes within 48h of payment. Read the result later withcheck_server. No contact details — the request records the calling client's self-reported name/version.run_litmus— actively grade a server now (runs the harness end-to-end) and return the grade and the evidence. Optionalbearer(andheaderentries, each"Key: Value") grade a token-gatedhttps://MCP target — sent to that origin only, ignored for stdio/local targets, the same plumbing as the CLI's--bearer/--header. Grading a registry ref or local path launches the target's own code, so it requiresunsafe_host_exec: trueunlessLITMUS_STDIO_ISOLATION=dockeris set (the MCP mirror of--unsafe-host-exec).verify_attestation— read the onchain proof behind a published grade (EAS on Base) before trusting or paying a server.
The lookup tools (check_server, list_servers, request_grade) read
polygraph.so's hosted grade index; run_litmus and verify_attestation need no
polygraph service at all — the harness and the chain are enough. (The lookups
previously shipped as a separate @polygraphso/mcp package, now deprecated.)
It also registers two prompts that show up as slash commands — in Claude Code,
/mcp__polygraph-litmus__grade <server_ref> (run a fresh grade) and
/mcp__polygraph-litmus__check <server_ref> (read a published grade); other
clients surface the same prompts in their own UI. For a cleaner pair of commands
in Claude Code — /polygraph:grade and /polygraph:check — install the plugin
(below), which wires up this server and both commands in one step.
Prerequisites: Node ≥ 18. Docker is optional (without it, C-02 egress is
skipped and the grade caps at B). The lookup tools and verify_attestation talk
to https://polygraph.so by default; POLYGRAPH_API_URL overrides the base
(https required; plain http only for localhost).
Heads-up: onchain attestation publishing is still rolling out, so
verify_attestationcommonly returnsnot_availableeven for serverscheck_servershows as graded — that means no onchain proof yet, not a failing grade. To grade a server right now, userun_litmus.
Claude Code: one-click plugin (recommended)
The plugin bundles this MCP server and adds the /polygraph:grade and
/polygraph:check commands — one install does everything:
/plugin marketplace add polygraphso/litmus
/plugin install polygraph@polygraphsoThen just run /polygraph:grade npm/@modelcontextprotocol/server-filesystem.
Prefer to wire the server up by hand, or using another client? Add it once, then just talk to your agent.
Claude Code — one command:
claude mcp add polygraph-litmus -e POLYGRAPH_API_URL=https://polygraph.so \
-- npx -y -p @polygraphso/litmus polygraphso-litmus-mcpCursor — one-click install:
Or wire it up by hand — Claude Desktop (claude_desktop_config.json) / Cursor (~/.cursor/mcp.json):
{
"mcpServers": {
"polygraph-litmus": {
"command": "npx",
"args": ["-y", "-p", "@polygraphso/litmus", "polygraphso-litmus-mcp"],
"env": { "POLYGRAPH_API_URL": "https://polygraph.so" }
}
}
}The
-pflag is required: this package ships two bins, so plainnpx @polygraphso/litmuscan't tell which to run. If you installed globally (npm i -g @polygraphso/litmus) you can instead use"command": "polygraphso-litmus-mcp"with no args.
Any other MCP client / the Claude Agent SDK: spawn the same stdio command —
npx -y -p @polygraphso/litmus polygraphso-litmus-mcp.
Then just ask your agent
Run polygraph against
npm/@modelcontextprotocol/server-filesystemand tell me the grade.
The agent calls run_litmus, which launches that server in the harness, runs
C-01/C-02/C-03/C-04, and returns the grade (A–F), the per-category results, and the
tool-surface fingerprint. Use verify_attestation instead to read a grade
that's already published.
run_litmus launches the target server's code to exercise it (egress-sandboxed
when Docker is present). It needs no wallet or RPC.
ChatGPT and other remote clients
ChatGPT's MCP support expects a remote Streamable-HTTP server. For grade lookups, point
it at polygraph's hosted endpoint (https://polygraph.so/api/mcp), no install needed.
The npm package itself remains stdio-only and exposes the full toolset, including
run_litmus and run_skill_litmus. To reach that over a remote transport instead, self-host
a bridge, e.g.
npx -y supergateway --stdio "npx -y -p @polygraphso/litmus polygraphso-litmus-mcp" --port 8000(or mcp-proxy) — then point your client
at that endpoint. polygraph does not host this for you; the bridge runs on your own
machine.
Troubleshooting
- Two bins /
npx:npxneeds-p @polygraphso/litmusplus the bin name (polygraphso-litmusorpolygraphso-litmus-mcp); plainnpx @polygraphso/litmuscan't choose which to run. Installed globally? Use the bin name directly, no-p. - Docker optional: without Docker, C-02 (egress) is skipped and the grade caps
at B — the C-02 row reads
skippedwith reasonno sandbox (Docker unavailable). Not a failure, just unverified. verify_attestationsayslookup_failed: the grade index or RPC was unreachable — that's unknown, not no grade. Retry; checkPOLYGRAPH_API_URL.
Grade a skill
Claude Code / Agent Skills (a SKILL.md plus an optional bundle) are graded by a
separate static litmus (litmus-skill-v3). It scans the skill's bytes — S-01
prompt injection in the body, S-03 data-exfiltration instructions, S-04
dangerous commands in the SKILL.md body or bundled executable scripts (including
base64-obfuscated curl | bash) — and content-hashes the whole directory. The letter
is A/B/D/F.
This is a static scan: it does not execute the skill or its scripts, so an A
means the static checks were clean, not that the skill is behaviorally safe. A
command the skill builds or fetches at runtime is not visible to it.
CLI
polygraphso-litmus-skill <path-to-skill-dir> # grade a local skill folder (must contain SKILL.md)
polygraphso-litmus-skill --json <path-to-skill-dir> # machine-readable safety + quality bundlesIt also prints a separate, advisory quality signal (well-formed / issues /
malformed) — never an A–F letter, never minted. Its deterministic checks
(frontmatter + bundled-link resolution) always run; the optional LLM-judged axes
(honesty, coherence) run only when a judge is available:
Inside an agent (the MCP tool below): the host agent's own model judges via MCP sampling — no key, any provider.
Standalone: bring your own key for any OpenAI-compatible endpoint:
export LITMUS_LLM_API_KEY=… # your key (any OpenAI-compatible endpoint) export LITMUS_LLM_MODEL=gpt-4o # a model the endpoint serves export LITMUS_LLM_BASE_URL=https://api.openai.com/v1 # optional; defaults to OpenAI # Other providers via their OpenAI-compatible endpoint, e.g.: # Claude: LITMUS_LLM_BASE_URL=https://api.anthropic.com/v1 LITMUS_LLM_MODEL=claude-sonnet-4-6 # Gemini: LITMUS_LLM_BASE_URL=https://generativelanguage.googleapis.com/v1beta/openai LITMUS_LLM_MODEL=gemini-2.5-flashWith neither, the judged axes are skipped — the grade and deterministic quality still run. The core never needs a key.
From an AI agent (MCP)
The same polygraphso-litmus-mcp server exposes two skill tools (plus grade-skill /
check-skill prompts):
run_skill_litmus— grade a skill now (static; uses the host model via sampling for the quality axes, no key). Takes a local skill directory or a public GitHub skill — a github.com URL to the skill folder or its SKILL.md, orgithub/<owner>/<repo>#<path>— downloaded over TLS and scanned locally.verify_skill_attestation— read a skill's already-published grade by itsskill_ref(source/owner/repo#path, e.g.github/anthropics/skills#skills/pdf). It returns the attestedcontentHash; recompute the skill's hash and require equality before installing — the content hash, not the version, is the trust anchor.
Library
import { runLitmus, gateDecision, liveFingerprint, readAttestation } from "@polygraphso/litmus";
const bundle = await runLitmus("npm/@modelcontextprotocol/server-filesystem");
console.log(bundle.grade, bundle.gradeRationale);
// Skills: static safety grade + a separate advisory quality bundle.
import { runSkillLitmus, runSkillQuality } from "@polygraphso/litmus";
const skill = runSkillLitmus("./skills/my-skill");
console.log(skill.grade, skill.contentHash);License
Apache-2.0
