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.
- package/CLAUDE.md +110 -0
- package/README.md +127 -0
- package/dist/index.js +486 -0
- 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
|
+
}
|