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

mcp-server-secure-wayleave

v0.0.1

Published

Stop your MCP server being drained by a runaway agent. Per-tool rate limits, concurrency caps, spend ceilings and repeat-loop detection, in one line. Zero dependencies.

Readme

mcp-server-secure-wayleave

Stop your MCP server being drained by a runaway agent.

npm install mcp-server-secure-wayleave
# or, the same package under the Wayleave scope:
npm install @wayleave/mcp

Both names are published from one source at the same version and are byte-identical. Install one of them — two copies would keep two sets of counters, and neither would see the other's calls.

import { McpGuard } from 'mcp-server-secure-wayleave';

const guard = new McpGuard({ dailyBudgetUsd: 5 });

await server.connect(guard.protect(transport));   // ← the only line you add

Zero dependencies. Works on stdio and Streamable HTTP. Nothing to sign up for.

The problem this exists for

You publish an MCP server. A tool in it calls a paid API, or an LLM, or a database that is not free to scan. An agent connects, decides the answer it got is not the answer it wanted, and calls the same tool again. And again.

Nothing here is malicious and no individual call is unreasonable. Each one is well under any per-minute limit you would think to set. The loop runs for four hours while you are asleep and the bill is waiting when you wake up.

That is the failure this package prevents. Not a DDoS — a polite agent stuck in a loop, spending your money one perfectly ordinary call at a time.

What it does

| | | |---|---| | Rate limits | Per tool, per client, on a sliding window | | Concurrency cap | How many tool calls may run at once | | Spend ceiling | Declare what a tool costs; set a hard limit it cannot cross | | Loop detection | Notices the same call repeated with identical arguments | | Argument size | Refuses payloads past a limit before anything else runs | | Block list | Turn a tool off without redeploying it |

A refused call never reaches your handler. That is the point, and it is what the integration tests check against a real MCP client: a limiter that returns a tidy error while the expensive handler runs anyway has protected nothing.

Configure it

Everything is optional. The defaults are chosen to be invisible to a working server and fatal to a loop.

const guard = new McpGuard({
  // Default for every tool: 60 calls/minute per client
  perMinute: 60,

  perTool: {
    search:    { perMinute: 30 },
    summarize: { perMinute: 10, perHour: 100, costUsd: 0.002 },
    reindex:   { perMinute: 1, maxConcurrent: 1 },
  },

  // Hard ceiling on declared costs, over a rolling 24h window.
  // Tools with no costUsd keep working after it is reached.
  dailyBudgetUsd: 5,

  maxConcurrent: 8,
  maxArgBytes: 256 * 1024,

  // 6 identical calls in a minute looks like a loop.
  // 'warn' (default) lets it through and flags it; 'deny' stops it.
  repeat: { identicalCalls: 6, windowMs: 60_000, action: 'warn' },

  block: ['dangerous_tool'],

  onDecision: (d) => {
    if (!d.allowed) console.warn(`[guard] refused ${d.tool}: ${d.reason}`);
    if (d.warning) console.warn(`[guard] ${d.repeatWarning}`);
  },
});

Why a refusal is a tool result, not a protocol error

A JSON-RPC error tends to be swallowed by the client's transport and retried, which makes a loop go faster. A result marked isError reaches the model as text it can read:

This exact call has been made 6 times in the last 60 seconds. The result will not change — stop calling "search" with these arguments and try a different approach.

Breaking the loop at the model works better than refusing it at the wire.

Why identical arguments

A rate limit answers how fast. It cannot answer is this going anywhere. An agent making progress varies its input; a stuck one does not. So the detector fingerprints (tool, arguments) with sorted keys — argument order cannot disguise a repeat — and counts how often that exact call recurs.

An agent paging through results with {page: 1}, {page: 2}, {page: 3} is never touched by it, at any speed.

Using it without the transport wrapper

If you have written your own server, call the guard directly:

const decision = guard.check({ tool: name, args, clientId: session });
if (!decision.allowed) return decision.toolResult;

try {
  return await runTool(name, args);
} finally {
  decision.release?.();   // only needed if you passed a requestKey
}

Concurrency is only counted when you pass a requestKey, because a slot has to be given back and only a caller holding an identifier can give it back. protect() supplies one for every request it sees.

What it does not do

Being clear about this is more useful than a longer feature list.

  • No bot detection. Wayleave's HTTP gate sorts web traffic into four lanes by checking RFC 9421 signatures and bot directories. That model does not transfer to MCP: over stdio there is no HTTP request to inspect, and MCP clients — Claude Desktop, Cursor, an SDK script — do not carry Web Bot Auth signatures. A "verified agent" lane would be permanently empty. There are also no humans on an MCP server, so "humans always free" has nothing to protect.
  • No authentication. If your server is remote, use MCP's own OAuth. This runs after that and cares only about what a client does, not who it is.
  • Counts are per process. Everything is in memory. Run three instances and each keeps its own counters. For one server — what most people run — the numbers are exact.
  • costUsd is what you declare. It does not measure anything. If you say a tool costs $0.002 and it costs $0.02, the ceiling is ten times higher than you think.

Identity

clientId defaults to transport.sessionId when the transport has one (Streamable HTTP gives each session its own), and to 'stdio' otherwise — because a stdio server has exactly one client by definition. Override it if you have something better, such as an authenticated subject:

await server.connect(guard.protect(transport, { clientId: user.id }));

Wayleave

Wayleave is how APIs charge AI agents for access — signature-verified identity, HTTP 402, and metering, at wayleave.dev. This package is the MCP piece of it and works entirely on its own: no account, no key, no calls home.

MIT.