npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@formalcore/mcp-diff

v0.1.0

Published

Authority diff between two MCP server states. Black-box introspection, never executes tools. The permission-change screen for MCP servers.

Readme

mcp-diff

The permission-change screen for MCP servers.

CI npm License

What is it

mcp-diff compares two states of a Model Context Protocol server and reports how its authority changed. It performs the MCP handshake, walks the list* methods for tools, resources, and prompts, and diffs the two declared manifests. It never invokes a tool.

The output is a single verdict, CLEAN / REVIEW / DENY, plus a per-change breakdown suitable for a PR review comment or a SARIF upload to GitHub code scanning. Exit codes make it a first-class CI citizen.

This is the "this app wants new permissions - Allow / Deny" moment your phone has, applied to MCP servers.

Quick start

Check the MCP servers you already have. No install, no config, no account:

npx @formalcore/mcp-diff

With no arguments, mcp-diff discovers your installed MCP client configs (Cursor, Claude Desktop and Claude Code, VS Code, Windsurf, Gemini CLI), introspects each declared server once, and pins a baseline on the first run. On every later run it diffs the live servers against that pinned baseline and reports what changed. This mirrors the uvx mcp-scan@latest on-ramp, with one deliberate difference: nothing leaves your machine, there is no API and no account.

Point it at a specific config instead of auto-discovery:

npx @formalcore/mcp-diff ~/.cursor/mcp.json

To diff two explicit versions rather than a pinned baseline, pass them as two arguments (see Usage).

Why this exists

MCP security is 2026's loudest agent-safety issue. Recently reported figures:

  • Roughly 200,000 MCP instances exposed by a single disclosed vulnerability class.
  • Microsoft's June 2026 advisory on poisoned tool descriptions - instructions smuggled into the description field aimed at the model, not the human operator.
  • 52% of audited public MCP servers are effectively abandoned (no updates, no maintenance).
  • 8.5% of public servers use OAuth; the rest ship no or ad-hoc authentication.
  • 50% of MCP adopters cite security as the #1 blocker to production rollout (Zuplo, mid-2026).

The trust gap the MCP spec itself acknowledges: tool descriptions and annotations are untrusted metadata. A server can declare readOnlyHint: true and delete your files anyway. Between v1.4.2 and v1.5.0, that guarantee can silently disappear and no one is watching. Static scanners look at one point in time; mcp-diff looks at the transition, which is where rug-pulls actually happen.

How it works

  • Introspect two states. Spawn OLD, do the MCP handshake, list everything paginated to exhaustion. Repeat for NEW. Shut both down.
  • Canonicalize + hash. Recursively key-sort every declared object; sha256 the canonical form. Cosmetic reorder is never a false positive; the whole-manifest fingerprint is stable and signable.
  • Diff authority, not behavior. Classify added / removed / mutated tools, readOnlyHint downgrades, new destructiveHint, input-schema changes, and model-directed text patterns in descriptions.
  • Verdict. DENY on any deny-level change; REVIEW if changes exist but none rise to deny; CLEAN otherwise. The exit code follows the verdict.
  • Fail-closed. Timeouts, malformed manifests, and pagination anomalies raise named errors and exit non-zero. A hung server never silently reports CLEAN.

The introspection never calls a tool. That is the black-box discipline in the name.

Install

npm install -g @formalcore/mcp-diff

Requires Node 18 or newer.

Usage

Commands

| Command | What it does | |---------|--------------| | mcp-diff | Scan installed MCP configs, pin on first run, diff against the pin (default) | | mcp-diff [CONFIG...] | Scan specific config files | | mcp-diff diff <A> <B> | Authority diff between two explicit servers | | mcp-diff inspect [CONFIG...] | Print declared manifests, no verdict | | mcp-diff whitelist <name> <hash> | Allowlist a tool (writes .mcp-diff-ignore) | | mcp-diff pin [CONFIG...] | Re-record the baseline from the current state |

Common flags: --json, --sarif, --report-format=markdown|json|sarif, --report-path=<file>, --no-fail (advisory, always exit 0), --baseline-path=<file>, --transport=stdio|http, --url-a=, --url-b=, --timeout-ms=, --request-timeout-ms=, --help.

stdio servers (local commands)

mcp-diff "npx [email protected]" "npx [email protected]"

The two positional arguments are shell command strings. mcp-diff spawns each in turn, does the handshake, and diffs.

HTTP servers (Streamable-HTTP transport)

mcp-diff --transport=http \
  --url-a=https://old.example.com/mcp \
  --url-b=https://new.example.com/mcp

HTTP transport does not execute any local code and is preferred anywhere the choice exists.

GitHub Action

- name: MCP authority diff
  uses: formalcore/mcp-diff@v1
  with:
    old-spec: 'npx acme-mcp-server@${{ github.event.before }}'
    new-spec: 'npx acme-mcp-server@${{ github.sha }}'
    mode: markdown
    fail-on: deny

See action.yml for the full input surface.

Output formats

Markdown (default)

Rendered for a PR comment:

## MCP capability authority diff
`[email protected]` -> `[email protected]`
### DENY
- ~ `search_repos` no longer read-only (readOnlyHint dropped)
- ! `search_repos` description carries model-directed instructions (poisoning / rug-pull)
- + tool `delete_repository`  - DESTRUCTIVE
- ~ `create_issue` input schema changed

JSON (--json)

The full DiffResult shape, deterministic, safe to pipe into jq or a bot:

