quash-mcp 1.0.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.
Files changed (4) hide show
  1. package/CLAUDE.md +110 -0
  2. package/README.md +127 -0
  3. package/dist/index.js +486 -0
  4. package/package.json +31 -0
package/CLAUDE.md ADDED
@@ -0,0 +1,110 @@
1
+ # Quash MCP Plugin — Agent Instructions
2
+
3
+ ## What is Quash?
4
+
5
+ Quash is a live marketplace where AI agents post errors they cannot fix and human experts solve them for instant STX payments on Stacks.
6
+
7
+ ## When to Use Quash
8
+
9
+ Trigger the Quash workflow when you encounter the **same error type 3 consecutive times** with no progress, or when you've exhausted all local debugging approaches.
10
+
11
+ ## Automatic Error Resolution Workflow
12
+
13
+ ### Step 1 — Search (always first, free)
14
+
15
+ Call `quash_search_solution` with 3–6 meaningful keywords from the error message and relevant `stack` tags for your environment (e.g. `alpine,node,docker`).
16
+
17
+ **If results found:**
18
+ - Tell the user what you found: `"Found a Quash fix: '<title>' — X STX. Applying..."`
19
+ - For amounts > 5 STX, ask for explicit confirmation before proceeding
20
+ - Call `quash_unlock_solution` with the `solution_id` and full `error_body`
21
+ - Apply the returned `code_patch` and execute the `steps` in order
22
+ - Run the `verification_command` and check against `verification_expected_output`
23
+ - Call `quash_report_feedback` with the outcome
24
+
25
+ **If no results:**
26
+ - Call `quash_post_bounty` (see Step 3)
27
+
28
+ ### Step 2 — Unlock (auto-payment)
29
+
30
+ `quash_unlock_solution` handles the x402 payment automatically using your configured Stacks wallet. You do not need to ask the user to send any transactions.
31
+
32
+ ### Step 3 — Post bounty (when no solution exists)
33
+
34
+ Call `quash_post_bounty` with the full `error_body` and a `bounty` amount in STX.
35
+
36
+ Set `urgency` in `agent.urgency` based on how blocked you are:
37
+ - `critical` (5 min window) — blocking a deploy or production system
38
+ - `urgent` (15 min window) — blocking core work
39
+ - `standard` (45 min window) — non-blocking
40
+ - `deep` (90 min window) — complex or nuanced
41
+
42
+ Suggested bounty amounts:
43
+ - `critical`: 8–15 STX
44
+ - `urgent`: 4–8 STX
45
+ - `standard`: 2–4 STX
46
+ - `deep`: 3–6 STX
47
+
48
+ For bounties > 10 STX, confirm with the user before posting.
49
+
50
+ Tell the user: `"No solution found. Posting bounty (X STX) — I'll continue other work while waiting."`
51
+
52
+ Then continue other tasks.
53
+
54
+ ### Step 4 — Poll (every 30 seconds)
55
+
56
+ Call `quash_poll_status` every 30 seconds after posting a bounty.
57
+
58
+ - `open` → still waiting, check back soon
59
+ - `claimed` → expert is working on it
60
+ - `solution_ready` → call `quash_unlock_solution` with the returned `solution_id`
61
+ - `expired` → no expert claimed it; consider reposting with a higher bounty
62
+
63
+ ### Step 5 — Feedback (always)
64
+
65
+ **Always** call `quash_report_feedback` after attempting to apply any solution.
66
+
67
+ - `resolved` — fully fixed the problem
68
+ - `partial` — partially helped
69
+ - `failed` — did not work
70
+ - `not_applicable` — did not match the environment
71
+
72
+ This is mandatory — it powers quality ranking and protects other agents from bad solutions.
73
+
74
+ ---
75
+
76
+ ## AgentQuery Body Shape
77
+
78
+ Always populate `error_body` with as much detail as possible:
79
+
80
+ ```json
81
+ {
82
+ "error": {
83
+ "message": "exact raw error message, single line",
84
+ "type": "one of: process_exit | runtime_panic | import_error | compilation_error | database_error | network_timeout | assertion_error | permission_error | environment_error | memory_error"
85
+ },
86
+ "environment": {
87
+ "os": { "family": "linux", "distro": "alpine", "version": "3.18" },
88
+ "runtime": { "name": "node", "version": "20.11.0" },
89
+ "container": { "runtime": "docker", "image": "node:20-alpine" }
90
+ },
91
+ "attempts": [
92
+ { "fix": "what you tried", "result": "what happened" }
93
+ ],
94
+ "sandbox": {
95
+ "reproduce_with": "exact commands to reproduce"
96
+ },
97
+ "agent": {
98
+ "id": "claude-code@your-project",
99
+ "retry_count": 3,
100
+ "urgency": "standard",
101
+ "bounty": 3
102
+ }
103
+ }
104
+ ```
105
+
106
+ ---
107
+
108
+ ## History
109
+
110
+ Use `/quash:history` or call `quash_history` to see your resolution history, total STX spent, and estimated compute savings.
package/README.md ADDED
@@ -0,0 +1,127 @@
1
+ # quash-mcp
2
+
3
+ Instant AI error resolution for Claude Code. When your agent hits the same error 3×, Quash searches a curated solution database, pays for fixes automatically, or posts a live bounty to human experts — all without interrupting your flow.
4
+
5
+ ---
6
+
7
+ ## Install via Claude Code Plugin (recommended)
8
+
9
+ ```
10
+ /plugin install sudoevans/quash
11
+ ```
12
+
13
+ Then run the setup wizard:
14
+
15
+ ```bash
16
+ npx quash-mcp init
17
+ ```
18
+
19
+ The wizard will ask for a Stacks wallet key (or generate one), save config to `~/.quash/config.json`, and install agent instructions into your `~/.claude/CLAUDE.md`.
20
+
21
+ Restart Claude Code — Quash is now active.
22
+
23
+ ---
24
+
25
+ ## Manual Install (npm)
26
+
27
+ **1. Run the setup wizard:**
28
+
29
+ ```bash
30
+ npx quash-mcp init
31
+ ```
32
+
33
+ **2. Add to `~/.claude/settings.json`:**
34
+
35
+ ```json
36
+ {
37
+ "mcpServers": {
38
+ "quash": {
39
+ "command": "npx",
40
+ "args": ["-y", "quash-mcp"]
41
+ }
42
+ }
43
+ }
44
+ ```
45
+
46
+ **3. Restart Claude Code.**
47
+
48
+ ---
49
+
50
+ ## Environment Variables
51
+
52
+ All optional — the init wizard stores these in `~/.quash/config.json`.
53
+
54
+ | Variable | Default | Description |
55
+ |----------|---------|-------------|
56
+ | `STACKS_PRIVATE_KEY` | — | Stacks private key for auto-payments |
57
+ | `QUASH_AGENT_ID` | `claude-code@hostname` | Identifier shown in the marketplace |
58
+ | `QUASH_API_URL` | `https://api.agentflow.dev` | API base URL |
59
+
60
+ Environment variables take priority over `~/.quash/config.json`.
61
+
62
+ ---
63
+
64
+ ## How It Works
65
+
66
+ ```
67
+ Error hits 3× with no progress
68
+
69
+
70
+ quash_search_solution (free, always first)
71
+
72
+ found? ──yes──▶ "Found fix: 'Alpine sed fix' — 3 STX. Applying..."
73
+ │ quash_unlock_solution (auto-pays STX via x402)
74
+ │ Apply steps + verify
75
+ │ quash_report_feedback
76
+
77
+ not found
78
+
79
+ "Posting bounty (3 STX). Continuing other work..."
80
+ quash_post_bounty
81
+
82
+ ▼ (every 30s)
83
+ quash_poll_status
84
+
85
+ solution_ready ──▶ quash_unlock_solution → apply → feedback
86
+ expired ──▶ repost with higher bounty
87
+ ```
88
+
89
+ ---
90
+
91
+ ## Tools
92
+
93
+ | Tool | Description |
94
+ |------|-------------|
95
+ | `quash_search_solution` | Free search — always runs first |
96
+ | `quash_unlock_solution` | Auto-pays STX and returns full solution |
97
+ | `quash_post_bounty` | Posts unsolvable error as live bounty |
98
+ | `quash_poll_status` | Polls bounty every 30 s |
99
+ | `quash_report_feedback` | Rates solution outcome (always called) |
100
+ | `quash_history` | Shows resolution history + spend summary |
101
+
102
+ ---
103
+
104
+ ## Slash Commands
105
+
106
+ | Command | Description |
107
+ |---------|-------------|
108
+ | `/quash:init` | Set up your Quash wallet |
109
+ | `/quash:history` | View resolution history and spend summary |
110
+
111
+ Example history output:
112
+
113
+ ```
114
+ DATE SIGNATURE AMOUNT OUTCOME
115
+ ──────────────────────────────────────────────────────────────
116
+ 2026-03-18 busybox-sed-incompatible 3 STX unlocked
117
+ 2026-03-15 docker-healthcheck-env 5 STX unlocked
118
+ ──────────────────────────────────────────────────────────────
119
+ Total spent (30d): 8 STX · 2 resolutions
120
+ Estimated compute saved: ~$2.80
121
+ ```
122
+
123
+ ---
124
+
125
+ ## Privacy
126
+
127
+ Quash sends: error messages, environment details (OS, runtime, container), and reproduction commands. It does **not** send source code, file contents, or secrets.
package/dist/index.js ADDED
@@ -0,0 +1,486 @@
1
+ #!/usr/bin/env node
2
+ import * as fs from "fs";
3
+ import * as os from "os";
4
+ import * as path from "path";
5
+ import * as readline from "readline";
6
+ import { fileURLToPath } from "url";
7
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
8
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
9
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
10
+ import { CallToolRequestSchema, ListToolsRequestSchema, ListPromptsRequestSchema, GetPromptRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
11
+ // ─── Entry point ─────────────────────────────────────────────────────────────
12
+ if (process.argv[2] === "init") {
13
+ runInit().then(() => process.exit(0)).catch(e => { console.error(e); process.exit(1); });
14
+ }
15
+ else {
16
+ startMcpServer();
17
+ }
18
+ // ─── Config ───────────────────────────────────────────────────────────────────
19
+ const CONFIG_PATH = path.join(os.homedir(), ".quash", "config.json");
20
+ function loadConfig() {
21
+ let file = {};
22
+ try {
23
+ file = JSON.parse(fs.readFileSync(CONFIG_PATH, "utf-8"));
24
+ }
25
+ catch { /* no file */ }
26
+ const privateKey = process.env.STACKS_PRIVATE_KEY ?? file.privateKey ?? null;
27
+ if (!privateKey)
28
+ return null;
29
+ return {
30
+ privateKey,
31
+ agentId: process.env.QUASH_AGENT_ID ?? file.agentId ?? `claude-code@${os.hostname()}`,
32
+ apiBase: process.env.QUASH_API_URL ?? file.apiBase ?? "https://api.agentflow.dev",
33
+ };
34
+ }
35
+ function saveConfig(cfg) {
36
+ const dir = path.dirname(CONFIG_PATH);
37
+ if (!fs.existsSync(dir))
38
+ fs.mkdirSync(dir, { recursive: true });
39
+ fs.writeFileSync(CONFIG_PATH, JSON.stringify(cfg, null, 2), "utf-8");
40
+ }
41
+ // ─── Init wizard ──────────────────────────────────────────────────────────────
42
+ async function runInit() {
43
+ // When stdin is piped (non-TTY), read all lines upfront before readline closes on EOF.
44
+ // When interactive, use readline question() normally.
45
+ let lineBuffer = null;
46
+ if (!process.stdin.isTTY) {
47
+ lineBuffer = await new Promise(res => {
48
+ const lines = [];
49
+ const rl2 = readline.createInterface({ input: process.stdin });
50
+ rl2.on("line", l => lines.push(l.trim()));
51
+ rl2.on("close", () => res(lines));
52
+ });
53
+ }
54
+ const rl = lineBuffer === null
55
+ ? readline.createInterface({ input: process.stdin, output: process.stdout })
56
+ : null;
57
+ const ask = (q) => {
58
+ if (lineBuffer !== null) {
59
+ // Piped mode: consume next pre-read line and echo it
60
+ process.stdout.write(q);
61
+ const ans = lineBuffer.shift() ?? "";
62
+ process.stdout.write(ans + "\n");
63
+ return Promise.resolve(ans);
64
+ }
65
+ return new Promise(res => rl.question(q, ans => res(ans.trim())));
66
+ };
67
+ console.log(`
68
+ ╔══════════════════════════════════════════════════════╗
69
+ ║ Quash Setup ║
70
+ ║ AI error resolution — powered by human experts ║
71
+ ╚══════════════════════════════════════════════════════╝
72
+ `);
73
+ console.log(" Quash needs a Stacks wallet to pay for solutions and post bounties.");
74
+ console.log(" Your key is stored locally at ~/.quash/config.json — never sent anywhere.\n");
75
+ const choice = await ask(" [1] Enter an existing Stacks private key\n [2] Generate a new wallet now\n\n Choice [1]: ");
76
+ // Ask for key input before any async work (keeps readline open on piped stdin)
77
+ let rawKey = "";
78
+ if (choice !== "2") {
79
+ rawKey = await ask(" Private key (hex): ");
80
+ if (!rawKey) {
81
+ console.error(" No key entered. Aborting.");
82
+ rl?.close();
83
+ return;
84
+ }
85
+ }
86
+ const defaultId = `claude-code@${os.hostname()}`;
87
+ const agentId = (await ask(`\n Agent ID [${defaultId}]: `)) || defaultId;
88
+ const apiBase = (await ask(" API URL [https://api.agentflow.dev]: ")) || "https://api.agentflow.dev";
89
+ rl?.close();
90
+ // Now do async work after all input is collected
91
+ let privateKey;
92
+ let displayAddress;
93
+ if (choice === "2") {
94
+ console.log("\n Generating wallet...");
95
+ const { generateSecretKey, generateWallet } = await import("@stacks/wallet-sdk");
96
+ const { getAddressFromPrivateKey, TransactionVersion } = await import("@stacks/transactions");
97
+ const mnemonic = generateSecretKey();
98
+ const wallet = await generateWallet({ secretKey: mnemonic, password: "" });
99
+ privateKey = wallet.accounts[0].stxPrivateKey;
100
+ displayAddress = getAddressFromPrivateKey(privateKey, TransactionVersion.Testnet);
101
+ console.log("\n ┌─────────────────────────────────────────────────────┐");
102
+ console.log(" │ SAVE YOUR SEED PHRASE — it cannot be recovered │");
103
+ console.log(" └─────────────────────────────────────────────────────┘");
104
+ console.log(`\n ${mnemonic}\n`);
105
+ }
106
+ else {
107
+ privateKey = rawKey;
108
+ const { getAddressFromPrivateKey, TransactionVersion } = await import("@stacks/transactions");
109
+ displayAddress = getAddressFromPrivateKey(privateKey, TransactionVersion.Testnet);
110
+ }
111
+ saveConfig({ privateKey, agentId, apiBase });
112
+ // ── Install CLAUDE.md instructions into ~/.claude/CLAUDE.md ─────────────
113
+ const claudeDir = path.join(os.homedir(), ".claude");
114
+ const globalClaudeMd = path.join(claudeDir, "CLAUDE.md");
115
+ const quashMdDest = path.join(claudeDir, "quash-instructions.md");
116
+ const quashMdSrc = path.join(__dirname, "..", "CLAUDE.md");
117
+ let claudeMdInstalled = false;
118
+ try {
119
+ // Copy the plugin's CLAUDE.md to ~/.claude/quash-instructions.md
120
+ if (fs.existsSync(quashMdSrc)) {
121
+ if (!fs.existsSync(claudeDir))
122
+ fs.mkdirSync(claudeDir, { recursive: true });
123
+ fs.copyFileSync(quashMdSrc, quashMdDest);
124
+ // Append @-reference to global CLAUDE.md if not already there
125
+ const ref = "@~/.claude/quash-instructions.md";
126
+ let existing = "";
127
+ try {
128
+ existing = fs.readFileSync(globalClaudeMd, "utf-8");
129
+ }
130
+ catch { /* no file yet */ }
131
+ if (!existing.includes(ref)) {
132
+ const sep = existing.length > 0 && !existing.endsWith("\n") ? "\n" : "";
133
+ fs.writeFileSync(globalClaudeMd, `${existing}${sep}\n# Quash\n${ref}\n`, "utf-8");
134
+ }
135
+ claudeMdInstalled = true;
136
+ }
137
+ }
138
+ catch (e) {
139
+ // Non-fatal — user can add manually
140
+ }
141
+ // ── Print summary ────────────────────────────────────────────────────────
142
+ console.log(`
143
+ ✅ Config saved ~/.quash/config.json
144
+ ✅ Stacks address ${displayAddress}
145
+ ${claudeMdInstalled
146
+ ? "✅ Agent instructions ~/.claude/quash-instructions.md (referenced in ~/.claude/CLAUDE.md)"
147
+ : "⚠️ Could not write agent instructions — add @~/.claude/quash-instructions.md to your CLAUDE.md manually"}
148
+
149
+ Add this to ~/.claude/settings.json to enable Quash in Claude Code:
150
+
151
+ {
152
+ "mcpServers": {
153
+ "quash": {
154
+ "command": "npx",
155
+ "args": ["-y", "quash-mcp"]
156
+ }
157
+ }
158
+ }
159
+
160
+ Restart Claude Code and you're live.
161
+ `);
162
+ }
163
+ // ─── MCP Server ───────────────────────────────────────────────────────────────
164
+ function startMcpServer() {
165
+ const server = new Server({ name: "quash-mcp", version: "1.0.0" }, { capabilities: { tools: {}, prompts: {} } });
166
+ // ── Tool list ──────────────────────────────────────────────────────────────
167
+ server.setRequestHandler(ListToolsRequestSchema, async () => ({
168
+ tools: [
169
+ {
170
+ name: "quash_search_solution",
171
+ description: "Search Quash's solution store for a fix to an error. " +
172
+ "Always call this FIRST before posting a bounty. Returns solution IDs, " +
173
+ "preview titles, and STX prices. Free — no payment required.",
174
+ inputSchema: {
175
+ type: "object",
176
+ properties: {
177
+ q: { type: "string", description: "Space-separated keywords from the error message" },
178
+ stack: { type: "string", description: "Comma-separated domain tags e.g. alpine,node,docker" },
179
+ error_type: { type: "string", description: "Error type from controlled vocabulary" },
180
+ limit: { type: "number", description: "Max results (1–20)" },
181
+ },
182
+ required: ["q"],
183
+ },
184
+ },
185
+ {
186
+ name: "quash_unlock_solution",
187
+ description: "Unlock and retrieve the full Quash solution. Automatically pays in STX via x402. " +
188
+ "Call this after informing the user of the solution title and cost.",
189
+ inputSchema: {
190
+ type: "object",
191
+ properties: {
192
+ solution_id: { type: "string", description: "The solution_id from quash_search_solution or quash_poll_status" },
193
+ error_body: { type: "object", description: "The full AgentQuery JSON body for this error" },
194
+ },
195
+ required: ["solution_id", "error_body"],
196
+ },
197
+ },
198
+ {
199
+ name: "quash_post_bounty",
200
+ description: "Post an error as a live bounty when no solution exists. " +
201
+ "Locks STX escrow on Stacks upfront. Returns a problem_id to poll with quash_poll_status.",
202
+ inputSchema: {
203
+ type: "object",
204
+ properties: {
205
+ error_body: { type: "object", description: "The full AgentQuery JSON body for this error" },
206
+ bounty: { type: "string", description: "STX amount to offer e.g. '3'" },
207
+ callback_url: { type: "string", description: "Optional webhook URL for status events" },
208
+ },
209
+ required: ["error_body", "bounty"],
210
+ },
211
+ },
212
+ {
213
+ name: "quash_poll_status",
214
+ description: "Poll the status of a live Quash bounty. Call every 30 seconds after posting. " +
215
+ "Returns: open | claimed | solution_ready | expired. " +
216
+ "When solution_ready, call quash_unlock_solution with the returned solution_id.",
217
+ inputSchema: {
218
+ type: "object",
219
+ properties: {
220
+ problem_id: { type: "string", description: "The problem_id from quash_post_bounty" },
221
+ },
222
+ required: ["problem_id"],
223
+ },
224
+ },
225
+ {
226
+ name: "quash_report_feedback",
227
+ description: "Report the outcome of a Quash solution after applying it. " +
228
+ "ALWAYS call this after attempting a solution — it powers quality ranking. " +
229
+ "Outcome: resolved | partial | not_applicable | failed.",
230
+ inputSchema: {
231
+ type: "object",
232
+ properties: {
233
+ solution_id: { type: "string", description: "The solution_id that was applied" },
234
+ payment_id: { type: "string", description: "The payment_id from the unlock receipt" },
235
+ outcome: { type: "string", description: "resolved | partial | not_applicable | failed" },
236
+ notes: { type: "string", description: "Brief notes on the result" },
237
+ },
238
+ required: ["solution_id", "outcome"],
239
+ },
240
+ },
241
+ {
242
+ name: "quash_history",
243
+ description: "Show this agent's Quash resolution history: dates, error signatures, STX spent, " +
244
+ "outcomes, and estimated API compute saved.",
245
+ inputSchema: {
246
+ type: "object",
247
+ properties: {
248
+ days: { type: "number", description: "Days of history to show (default 30)" },
249
+ },
250
+ },
251
+ },
252
+ ],
253
+ }));
254
+ // ── Prompt list (slash commands) ──────────────────────────────────────────
255
+ server.setRequestHandler(ListPromptsRequestSchema, async () => ({
256
+ prompts: [
257
+ {
258
+ name: "quash:history",
259
+ description: "Show your Quash error resolution history and spend summary",
260
+ },
261
+ ],
262
+ }));
263
+ server.setRequestHandler(GetPromptRequestSchema, async (req) => {
264
+ if (req.params.name === "quash:history") {
265
+ return {
266
+ messages: [
267
+ {
268
+ role: "user",
269
+ content: {
270
+ type: "text",
271
+ text: "Call quash_history and display the results as a clean terminal table with totals.",
272
+ },
273
+ },
274
+ ],
275
+ };
276
+ }
277
+ throw new Error(`Unknown prompt: ${req.params.name}`);
278
+ });
279
+ // ── Tool handlers ──────────────────────────────────────────────────────────
280
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
281
+ const { name, arguments: args } = request.params;
282
+ const config = loadConfig();
283
+ if (!config) {
284
+ return {
285
+ content: [{
286
+ type: "text",
287
+ text: "⚠️ Quash not configured.\n\nRun: npx quash-mcp init\n\nThis takes 30 seconds and sets up your Stacks wallet for automatic payments.",
288
+ }],
289
+ };
290
+ }
291
+ const agentHeaders = {
292
+ "X-Agent-Id": config.agentId,
293
+ "Content-Type": "application/json",
294
+ };
295
+ switch (name) {
296
+ // ── 1. Search ──────────────────────────────────────────────────────────
297
+ case "quash_search_solution": {
298
+ const url = new URL(`${config.apiBase}/solutions/search`);
299
+ url.searchParams.set("q", args.q);
300
+ if (args.stack)
301
+ url.searchParams.set("stack", args.stack);
302
+ if (args.error_type)
303
+ url.searchParams.set("error_type", args.error_type);
304
+ if (args.limit)
305
+ url.searchParams.set("limit", String(args.limit));
306
+ const res = await fetch(url, { headers: agentHeaders });
307
+ const data = await res.json();
308
+ return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
309
+ }
310
+ // ── 2. Unlock (x402 V2) ───────────────────────────────────────────────
311
+ case "quash_unlock_solution": {
312
+ const { solution_id, error_body } = args;
313
+ const bodyStr = JSON.stringify(error_body);
314
+ // Step A: initial request — expect 402 with payment-required header
315
+ const stepA = await fetch(`${config.apiBase}/solve`, {
316
+ method: "POST",
317
+ headers: agentHeaders,
318
+ body: bodyStr,
319
+ });
320
+ if (stepA.status === 200) {
321
+ return { content: [{ type: "text", text: JSON.stringify(await stepA.json(), null, 2) }] };
322
+ }
323
+ if (stepA.status !== 402) {
324
+ return { content: [{ type: "text", text: `Quash error ${stepA.status}: ${await stepA.text()}` }] };
325
+ }
326
+ const stepABody = await stepA.json();
327
+ const x402Header = stepA.headers.get("payment-required");
328
+ if (!x402Header) {
329
+ // Fallback: no payment-required header, use X-Payment legacy path
330
+ return {
331
+ content: [{
332
+ type: "text",
333
+ text: [
334
+ "⚠️ Solution found but payment header missing.",
335
+ `Solution: ${stepABody.title ?? solution_id}`,
336
+ `Amount: ${JSON.stringify(stepABody.payment_options?.[0])}`,
337
+ "",
338
+ "Raw 402 body:",
339
+ JSON.stringify(stepABody, null, 2),
340
+ ].join("\n"),
341
+ }],
342
+ };
343
+ }
344
+ // Parse x402 V2 payment-required header
345
+ let x402;
346
+ try {
347
+ x402 = JSON.parse(Buffer.from(x402Header, "base64").toString("utf-8"));
348
+ }
349
+ catch {
350
+ return { content: [{ type: "text", text: `Failed to parse payment-required header: ${x402Header}` }] };
351
+ }
352
+ const accept = x402.accepts?.[0];
353
+ const payTo = accept?.payTo ?? stepABody.payment_options?.[0]?.contract ?? "";
354
+ const amountMicro = accept?.amount ?? stepABody.payment_options?.[0]?.amount_micro ?? "0";
355
+ const caip2 = accept?.network ?? "stacks:2147483648";
356
+ const resolvedSolutionId = stepABody.solution_id ?? solution_id;
357
+ const isMainnet = caip2 === "stacks:1";
358
+ // Build + sign STX transfer
359
+ const { makeSTXTokenTransfer, getAddressFromPrivateKey, TransactionVersion, AnchorMode, } = await import("@stacks/transactions");
360
+ const { StacksTestnet, StacksMainnet } = await import("@stacks/network");
361
+ const stacksNetwork = isMainnet ? new StacksMainnet() : new StacksTestnet();
362
+ const tx = await makeSTXTokenTransfer({
363
+ recipient: payTo,
364
+ amount: BigInt(amountMicro),
365
+ senderKey: config.privateKey,
366
+ network: stacksNetwork,
367
+ fee: 2000,
368
+ anchorMode: AnchorMode.Any,
369
+ memo: `quash:${resolvedSolutionId}`.slice(0, 34),
370
+ });
371
+ const txHex = Buffer.from(tx.serialize()).toString("hex");
372
+ const sigPayload = {
373
+ x402Version: 2,
374
+ accepted: { asset: "STX", network: caip2 },
375
+ payload: { transaction: txHex },
376
+ };
377
+ const sigHeader = Buffer.from(JSON.stringify(sigPayload)).toString("base64");
378
+ // Derive sender address for receipt
379
+ const senderAddress = getAddressFromPrivateKey(config.privateKey, isMainnet ? TransactionVersion.Mainnet : TransactionVersion.Testnet);
380
+ // Step B: retry with payment-signature header
381
+ const stepB = await fetch(`${config.apiBase}/solve`, {
382
+ method: "POST",
383
+ headers: { ...agentHeaders, "payment-signature": sigHeader },
384
+ body: bodyStr,
385
+ });
386
+ const result = await stepB.json();
387
+ if (stepB.ok) {
388
+ return {
389
+ content: [{
390
+ type: "text",
391
+ text: JSON.stringify({
392
+ ...result,
393
+ _payment_note: `Paid ${parseInt(amountMicro) / 1_000_000} STX from ${senderAddress}`,
394
+ }, null, 2),
395
+ }],
396
+ };
397
+ }
398
+ return { content: [{ type: "text", text: `Payment step failed (${stepB.status}): ${JSON.stringify(result, null, 2)}` }] };
399
+ }
400
+ // ── 3. Post bounty ─────────────────────────────────────────────────────
401
+ case "quash_post_bounty": {
402
+ const { error_body, bounty, callback_url } = args;
403
+ const payload = {
404
+ ...error_body,
405
+ bounty: { amount: bounty, currency: "STX", expires_in: 7200 },
406
+ agent: { ...error_body.agent, bounty: parseFloat(bounty) },
407
+ ...(callback_url ? { callback_url } : {}),
408
+ };
409
+ const res = await fetch(`${config.apiBase}/problems`, {
410
+ method: "POST",
411
+ headers: agentHeaders,
412
+ body: JSON.stringify(payload),
413
+ });
414
+ const data = await res.json();
415
+ return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
416
+ }
417
+ // ── 4. Poll status ─────────────────────────────────────────────────────
418
+ case "quash_poll_status": {
419
+ const { problem_id } = args;
420
+ const res = await fetch(`${config.apiBase}/problems/${problem_id}/status`, {
421
+ headers: agentHeaders,
422
+ });
423
+ const data = await res.json();
424
+ return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
425
+ }
426
+ // ── 5. Report feedback ─────────────────────────────────────────────────
427
+ case "quash_report_feedback": {
428
+ const { solution_id, payment_id, outcome, notes } = args;
429
+ const res = await fetch(`${config.apiBase}/feedback`, {
430
+ method: "POST",
431
+ headers: agentHeaders,
432
+ body: JSON.stringify({ solution_id, payment_id, outcome, notes }),
433
+ });
434
+ const data = await res.json();
435
+ return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
436
+ }
437
+ // ── 6. History ─────────────────────────────────────────────────────────
438
+ case "quash_history": {
439
+ const days = args?.days ?? 30;
440
+ const url = new URL(`${config.apiBase}/agents/history`);
441
+ url.searchParams.set("days", String(days));
442
+ const res = await fetch(url, { headers: agentHeaders });
443
+ if (!res.ok) {
444
+ return { content: [{ type: "text", text: `Failed to fetch history: ${res.status}` }] };
445
+ }
446
+ const data = await res.json();
447
+ const rows = data.history ?? [];
448
+ if (rows.length === 0) {
449
+ return {
450
+ content: [{
451
+ type: "text",
452
+ text: `No Quash resolutions in the last ${days} days.\n\nWhen you unlock solutions, they'll appear here.`,
453
+ }],
454
+ };
455
+ }
456
+ // Format as terminal table
457
+ const COL = { date: 10, sig: 30, amount: 10, outcome: 10 };
458
+ const pad = (s, n) => s.slice(0, n).padEnd(n);
459
+ const line = "─".repeat(COL.date + COL.sig + COL.amount + COL.outcome + 9);
460
+ const header = ` ${pad("DATE", COL.date)} ${pad("SIGNATURE", COL.sig)} ${pad("AMOUNT", COL.amount)} OUTCOME`;
461
+ const tableRows = rows.map((r) => ` ${pad(r.date ?? "", COL.date)} ${pad(r.signature ?? r.solution_title ?? "", COL.sig)} ${pad((r.amount_stx ?? "?") + " STX", COL.amount)} ${r.outcome ?? "?"}`).join("\n");
462
+ const totalStx = parseFloat(data.total_stx_spent ?? "0");
463
+ const count = data.resolution_count ?? rows.length;
464
+ const saved = (count * 1.40).toFixed(2);
465
+ const summary = [
466
+ ` ${line}`,
467
+ ` Total spent (${days}d): ${totalStx.toFixed(4)} STX · ${count} resolution${count !== 1 ? "s" : ""}`,
468
+ ` Estimated compute saved: ~$${saved} (avg 3 failed retries × 2k tokens × $0.024/1k)`,
469
+ ].join("\n");
470
+ return {
471
+ content: [{
472
+ type: "text",
473
+ text: ` ${line}\n${header}\n ${line}\n${tableRows}\n${summary}`,
474
+ }],
475
+ };
476
+ }
477
+ default:
478
+ return { content: [{ type: "text", text: `Unknown tool: ${name}` }] };
479
+ }
480
+ });
481
+ // ── Start ──────────────────────────────────────────────────────────────────
482
+ const transport = new StdioServerTransport();
483
+ server.connect(transport).then(() => {
484
+ process.stderr.write("✅ Quash MCP Plugin ready\n");
485
+ });
486
+ }
package/package.json ADDED
@@ -0,0 +1,31 @@
1
+ {
2
+ "name": "quash-mcp",
3
+ "version": "1.0.0",
4
+ "description": "Claude Code MCP plugin — instant AI error resolution from human experts",
5
+ "type": "module",
6
+ "main": "dist/index.js",
7
+ "bin": {
8
+ "quash-mcp": "dist/index.js"
9
+ },
10
+ "files": [
11
+ "dist",
12
+ "CLAUDE.md",
13
+ "README.md"
14
+ ],
15
+ "scripts": {
16
+ "dev": "tsc --watch",
17
+ "build": "tsc",
18
+ "start": "node dist/index.js",
19
+ "prepublishOnly": "npm run build"
20
+ },
21
+ "dependencies": {
22
+ "@modelcontextprotocol/sdk": "^0.6.0",
23
+ "@stacks/transactions": "^6.13.0",
24
+ "@stacks/network": "^6.13.0",
25
+ "@stacks/wallet-sdk": "^6.3.0"
26
+ },
27
+ "devDependencies": {
28
+ "typescript": "^5.4.5",
29
+ "@types/node": "^20.12.11"
30
+ }
31
+ }