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

@three-ws/mcp-policy

v0.1.1

Published

The three.ws MCP tool policy: every tool's group and tier, read-only by default with opt-in financial groups, named confirm flags, and preview ids that must precede every irreversible call.

Readme

@three-ws/mcp-policy

One tool policy for every three.ws MCP server: read-only by default, money behind a named confirm flag and a fresh preview.

Every tool on every three.ws MCP server, hosted or stdio, is classified here by group (what it is about) and tier (what it can do to the caller). A session sees the read and write tiers by default. Financial tools, the ones that move funds or cannot be undone, stay hidden until the person opts in, and even then each call must carry a named confirm flag (confirm_payment: true, confirm_swap: true, ...) plus a preview id issued by the matching preview tool within the last ten minutes, bound to the same arguments.

The hosted servers (through api/_mcp/policy.js), the /api/mcp-policy allowlist builder, and the stdio packages all read the same table, so a rule changes in one place.

Install

The package is a workspace of this monorepo (packages/mcp-policy) and is not on the npm registry yet. A root npm install links it, after which import '@three-ws/mcp-policy' resolves from any file in the repo:

npm install
node -e "import('@three-ws/mcp-policy').then((p) => console.log(Object.keys(p.POLICY).length, 'servers'))"

Zero runtime dependencies. Node 20+. ESM only. Its package.json is already set for a public release (publishConfig.access: public, files: src, README.md, LICENSE), so publishing is npm publish -w packages/mcp-policy, an owner-approved step.

The model

Tiers

| Tier | Meaning | Default | |---|---|---| | read | Changes nothing. | on | | write | Changes account state, and a follow-up call can undo it. | on | | financial | Moves funds or cannot be undone. | off; every call needs its confirm flag and a fresh preview id |

Groups

agents, chat, runs, skills, trading, orders, perps, lending, predictions, launch, marketplace, cards, mail, x402, wallet, billing, intelligence, integrations, account, allowlist, assets, gateway, sandbox, utility. Each has a label and a one-line summary in GROUPS (exported), written for the person deciding whether to turn it on.

Financial tools

A financial row names three things:

  • the confirm flag the call must set to literally true (one of CONFIRM_FLAGS, e.g. confirm_transfer, confirm_payment, confirm_run);
  • the preview tool that must run first (e.g. inspect_endpoint before pay_and_call);
  • the bound arguments that must be identical between preview and call, so a quote for one destination can never authorize a payment to another.

The preview id is quote_id when the preview tool is a quote, preview_id otherwise. It lives for PREVIEW_TTL_MS (10 minutes), belongs to the caller who previewed, and is burned the moment the financial call it authorized succeeds.

A tool with no row in the table is treated as financial with no group: it is hidden unless named explicitly, so an unclassified tool can never slip into a default session.

The spec grammar

One grammar is read from the X-Three-Tools header, the tools query parameter, the THREE_WS_TOOLS env var of a stdio server, and the saved settings. Tokens are comma- or space-separated and applied left to right:

| Token | Effect | |---|---| | default | read + write on, financial off (the implicit base) | | all / none / readonly | every tier / nothing / only read | | financial, -write | turn a tier on or off | | trading, -chat | turn a whole group on (every tier) or off | | swap_execute, -forget | turn one tool on or off |

Precedence is tool over group over tier: -trading,swap_quote hides the trading group except swap_quote. A spec made only of plain tool names is an exact allow list; to add a tool to the defaults instead, lead with a base: default,pay_and_call.

Hosted servers resolve a session in this order, first present wins: the client's X-Three-Allowed-Tools list, X-Three-Tools, the tools query parameter, the per-key setting layered over the account setting, then the default. A stdio process resolves THREE_WS_ALLOWED_TOOLS, then THREE_WS_TOOLS, then the selection npx three-ws tools saved under tools["three-ws-<name>"] in ~/.config/three-ws/credentials.json, then the default.

Quick start

