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

@seal-dev/mcp-proxy

v0.2.0

Published

Self-hostable MCP-native approval gateway: a transparent proxy that gates tools/call on the Seal approval engine.

Readme

@seal-dev/mcp-proxy

An MCP-native approval gateway. It sits between an MCP client and an upstream MCP server, relays every method untouched, and pauses tools/call when a policy says a human should look first. The agent's code does not change.

npx @seal-dev/mcp-proxy --config seal.config.ts

No account, no network, no card.

Quickstart

Write seal.config.json (or .ts, .mjs, .js, .cjs) next to your project:

{
  "policies": {
    "refunds": {
      "match": {},
      "rules": [
        { "when": [{ "field": "payload.amount", "op": "lt", "value": 100 }], "then": "allow" },
        { "when": [{ "field": "payload.amount", "op": "gt", "value": 10000 }], "then": "deny" },
        { "when": [], "then": "require_approval" }
      ],
      "approvers": { "channel": "dashboard", "timeoutSeconds": 300 }
    }
  },
  "auditPath": "seal-audit.jsonl",
  "upstreams": [
    {
      "name": "payments",
      "transport": "stdio",
      "command": "npx",
      "args": ["-y", "@your/payments-mcp"],
      "policyKey": "refunds"
    }
  ]
}

Point your MCP client at npx @seal-dev/mcp-proxy instead of the upstream server. Small refunds go straight through. Large ones are refused. Everything in between stops and asks you in the terminal:

────────────────────────────────────────────────────────────
Approve tool call `create_refund`?

Arguments:
{
  "amount": 4200
}

[a] approve   [r] reject   (anything else rejects)
>

Every evaluation, request and decision is appended to seal-audit.jsonl, hash-chained:

npx @seal-dev/mcp-proxy --verify seal-audit.jsonl
# audit chain intact — 6 events in seal-audit.jsonl

Editing or deleting a record breaks the chain and --verify exits non-zero.

Policies

rules is an ordered list; the first rule whose when conditions all pass wins. A rule with an empty when always passes — use it as the catch-all. then is one of:

| Verdict | Effect | | ------------------ | --------------------------------------------------------------- | | allow | Forwarded upstream immediately | | deny | Refused immediately; the call never reaches the upstream | | require_approval | Paused until a human decides, or until timeoutSeconds elapses |

If no rule matches, or the tool resolves to no policy at all, the verdict is require_approval. Conditions read a dot-path against the request — payload.amount, context.tier, action, agent — with eq, neq, gt, lt, contains and regex. A missing field never passes.

Per-tool policy — policyKey may be one string for every call the upstream relays, or a map:

"policyKey": {
  "default": "read-only",
  "overrides": { "git.push": "destructive", "git.reset": "destructive" }
}

A tool that resolves to no policy key fails closed. A map with neither a default nor any override is rejected at boot.

Fail-closed, by construction

  • An unknown, missing or malformed input yields the safe verdict, never the permissive one.
  • Only a approves at the prompt. Every other line, including an empty one, rejects.
  • A decision that never arrives expires, and an expired request is refused.
  • A config with no policies, or no controlling terminal to answer on, refuses to start rather than gating every call until it times out.
  • Tool descriptions from the upstream are untrusted (tool poisoning) and are sanitised before display. So are the arguments shown at the prompt: control, bidi and zero-width characters are stripped so a payload cannot move the cursor, repaint the prompt, or hide text behind an escape sequence.

How the approval reaches you

  1. Inline elicitation — if your MCP client advertised the elicitation capability on initialize, the proxy asks through the client itself, in-session. Form mode requests no input fields, so no sensitive data is ever solicited.
  2. The controlling terminal — otherwise the prompt above. In stdio mode stdin and stdout are the MCP channel, so the proxy opens the controlling terminal (/dev/tty, CONIN$ on Windows) rather than corrupting the protocol.
  3. Neither — if there is no terminal and the client cannot elicit, the proxy says so at startup instead of hanging. That is the point where the hosted product starts to matter.

Running it

npx @seal-dev/mcp-proxy                        # auto-discovers ./seal.config.*
npx @seal-dev/mcp-proxy --upstream payments    # pick one when the config declares several
npx @seal-dev/mcp-proxy --config ./ci.config.ts --port 9000
npx @seal-dev/mcp-proxy --verify seal-audit.jsonl

One process fronts one upstream. Client-facing transports are stdio (the client launches the proxy) and Streamable HTTP with per-session routing; upstreams may be stdio or Streamable HTTP. A malformed config fails to boot with a specific error — it never starts permissive.

Upstream credentials are named by env var (envPassthrough, auth.valueEnv) and resolved from the proxy's own environment. They are never sourced from the calling agent, never stored and never logged.

When one machine stops being enough

Local mode is one developer at one terminal, and it is complete for that: policy, approval, audit, verification. It has no server, so there are things it structurally cannot do — a second person approving, an agent running in CI with nobody at the terminal, an auditor who needs more than a JSONL file on your laptop.

Adding a seal block switches the same config to the hosted product at useseal.dev; approvals then arrive over Slack, email or the review dashboard, and the audit trail is hosted and exportable.

{
  "seal": { "apiUrl": "https://api.useseal.dev", "apiKeyEnv": "SEAL_API_KEY" },
  "asyncMode": "hold", // hold (default) blocks the call; pending returns an id to resume
  "upstreams": [...]
}

Nothing in this package is switched off when you do. mode is inferred from the config: a seal block means cloud, its absence means local, and declaring one while asserting the other is a validation error rather than a silent precedence rule. In cloud mode policies live in the dashboard, so a leftover local policies map is ignored — the proxy warns rather than letting you believe it is still in force.

Inline elicitation in cloud mode submits decisions to POST /v1/approvals/:id/decision, which needs the approvals:decide scope. Grant it only to a proxy that approves in-session; a create-only key (approvals:write) still covers the async paths.

What the relay guarantees

A raw JSON-RPC pump between two transports. Client capabilities propagate on initialize because the relay is transparent, and server→client requests (elicitation/create, sampling/createMessage) round-trip instead of being dropped — which is what separates a gateway from a one-way proxy. Session close is single and idempotent; send and transport errors tear the session down rather than hanging.

Known gap: relaying an upstream-initiated server→client request over Streamable HTTP still lacks the relatedRequestId association that the gate's own elicitation/create has. It works today over stdio and is proven transport-agnostically in tests.

Scripts

| Script | Purpose | | ---------------- | ---------------- | | pnpm build | Bundle with tsup | | pnpm typecheck | tsc -b | | pnpm test | Vitest |

Apache-2.0.