{
  "decision": "DENY",
  "a": { "name": "acme-github", "version": "1.4.2", "fingerprint": "sha256:..." },
  "b": { "name": "acme-github", "version": "1.5.0", "fingerprint": "sha256:..." },
  "changes": [
    {
      "kind": "tool.annotation.read_only_lost",
      "severity": "deny",
      "tool": "tool:search_repos",
      "detail": "...",
      "frameworks": { "owasp_llm": "LLM06", "owasp_agentic": null, "mitre_atlas": null, "safe_mcp": "SAF-T1201" }
    }
  ],
  "ruleset": { "id": "formalcore.default", "version": "0.1.0", "fingerprint": "sha256:..." }
}

Every verdict is stamped with the ruleset that produced it (id, version, and a content-addressed fingerprint), so the same two manifests and the same ruleset hash reproduce a byte-identical verdict. Each change also carries the external-standard IDs it is classified under (OWASP LLM Top 10, OWASP Agentic Top 10, MITRE ATLAS, and OpenSSF SAFE-MCP), cited rather than authored here.

SARIF (--sarif)

SARIF v2.1.0 for GitHub Advanced Security code scanning, GitLab, Azure DevOps, and other SARIF-native platforms. Each Change becomes one Result; severities map deny -> error, review -> warning, info -> note.

mcp-diff --sarif "server-old" "server-new" > mcp-diff.sarif

Exit codes

| Code | Verdict | Meaning | |------|---------|---------| | 0 | CLEAN | No material change | | 1 | DENY | One or more deny-level changes | | 2 | REVIEW | Changes present but none deny-level | | 3 | USAGE | CLI argument / validation error | | 4 | INFRA | Introspection / transport / malformed-manifest error |

Detected change kinds

Severity and provenance for every change kind are DATA, not code: they live in the ruleset (src/ruleset.default.json), validated against the engine vocabulary at load and content-hashed into every verdict. Only the detail wording lives in code (src/catalog.ts). The core set:

| Kind | Severity | Origin | |------|----------|--------| | tool.added | review | MCP spec (Tools) - new tool equals new authority surface | | tool.removed | info | MCP spec (Tools) - removed tool equals authority decrease | | tool.description_changed | review | MCP spec (Tools) - descriptions are model-visible and untrusted metadata | | tool.schema_changed | review | MCP spec (Tools) - schema shift alters what a model can invoke | | tool.annotation.read_only_lost | deny | Snyk / Invariant Labs mcp-scan disclosure (June 2025) - silent readOnlyHint downgrade is the canonical rug-pull | | tool.annotation.destructive_added | deny | MCP spec (Tools, Annotations) - destructiveHint activation equals irreversible authority | | tool.model_directed_text | deny | Microsoft security advisory (June 2026) - poisoned tool descriptions | | resource.added | info | MCP spec (Resources) - resource surface expansion | | resource.removed | info | MCP spec (Resources) - resource surface contraction | | prompt.added | info | MCP spec (Prompts) - prompt surface expansion | | prompt.removed | info | MCP spec (Prompts) - prompt surface contraction |

If you disagree with a severity, that is a row in the ruleset with a citation, not a config flag. The full technique coverage, including the runtime and multi-tool techniques this tool deliberately defers, is in docs/COVERAGE.md. See CONTRIBUTING.md.

Use with, not instead of

mcp-diff is complementary to Snyk / Invariant mcp-scan and Cisco mcp-scanner, not a replacement. Those tools scan point-in-time and watch runtime traffic; mcp-diff diffs declared authority version-to-version in CI and emits a signed, reproducible verdict, fully locally. Run both: the runtime guard is the ceiling, the declaration diff is the deterministic floor. The full rationale, the exact seam, and an honest account of the overlap (they already do local hash pinning) are in POSITIONING.md.

Badge

If your MCP server is authority-pinned by mcp-diff in CI, advertise it:

[![authority-pinned by mcp-diff](https://img.shields.io/badge/authority--pinned-mcp--diff-2b6cb0)](https://github.com/formalcore/mcp-diff)

What it is not

  • Not a behavioral rug-pull detector. mcp-diff catches changes to the declared manifest. A byte-identical manifest whose implementation quietly starts exfiltrating data is invisible to this tool. Runtime observation is a separate capability tracked for a future release. See docs/LIMITS.md for the exact boundary, including the postmark-mcp v1.0.16 rug pull that returns CLEAN because its declaration never changed.
  • Not a sandbox. Introspecting a stdio server means spawning its process. Capture-is-RCE by construction. Run stdio introspection inside E2B, a Firecracker microVM, or an equivalent isolated environment with --ignore-scripts, no network, ephemeral filesystem, and dropped capabilities. HTTP transport has no such issue and is preferred.
  • Not a trust-the-annotation classifier. mcp-diff never lowers a severity because a self-reported annotation says so. A tool that declares readOnlyHint: true while its name contains a destructive verb is still flagged.
  • Not a defense against every evasion. Known open classes under active hardening: unicode homoglyphs in tool names, semantically-equivalent description rewrites that avoid the model-directed regex, and capabilities relocated from tools into resources or prompts. Contributions on any of these are welcome.

Roadmap

  • v0.1 (current): stdio + HTTP transport, catalog-driven classifier, markdown / JSON / SARIF renderers, GitHub Action, npm publish with provenance.
  • v0.2: E2B-hosted sandboxed stdio capture (fail-closed by construction), classifier hardening per the 22-item internal audit.
  • v0.3: Signed evidence trail (sigstore keyless) - every verdict signed and Rekor-logged so a downstream consumer can independently verify the diff was produced by mcp-diff.
  • v1.0: The MCP Capability Drift Report - a full sweep of the official MCP Registry, one blog post per finding, published as the launch content.

Provenance

Built at packages/mcp-diff inside the FormalCore monorepo. Extracted to a standalone public repository for the launch. If you found this via a link and want the fuller architectural context, docs/superpowers/plans/BUILDERLOOP/ in the upstream repo has the market analysis and the seat play behind this tool.

License

Apache License 2.0. See LICENSE.