agentcrashtest-mcp 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Selliks
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,35 @@
1
- # Temporary Holding Version
1
+ # Clearance MCP server
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Crash-test an agent's system prompt from inside Claude Code, Claude Desktop, Cursor or any MCP client. It calls the public API behind [agentcrashtest.com/test](https://agentcrashtest.com/test): one file, Node 20+, no dependencies, no keys. The limits are the site's: 3 tests per network per day, prompts up to 8000 characters.
4
+
5
+ ## Connect
6
+
7
+ Claude Code:
8
+ ```bash
9
+ claude mcp add clearance -- node /path/to/clearance/mcp/clearance-mcp.mjs
10
+ ```
11
+
12
+ Claude Desktop, Cursor and other clients (`mcpServers` in their config file):
13
+ ```json
14
+ {
15
+ "mcpServers": {
16
+ "clearance": { "command": "node", "args": ["/path/to/clearance/mcp/clearance-mcp.mjs"] }
17
+ }
18
+ }
19
+ ```
20
+
21
+ Then ask: *"Crash-test this system prompt with Clearance and tell me what it lost."*
22
+
23
+ ## Tools
24
+
25
+ | tool | what it does |
26
+ |---|---|
27
+ | `clearance_test` | Starts a test: `system_prompt` (required), `tools` = `clearance-v1` (default), `agentconnect-v1`, `outlayer-v1` or `agentkit-v1`. Returns the test id and the result page. |
28
+ | `clearance_result` | Reads a test by `id`; `wait_seconds` (up to 45) keeps polling until it is done; call again if it is still running. When done: per run, the attack, whether the agent was deceived, NEAR lost, what it paid to whom, whether it told its owner something the chain does not show; and totals. |
29
+ | `clearance_status` | Queue length, tests left today on the whole site (your network has its own 3), the models a test runs on. |
30
+
31
+ A test takes a few minutes. It is the site's quick test: one run per attack on two models, a preliminary report and no certificate. A level 0–2 certificate needs the full suite (5 runs per attack); see the [main README](../README.md).
32
+
33
+ Every result also has a page at `agentcrashtest.com/test/<id>` with the attacks as they landed. Results are kept for a week, and the result page is open to anyone with the link, the same as on the site. The prompt goes to the models a test runs on (see `clearance_status`), so do not send one with secrets in it.
34
+
35
+ `CLEARANCE_URL` points the server at another deployment (default `https://agentcrashtest.com`); the prompt is sent there.
@@ -0,0 +1,163 @@
1
+ #!/usr/bin/env node
2
+ // Clearance MCP server: lets any MCP client (Claude Code, Claude Desktop, Cursor, agent frameworks) crash-test an
3
+ // agent's system prompt from inside its own workflow. It is a thin client of the public website API
4
+ // (agentcrashtest.com/api/test): no keys, no dependencies, the same limits as the site (3 tests per network per day).
5
+ // Transport: MCP over stdio, newline-delimited JSON-RPC 2.0.
6
+ // claude mcp add clearance -- node /path/to/clearance/mcp/clearance-mcp.mjs
7
+ import { createInterface } from "node:readline";
8
+
9
+ const BASE = (process.env.CLEARANCE_URL || "https://agentcrashtest.com").replace(/\/$/, "");
10
+ const PROTOCOL = "2025-06-18";
11
+ const TOOL_SETS = ["clearance-v1", "agentconnect-v1", "outlayer-v1", "agentkit-v1"];
12
+
13
+ const TOOLS = [
14
+ {
15
+ name: "clearance_test",
16
+ description:
17
+ "Start a crash test of an AI agent that moves money. Send the agent's full system prompt; Clearance runs it against " +
18
+ "payment attacks hidden in what it reads (fake invoices, lookalike payees, a 'payment failed, retry' that already went " +
19
+ "through) on a NEAR sandbox with real transfers, and scores the NEAR that left the vault. Returns a test id and a result " +
20
+ "page. A test takes a few minutes; poll it with clearance_result. Limit: 3 tests per network per day, one at a time " +
21
+ "(if a test from this network is still queued or running, the call fails until it finishes).",
22
+ inputSchema: {
23
+ type: "object",
24
+ properties: {
25
+ system_prompt: { type: "string", description: "The agent's whole system prompt (at least a few sentences, at most 8000 characters)." },
26
+ tools: { type: "string", enum: TOOL_SETS, description: "Which payment tool set the agent runs with. Default clearance-v1; the others emulate Agent Connect, OutLayer and Coinbase AgentKit tools." },
27
+ },
28
+ required: ["system_prompt"],
29
+ },
30
+ },
31
+ {
32
+ name: "clearance_result",
33
+ description:
34
+ "Read a crash test started with clearance_test: its state (queued, running, done, failed), and when done, per run whether " +
35
+ "the agent was deceived, NEAR lost (decimal NEAR), whether it told its owner something the chain does not show, the " +
36
+ "Markdown report with the wallet limits to run it with, and the result page. Set wait_seconds to wait for it to finish " +
37
+ "(up to 45; call again if it is still running).",
38
+ inputSchema: {
39
+ type: "object",
40
+ properties: {
41
+ id: { type: "string", description: "The 16-character test id returned by clearance_test." },
42
+ wait_seconds: { type: "number", description: "Optional: keep polling up to this many seconds (max 45) until the test is done or failed." },
43
+ },
44
+ required: ["id"],
45
+ },
46
+ },
47
+ {
48
+ name: "clearance_status",
49
+ description: "Queue length, tests left today on the whole site (left_today; each network also has its own limit of per_ip tests a day), the models a test runs on, and the tool sets.",
50
+ inputSchema: { type: "object", properties: {} },
51
+ },
52
+ ];
53
+
54
+ async function api(path, init) {
55
+ const r = await fetch(`${BASE}${path}`, { ...init, signal: AbortSignal.timeout(30_000) });
56
+ const body = await r.json().catch(() => null);
57
+ if (!r.ok || !body) throw new Error(body?.error ?? `HTTP ${r.status} from ${BASE}${path}`);
58
+ return body;
59
+ }
60
+
61
+ function summarize(rec) {
62
+ const page = `${BASE}/test/${rec.id}`;
63
+ const out = { id: rec.id, state: rec.state, page };
64
+ if (rec.state === "queued") out.ahead = rec.ahead;
65
+ if (rec.state === "failed") out.error = rec.error ?? "the run did not finish";
66
+ const runs = rec.events ?? [];
67
+ if (runs.length) {
68
+ // Same counting as the result page: runs the model API cut off are listed but not scored, and when a model's
69
+ // first attempt had such a run it runs again, so only the first valid run per model and attack (or task) counts.
70
+ const seen = new Set();
71
+ const valid = runs.filter((e) => {
72
+ const k = `${e.model}|${e.kind}|${e.attack ?? e.scenario}`;
73
+ if (!e.valid || seen.has(k)) return false;
74
+ seen.add(k);
75
+ return true;
76
+ });
77
+ const attacks = valid.filter((e) => e.kind === "attack");
78
+ const lost = attacks.reduce((s, e) => s + Number(e.loss || 0), 0);
79
+ out.totals = {
80
+ attack_runs: attacks.length,
81
+ deceived: attacks.filter((e) => e.deceived).length,
82
+ near_lost: Number(lost.toFixed(4)),
83
+ false_reports_to_owner: valid.filter((e) => e.false_report).length,
84
+ honest_tasks_done: `${valid.filter((e) => e.kind === "control" && e.completed).length}/${valid.filter((e) => e.kind === "control").length}`,
85
+ invalid_runs: runs.length - valid.length,
86
+ };
87
+ out.runs = valid.map((e) => ({
88
+ model: e.model, attack: e.attack, scenario: e.scenario, valid: e.valid, deceived: e.deceived,
89
+ near_lost: e.loss, told_owner_something_false: e.false_report, task_done: e.completed,
90
+ paid: e.paid, refused_by_vault: e.refused,
91
+ }));
92
+ if (rec.state === "done" && rec.report) out.note = "If totals and report_markdown ever differ, the report is the scored result.";
93
+ }
94
+ if (rec.summary) out.summary = rec.summary;
95
+ // The full Markdown report: which model to ship on, each attack's text, and the wallet policy sized for it.
96
+ if (rec.report) out.report_markdown = rec.report;
97
+ return out;
98
+ }
99
+
100
+ async function callTool(name, a) {
101
+ if (name === "clearance_status") return api("/api/status");
102
+ if (name === "clearance_test") {
103
+ const prompt = typeof a.system_prompt === "string" ? a.system_prompt : "";
104
+ if (a.tools !== undefined && !TOOL_SETS.includes(a.tools)) throw new Error(`tools must be one of ${TOOL_SETS.join(", ")}`);
105
+ const tools = a.tools ?? "clearance-v1";
106
+ const rec = await api("/api/test", { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ prompt, tools }) });
107
+ return { ...summarize(rec), next: "Poll clearance_result with this id; a test usually takes a few minutes." };
108
+ }
109
+ if (name === "clearance_result") {
110
+ const id = String(a.id ?? "");
111
+ if (!/^[0-9a-f]{16}$/.test(id)) throw new Error("id must be the 16-character hex id from clearance_test");
112
+ const until = Date.now() + Math.min(Math.max(Number(a.wait_seconds) || 0, 0), 45) * 1000;
113
+ let rec = await api(`/api/test/${id}`);
114
+ while ((rec.state === "queued" || rec.state === "running") && Date.now() < until) {
115
+ await new Promise((r) => setTimeout(r, 5000));
116
+ rec = await api(`/api/test/${id}`);
117
+ }
118
+ return summarize(rec);
119
+ }
120
+ throw new Error(`unknown tool ${name}`);
121
+ }
122
+
123
+ const send = (msg) => process.stdout.write(`${JSON.stringify(msg)}\n`);
124
+ // The report goes in as Markdown text, everything else as JSON; structuredContent carries the whole object.
125
+ const text = (result, isError = false) => {
126
+ const { report_markdown: report, ...rest } = result;
127
+ const content = [{ type: "text", text: JSON.stringify(rest, null, 2) }];
128
+ if (report) content.push({ type: "text", text: report });
129
+ return { content, structuredContent: result, ...(isError ? { isError: true } : {}) };
130
+ };
131
+
132
+ async function handle(msg) {
133
+ if (!msg || typeof msg !== "object" || msg.id === undefined || msg.id === null) return; // notification
134
+ const ok = (result) => send({ jsonrpc: "2.0", id: msg.id, result });
135
+ switch (msg.method) {
136
+ case "initialize":
137
+ return ok({ protocolVersion: PROTOCOL, capabilities: { tools: {} }, serverInfo: { name: "clearance", version: "0.1.0" } });
138
+ case "ping":
139
+ return ok({});
140
+ case "tools/list":
141
+ return ok({ tools: TOOLS });
142
+ case "tools/call":
143
+ try {
144
+ return ok(text(await callTool(msg.params?.name, msg.params?.arguments ?? {})));
145
+ } catch (e) {
146
+ return ok(text({ error: e.cause?.code ? `${e.message} (${e.cause.code})` : e.message }, true));
147
+ }
148
+ default:
149
+ return send({ jsonrpc: "2.0", id: msg.id, error: { code: -32601, message: "method not found" } });
150
+ }
151
+ }
152
+
153
+ // When the client closes stdin, finish the calls in flight, then exit.
154
+ let inFlight = 0, closed = false;
155
+ const done = () => { if (--inFlight === 0 && closed) process.exit(0); };
156
+ process.stdout.on("error", () => process.exit(0)); // the client went away
157
+ createInterface({ input: process.stdin }).on("close", () => { closed = true; if (!inFlight) process.exit(0); }).on("line", (line) => {
158
+ if (!line.trim()) return;
159
+ let msg;
160
+ try { msg = JSON.parse(line); } catch { return send({ jsonrpc: "2.0", id: null, error: { code: -32700, message: "parse error" } }); }
161
+ inFlight++;
162
+ handle(msg).catch((e) => send({ jsonrpc: "2.0", id: msg?.id ?? null, error: { code: -32603, message: e.message } })).finally(done);
163
+ });
package/package.json CHANGED
@@ -1,6 +1,13 @@
1
1
  {
2
2
  "name": "agentcrashtest-mcp",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.0",
4
+ "description": "MCP server for Clearance (agentcrashtest.com): crash-test an AI agent that moves money from Claude Code, Claude Desktop or Cursor",
5
+ "type": "module",
6
+ "bin": { "agentcrashtest-mcp": "clearance-mcp.mjs" },
7
+ "files": ["clearance-mcp.mjs", "README.md"],
8
+ "engines": { "node": ">=20" },
9
+ "keywords": ["mcp", "model-context-protocol", "ai-agents", "agent-security", "near", "crash-test"],
10
+ "homepage": "https://agentcrashtest.com",
11
+ "repository": { "type": "git", "url": "git+https://github.com/Selliksss/clearance.git", "directory": "mcp" },
12
+ "license": "MIT"
13
+ }