The full preview, confirm, burn cycle against the real x402-mcp table:

import { createPolicy, enablementFromSpec, describeEnablement } from '@three-ws/mcp-policy';

const policy = createPolicy({ serverId: 'x402-mcp' });
const en = enablementFromSpec('default,x402', 'example'); // the whole x402 group, financial tier included
console.log(describeEnablement(en));

const call = { url: 'https://three.ws/api/x402/example', method: 'GET' };

// 1. A financial call with no confirm flag is refused with a tool result the model can read.
const refused = await policy.beforeCall({ name: 'pay_and_call', args: call, en, principal: 'me' });
console.log(refused.ok, refused.result.structuredContent.reason); // false confirmation_required

// 2. The preview tool runs; afterCall stamps a preview id onto its result.
const preview = await policy.afterCall({
	name: 'inspect_endpoint',
	args: call,
	principal: 'me',
	result: { content: [{ type: 'text', text: 'price: $0.01 per call' }] },
});
const previewId = preview._meta['three.ws/preview'].preview_id;

// 3. The confirmed call with the fresh preview id passes, policy-only arguments stripped.
const gate = await policy.beforeCall({
	name: 'pay_and_call',
	args: { ...call, preview_id: previewId, confirm_payment: true },
	en,
	principal: 'me',
});
console.log(gate.ok, gate.args); // true { url: ..., method: 'GET' }

// 4. After it succeeds the preview id is burned, so it can never authorize a second payment.
await policy.afterCall({ name: 'pay_and_call', args: call, principal: 'me', result: { content: [] }, preview: gate.preview });
const replay = await policy.beforeCall({ name: 'pay_and_call', args: { ...call, preview_id: previewId, confirm_payment: true }, en, principal: 'me' });
console.log(replay.ok, replay.result.structuredContent.reason); // false preview_unknown

Refusals are MCP tool results with isError: true and a structuredContent reason (tool_disabled, confirmation_required, preview_required, preview_unknown, preview_stale, preview_mismatch), never JSON-RPC errors: a model reads a tool result and acts on it, while most clients show a protocol error only to the person.

Wiring a stdio server

High-level McpServer

Call applyPolicy before the registerTool loop. It wraps registerTool, so a disabled tool is never registered (calling it anyway explains how to turn it on), a financial tool gains its confirm flag and preview-id arguments in its zod schema, and a preview tool stamps a preview id onto its result.

import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import { z } from 'zod';
import { applyPolicy } from '@three-ws/mcp-policy/stdio';

const server = new McpServer({ name: 'x402-mcp', version: '1.0.0' });
const { enablement, hidden } = applyPolicy(server, { serverId: 'x402-mcp', z });

server.registerTool(
	'inspect_endpoint',
	{ description: 'Price an x402 endpoint without paying.', inputSchema: { url: z.string().url(), method: z.string().default('GET') } },
	async ({ url, method }) => ({ content: [{ type: 'text', text: `${method} ${url}: $0.01 per call` }] }),
);
server.registerTool(
	'pay_and_call',
	{ description: 'Pay an x402 endpoint and return its response.', inputSchema: { url: z.string().url(), method: z.string().default('GET') } },
	async ({ url }) => ({ content: [{ type: 'text', text: `paid ${url}` }] }),
);

console.error(`policy source: ${enablement.source}; hidden: ${[...hidden].join(', ') || 'none'}`);
if (process.argv.includes('--serve')) await server.connect(new StdioServerTransport());

Run it bare and it prints policy source: default; hidden: pay_and_call. Run it with THREE_WS_TOOLS=default,x402 and it prints policy source: env; hidden: none. Pass the package's own z so the added arguments match its zod version.

Low-level Server with JSON-Schema tools

import { createStdioPolicy } from '@three-ws/mcp-policy/stdio';

