New:Socket for Asana Is Now Available.Learn more
Get Started

@grantor/mcp

Package Overview
Dependencies
Maintainers
1
Versions
9
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@grantor/mcp - npm Package Compare versions

Comparing version
0.1.6
to
0.1.7
+110
src/verbs/demo.js
// demo.js — the first denial, in your own terminal, ~30 seconds.
// Wraps the bundled toy server through the REAL wrap machinery (grant mint,
// relay, enforcement, chain-read revocation check — nothing simulated except
// the client), lets one allowed call through, then watches `delete_everything`
// get refused before the server ever sees it. State lives in a temp file so
// the demo never touches the user's broker state.
import { spawn } from "node:child_process";
import { mkdtempSync, rmSync } from "node:fs";
import { tmpdir } from "node:os";
import { join, dirname } from "node:path";
import { fileURLToPath } from "node:url";
import { PassThrough } from "node:stream";
import { wrapVerb } from "../wrap/run.js";
import { lineSplit } from "../wrap/proxy.js";
import { loadState } from "../state.js";
const TOY = join(dirname(fileURLToPath(import.meta.url)), "..", "wrap", "toy-server.js");
export async function demoVerb(ctx, _args = {}, deps = {}) {
const say = deps.say ?? ((m) => console.error(m));
const tmp = mkdtempSync(join(tmpdir(), "grantor-mcp-demo-"));
const statePath = join(tmp, "state.json");
const demoCtx = { ...ctx, statePath, state: loadState(statePath) };
// The scripted "agent": three requests through the real relay.
const clientIn = new PassThrough();
const clientOut = new PassThrough();
const responses = new Map(); // id -> resolve
lineSplit(clientOut, (line) => {
try {
const msg = JSON.parse(line);
if (msg.id !== undefined && responses.has(msg.id)) {
responses.get(msg.id)(msg);
responses.delete(msg.id);
}
} catch { /* ignore non-JSON */ }
});
const request = (msg, timeoutMs = 30_000) =>
new Promise((resolve, reject) => {
const t = setTimeout(() => reject(new Error(`demo: no response to ${msg.method} within ${timeoutMs}ms`)), timeoutMs);
responses.set(msg.id, (m) => { clearTimeout(t); resolve(m); });
clientIn.write(`${JSON.stringify(msg)}\n`);
});
say("grantor-mcp demo — the first denial, live in this terminal.");
say("");
say("Wrapping a toy MCP server behind a grant for ONE tool: `search`.");
say("The server also exposes `delete_everything`. Watch what happens.");
say("");
// Capture the toy child through the injectable spawn seam: a wrapped MCP
// server normally outlives the script (the relay never closes the child's
// stdin — servers are long-lived), so the demo must kill it explicitly or
// wrapVerb's promise never resolves.
let toyProc;
const spawnCapture = (...a) => { toyProc = spawn(...a); return toyProc; };
const done = wrapVerb(
demoCtx,
{ tools: ["search"], max_uses: 5, ttl_secs: 600, childCmd: [process.execPath, TOY] },
{
io: { stdin: clientIn, stdout: clientOut },
spawnImpl: spawnCapture,
checkImpl: deps.checkImpl, // default: the real chain-reading checkVerb
log: (m) => say(` ${m}`),
},
);
let exitCode = 0;
try {
await request({ jsonrpc: "2.0", id: 1, method: "initialize", params: { protocolVersion: "2025-06-18", capabilities: {}, clientInfo: { name: "demo-agent", version: "0" } } });
clientIn.write(`${JSON.stringify({ jsonrpc: "2.0", method: "notifications/initialized" })}\n`);
say("");
say('The "agent" calls the tool it was granted:');
const ok = await request({ jsonrpc: "2.0", id: 2, method: "tools/call", params: { name: "search", arguments: { query: "anything" } } });
say(` → ALLOWED, served by the wrapped server: ${ok.result?.content?.[0]?.text ?? JSON.stringify(ok.result ?? ok.error)}`);
say("");
say("Now it tries the one it was NOT granted:");
const denied = await request({ jsonrpc: "2.0", id: 3, method: "tools/call", params: { name: "delete_everything", arguments: {} } });
const deniedOk = denied.result?.isError === true;
say("");
say(deniedOk
? "That box is the product: the grant said search-only, so the broker"
: "UNEXPECTED: the call was not denied — this is a bug, please report it.");
if (deniedOk) {
say("answered the call itself. The server never saw it — there is no");
say("prompt to talk your way past.");
say("");
say("Next, on your own server:");
say(" npx -y @grantor/mcp tools -- <your MCP server command>");
say(" (lists its tools and suggests a ready-to-paste wrap command)");
say("Guide: https://chaingrantor.com/docs/guide/first-denial.html");
} else {
exitCode = 1;
}
} catch (e) {
say(`demo failed: ${e?.message ?? e}`);
exitCode = 1;
} finally {
clientIn.end();
toyProc?.stdin?.end(); // toy exits on ITS stdin end → wrapVerb resolves
const timeout = setTimeout(() => toyProc?.kill("SIGKILL"), 3000);
await done.catch(() => {});
clearTimeout(timeout);
rmSync(tmp, { recursive: true, force: true });
}
process.exitCode = exitCode;
return undefined; // human output only — no JSON result line
}
// tools.js — inspect an MCP server and SUGGEST a wrap command. This is the
// honest form of a "read-only preset": the broker's grant grammar names
// tools explicitly (no wildcards, on purpose — monotone narrowing needs
// concrete names), so a preset cannot know a server's tool names in advance.
// This verb discovers them, splits read-like from write-like BY NAME
// HEURISTIC, and prints a ready-to-paste suggestion the user reviews —
// informed consent, not magic granting. Needs no broker config/state/chain.
import { spawn } from "node:child_process";
import { lineSplit } from "../wrap/proxy.js";
// Name-heuristic ONLY — the suggestion says so. Write-ish wins over read-ish
// (a `delete_read_marker` is write-ish).
const WRITEY = /(write|creat|delet|remov|updat|move|renam|exec|run|shell|command|spawn|send|post|put|patch|insert|drop|kill|install|deploy|push|publish|set_|^set$|upload|edit|append|mkdir|rm_|^rm$|clear|reset|revoke|approve|transfer|sign)/i;
export function splitByRisk(names) {
const readLike = [], writeLike = [];
for (const n of names) (WRITEY.test(n) ? writeLike : readLike).push(n);
return { readLike, writeLike };
}
export async function toolsVerb({ childCmd }, deps = {}) {
const say = deps.say ?? ((m) => console.log(m));
const spawnImpl = deps.spawnImpl ?? spawn;
if (!Array.isArray(childCmd) || childCmd.length === 0) {
throw new Error("grantor-mcp tools: missing server command — usage: grantor-mcp tools -- <cmd> [args...]");
}
const proc = spawnImpl(childCmd[0], childCmd.slice(1), { stdio: ["pipe", "pipe", "inherit"] });
proc.stdin?.on("error", () => {});
const responses = new Map();
lineSplit(proc.stdout, (line) => {
try {
const msg = JSON.parse(line);
if (msg.id !== undefined && responses.has(msg.id)) {
responses.get(msg.id)(msg);
responses.delete(msg.id);
}
} catch { /* ignore */ }
});
const request = (msg, timeoutMs = 20_000) =>
new Promise((resolve, reject) => {
const t = setTimeout(() => reject(new Error(`grantor-mcp tools: no response to ${msg.method} within ${timeoutMs}ms — is this an MCP stdio server?`)), timeoutMs);
responses.set(msg.id, (m) => { clearTimeout(t); resolve(m); });
proc.stdin.write(`${JSON.stringify(msg)}\n`);
});
try {
await request({ jsonrpc: "2.0", id: 1, method: "initialize", params: { protocolVersion: "2025-06-18", capabilities: {}, clientInfo: { name: "grantor-mcp-tools", version: "0" } } });
proc.stdin.write(`${JSON.stringify({ jsonrpc: "2.0", method: "notifications/initialized" })}\n`);
const listed = await request({ jsonrpc: "2.0", id: 2, method: "tools/list", params: {} });
const names = (listed.result?.tools ?? []).map((t) => t.name);
const { readLike, writeLike } = splitByRisk(names);
say(`tools exposed by: ${childCmd.join(" ")}`);
for (const n of names) say(` ${WRITEY.test(n) ? "✎" : "·"} ${n}`);
say("");
if (readLike.length === 0) {
say("no read-like tool names found — pick the grant yourself:");
say(` npx -y @grantor/mcp wrap --tools <comma,separated> --max-uses 50 --ttl-secs 3600 -- ${childCmd.join(" ")}`);
} else {
say("suggested wrap (read-like tools only — a NAME heuristic, review it):");
say(` npx -y @grantor/mcp wrap --tools ${readLike.join(",")} --max-uses 50 --ttl-secs 3600 -- ${childCmd.join(" ")}`);
if (writeLike.length > 0) say(`held back as write-like: ${writeLike.join(", ")}`);
}
say("every tool NOT in --tools is denied before the server sees the call.");
return undefined;
} finally {
proc.kill("SIGTERM");
}
}
#!/usr/bin/env node
// toy-server.js — the demo's victim: a minimal MCP stdio server with one
// harmless tool and one scary-looking one. It exists so `grantor-mcp demo`
// can produce a real, first-person denial without touching anything real —
// `delete_everything` deletes nothing anywhere, but through the wrap the
// point is sharper: the broker denies it BEFORE this process even sees it.
// Standalone on purpose (no imports from the kit): spawned as a child.
process.stdin.setEncoding("utf8");
let buf = "";
const TOOLS = [
{
name: "search",
description: "Search the toy corpus (harmless).",
inputSchema: { type: "object", properties: { query: { type: "string" } } },
},
{
name: "delete_everything",
description: "Delete all the things (toy — deletes nothing, anywhere).",
inputSchema: { type: "object", properties: {} },
},
];
const send = (obj) => process.stdout.write(`${JSON.stringify(obj)}\n`);
function handle(msg) {
if (msg.method === "initialize") {
send({
jsonrpc: "2.0", id: msg.id,
result: {
protocolVersion: msg.params?.protocolVersion ?? "2025-06-18",
capabilities: { tools: {} },
serverInfo: { name: "grantor-toy-server", version: "0.0.1" },
},
});
return;
}
if (msg.method === "tools/list") {
send({ jsonrpc: "2.0", id: msg.id, result: { tools: TOOLS } });
return;
}
if (msg.method === "tools/call") {
const name = msg.params?.name;
if (name === "search") {
const q = msg.params?.arguments?.query ?? "";
send({
jsonrpc: "2.0", id: msg.id,
result: { content: [{ type: "text", text: `3 toy results for "${q}"` }] },
});
return;
}
if (name === "delete_everything") {
send({
jsonrpc: "2.0", id: msg.id,
result: { content: [{ type: "text", text: "everything deleted (toy — nothing actually happened)" }] },
});
return;
}
send({
jsonrpc: "2.0", id: msg.id,
error: { code: -32602, message: `unknown tool ${name}` },
});
return;
}
// notifications and anything else: ignore
if (msg.id !== undefined) {
send({ jsonrpc: "2.0", id: msg.id, error: { code: -32601, message: `unhandled ${msg.method}` } });
}
}
process.stdin.on("data", (chunk) => {
buf += chunk;
let i;
while ((i = buf.indexOf("\n")) >= 0) {
const line = buf.slice(0, i).trim();
buf = buf.slice(i + 1);
if (!line) continue;
try { handle(JSON.parse(line)); } catch { /* toy: drop garbage */ }
}
});
process.stdin.on("end", () => process.exit(0));
+1
-1
{
"name": "@grantor/mcp",
"mcpName": "com.chaingrantor/grantor-mcp",
"version": "0.1.6",
"version": "0.1.7",
"description": "Grantor permission broker for multi-agent frameworks \u2014 grant, delegate, check, revoke bounded capabilities over MCP, and wrap any stdio MCP server with enforced permissions. No authorization server anywhere.",

@@ -6,0 +6,0 @@ "license": "SEE LICENSE IN LICENSE",

@@ -29,2 +29,21 @@ # @grantor/mcp

## 30 seconds: watch a denial happen
```sh
npx -y @grantor/mcp demo
```
Wraps a bundled toy server behind a grant for one tool, lets the granted
call through, and shows `delete_everything` getting refused **before the
server sees it** — enforcement is real (broker + on-chain revocation read),
only the "agent" is scripted. Then inspect your own server and get a
ready-to-paste wrap suggestion:
```sh
npx -y @grantor/mcp tools -- <your MCP server command>
```
The suggestion grants read-like tool names only (a name heuristic — review
it before trusting it); everything outside `--tools` is denied.
## 60-second first run

@@ -255,2 +274,4 @@

node src/cli.js status
node src/cli.js demo # the first denial, scripted, in this terminal
node src/cli.js tools -- npx some-mcp-server # list a server's tools + suggest a wrap
```

@@ -257,0 +278,0 @@

@@ -22,2 +22,4 @@ #!/usr/bin/env node

import { wrapVerb } from "./wrap/run.js";
import { demoVerb } from "./verbs/demo.js";
import { toolsVerb } from "./verbs/tools.js";

@@ -106,2 +108,18 @@ export const VERBS = { grant: grantVerb, delegate: delegateVerb, check: checkVerb, revoke: revokeVerb, status: statusVerb };

}
if (verb === "demo") {
// The first denial, live in this terminal — wraps the bundled toy server
// through the real machinery with its own temp state. Human output only.
const ctx = buildCtx(configPath);
return demoVerb(ctx);
}
if (verb === "tools") {
// Inspect an MCP server + suggest a wrap command. Same `--` contract as
// wrap; deliberately needs NO broker config/state/chain (works before
// any setup at all).
const sep = rest.indexOf("--");
if (sep === -1) {
throw new Error("grantor-mcp tools: missing `--` separator — usage: grantor-mcp tools -- <cmd> [args...]");
}
return toolsVerb({ childCmd: rest.slice(sep + 1) });
}
if (verb === "wrap") {

@@ -133,3 +151,3 @@ // wrap parses its OWN argv: everything after the first bare `--` is the

throw new Error(
`grantor-mcp: unknown verb "${verb ?? ""}" — expected one of: serve, ${Object.keys(VERBS).join(", ")}, setup-sandbox, wrap`,
`grantor-mcp: unknown verb "${verb ?? ""}" — expected one of: serve, ${Object.keys(VERBS).join(", ")}, setup-sandbox, wrap, demo, tools`,
);

@@ -136,0 +154,0 @@ }

@@ -66,2 +66,33 @@ // protocol.js — pure classification + rewrite helpers for the wrap proxy.

/** The denial ARTIFACT — the human-facing boxed record of a refusal, printed
* to stderr alongside (never instead of) the greppable `DENY …` log line.
* Pure: a function of what the relay already holds. This is deliberately the
* product's aha moment made visible — what was asked, what the grant allows,
* and the line that carries the thesis. */
export function denyArtifact({ tool, method, decision = {}, grants, childId, learnUrl = "https://chaingrantor.com/docs/guide/first-denial.html" }) {
const allowed = grantedToolSummary(grants);
const requested = method ?? tool ?? "(unknown)";
const rows = [
["requested", requested],
["grant allows", allowed.length ? allowed.join(", ") : "(no tools)"],
["refused as", (decision.code ?? "Denied") + (decision.reason ? ` — ${decision.reason}` : "")],
];
const label = Math.max(...rows.map(([k]) => k.length));
const body = rows.map(([k, v]) => ` ${k.padEnd(label)} ${v}`);
const tail = [
"",
" Denied by the grant, not by a prompt — the server never saw the call.",
...(childId ? [` revoke everything: grantor-mcp revoke --child ${childId}`] : []),
` what just happened: ${learnUrl}`,
];
const lines = [...body, ...tail];
const width = Math.min(76, Math.max(...lines.map((l) => l.length), 28) + 2);
const clip = (l) => (l.length > width ? l.slice(0, width - 1) + "…" : l);
return [
`┌─ DENIED ${"─".repeat(Math.max(1, width - 9))}┐`,
...lines.map((l) => `│${clip(l).padEnd(width)}│`),
`└${"─".repeat(width)}┘`,
].join("\n");
}
export function toolDenyResponse(id, deny) {

@@ -68,0 +99,0 @@ return {

@@ -9,3 +9,3 @@ // proxy.js — the wrap relay: four streams, one enforcement seam.

stripGatedCapabilities, toolDenyResponse, resourceDenyResponse,
batchRefusalResponse,
batchRefusalResponse, denyArtifact,
} from "./protocol.js";

@@ -37,3 +37,9 @@

enforce, grants, allowResources = false, verbose = false, log = () => {},
childId, quiet = false,
}) {
// The boxed artifact rides the same stderr log seam as the DENY line it
// accompanies (never replaces — that line stays greppable/parseable).
const artifact = (fields) => {
if (!quiet) log(`\n${denyArtifact({ ...fields, grants, childId })}`);
};
const pending = new Map(); // request id -> method, ONLY for forwarded REWRITE_METHODS

@@ -74,2 +80,3 @@ const send = (stream, obj) => stream.write(`${JSON.stringify(obj)}\n`);

log(`DENY tools/call ${cls.tool}: ${code} (enforce threw)`);
artifact({ tool: cls.tool, decision: { code, reason } });
if (msg.id !== undefined) send(clientOut, toolDenyResponse(msg.id, { allow: false, code, reason }));

@@ -83,2 +90,3 @@ return;

log(`DENY tools/call ${cls.tool}: ${decision.code}`);
artifact({ tool: cls.tool, decision });
// Answered locally; the child NEVER sees a denied request, so its

@@ -85,0 +93,0 @@ // id cannot collide with anything we later forward.

@@ -66,2 +66,4 @@ // run.js — the wrap runner: validate, (optionally) inline-grant, fail fast,

verbose: !!opts.verbose,
quiet: !!opts.quiet,
childId,
log,

@@ -68,0 +70,0 @@ });