const gate = createStdioPolicy('x402-mcp');
// ListTools handler:  return { tools: gate.listTools(TOOLS) };
// CallTool handler:   return gate.callTool(name, args, (cleanArgs) => runTool(name, cleanArgs));

listTools drops disabled tools, appends the financial rule to each financial tool's description, adds the confirm and preview arguments to its schema, and stamps _meta['three.ws/policy'] (group, tier, confirm flag, preview tool, preview argument) on every tool.

API

@three-ws/mcp-policy

| Export | What it is | |---|---| | GROUPS, GROUP_IDS | Every group { id, label, summary }, in settings-page order, and just the ids. | | TIERS, DEFAULT_TIERS | ['read', 'write', 'financial'] and the tiers a fresh key sees (['read', 'write']). | | CONFIRM_FLAGS, PREVIEW_ARGS, PREVIEW_TTL_MS | The confirm flag vocabulary, ['quote_id', 'preview_id'], and the 10-minute preview lifetime. | | POLICY | The table: { [serverId]: { [toolName]: { group, tier, confirmFlag?, previewTool?, bind?, ownsPreview? } } }. The single source of truth. | | SERVERS | Human names, endpoints and transport (remote or stdio) for every server id in POLICY. | | createPolicy({ serverId, table?, store?, settingsUrl?, now? }) | The runtime for one server: listTools(tools, en), beforeCall({ name, args, en, principal, ownArgs }), afterCall({ name, args, principal, result, preview }), enabled(name, en), entry(name), disabledResult, policyMeta, financialNote. Throws when serverId has no table. | | createMemoryPreviewStore() | An in-process { put, get, del } preview store, right for a single stdio process. The hosted servers pass a Redis-backed store instead so a quote issued on one instance is honored on another. | | normalizeEntry(name, row) | Expand a terse table row into the shape every consumer reads. | | parseSpec(spec) | Spec string or array to { tokens, invalid }; a typo lands in invalid instead of being silently dropped. | | enablementFromSpec(spec, source) | A spec to an enablement, honoring the exact-allow-list rule. | | enablementFromTokens(tokens, source), defaultEnablement(), allowListEnablement(names, source?) | Build an enablement directly. | | settingsToTokens(settings) | Saved { groups: { id: bool }, tools: { name: bool } } settings to tokens; unknown groups are dropped. | | resolveEnablement({ allowedTools, headerSpec, querySpec, keySettings, accountSettings }) | The hosted resolution order above, in one call. | | enablementFromSelection(sel, source?), selectionToTokens(sel), specFromSelection(sel) | Convert a three-ws tools selection ({ tiers, allow, deny }) to an enablement, tokens, or an X-Three-Tools spec. | | isToolEnabled(name, entry, en) | Is this tool on for the session. | | describeEnablement(en) | A compact, serializable view for logs and the settings API. | | SETTINGS_URL, CLI_COMMAND | https://three.ws/settings/mcp-tools and npx three-ws tools, quoted in every refusal. |

@three-ws/mcp-policy/stdio

| Export | What it is | |---|---| | applyPolicy(server, { serverId, z, env?, enablement?, store? }) | Put the policy in front of every tool an McpServer registers from now on. Returns { policy, enablement, hidden }. | | createStdioPolicy(serverId, { env?, enablement?, store? }) | The same policy for a low-level Server: { policy, enablement, listTools(tools), callTool(name, args, run, ownArgs?) }. | | resolveStdioEnablement(serverId, env?) | The stdio resolution order above. | | cliSlug(serverId) | The CLI's config key for a package: x402-mcp becomes three-ws-x402. |

Adding a tool

Add a row for it under its server in src/table.js with the row helpers at the top of the file: r(group), w(group), f(group, flag, previewTool, bind), or own(group, flag, previewTool) for a tool that verifies its own preview id. A financial tool must name the preview tool a person sees before it runs. The audit that checks each tool's annotations against its handler is npm run audit:mcp-safety (docs/mcp-safety.md).

Related

License

Apache-2.0. See LICENSE.