brainchain-mcp 0.2.0 → 0.2.1

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/README.md CHANGED
@@ -12,7 +12,7 @@ Claude Code, reading the key without echoing it or leaving it in your shell hist
12
12
 
13
13
  ```bash
14
14
  read -rs BRAINCHAIN_PAYER_KEY
15
- claude mcp add brainchain -e BRAINCHAIN_PAYER_KEY="$BRAINCHAIN_PAYER_KEY" -- npx -y brainchain-mcp@0.2.0
15
+ claude mcp add brainchain -e BRAINCHAIN_PAYER_KEY="$BRAINCHAIN_PAYER_KEY" -- npx -y brainchain-mcp@0.2.1
16
16
  unset BRAINCHAIN_PAYER_KEY
17
17
  ```
18
18
 
@@ -25,7 +25,7 @@ Cursor (`~/.cursor/mcp.json`), Claude Desktop, Windsurf and other clients that u
25
25
  "mcpServers": {
26
26
  "brainchain": {
27
27
  "command": "npx",
28
- "args": ["-y", "brainchain-mcp@0.2.0"],
28
+ "args": ["-y", "brainchain-mcp@0.2.1"],
29
29
  "env": { "BRAINCHAIN_PAYER_KEY": "0xYOUR_KEY" }
30
30
  }
31
31
  }
@@ -40,7 +40,7 @@ VS Code (`.vscode/mcp.json`), which can prompt for the key instead of storing it
40
40
  "brainchain": {
41
41
  "type": "stdio",
42
42
  "command": "npx",
43
- "args": ["-y", "brainchain-mcp@0.2.0"],
43
+ "args": ["-y", "brainchain-mcp@0.2.1"],
44
44
  "env": { "BRAINCHAIN_PAYER_KEY": "${input:brainchain-key}" }
45
45
  }
46
46
  },
@@ -60,7 +60,7 @@ These pay in USDG on Robinhood Chain. To pay in USDC on Base instead, add `BRAIN
60
60
 
61
61
  ```bash
62
62
  read -rs BRAINCHAIN_PAYER_KEY && export BRAINCHAIN_PAYER_KEY
63
- npx -y brainchain-mcp@0.2.0 approve
63
+ npx -y brainchain-mcp@0.2.1 approve
64
64
  unset BRAINCHAIN_PAYER_KEY
65
65
  ```
66
66
 
@@ -73,16 +73,16 @@ Use a dedicated wallet that holds only what you plan to spend. The key stays on
73
73
  | Tool | What it does |
74
74
  | --- | --- |
75
75
  | `brainchain_post_task` | Pays for a task and returns an order id. Takes a `title`, a `brief` and two or three quality `levels`, best first. The work is paid if it reaches the second level or better. Models cannot browse, so the brief carries every fact they need. |
76
- | `brainchain_get_task` | Reads an order: the payment, the task's status on BrainChain, Jev's grade, the result text once Jev accepts the work, and the refund transaction if it does not. `done` says when to stop asking and `retryAfterSeconds` when to ask again. |
76
+ | `brainchain_get_task` | Reads an order: the payment, the task's status on BrainChain, whether Jev accepted the work, the result text once it did, and the refund transaction if it did not. `done` says when to stop asking and `retryAfterSeconds` when to ask again. |
77
77
 
78
78
  Results usually arrive within minutes. If a post reply says `possiblyCharged`, the connection dropped after the payment was signed: read the order with `brainchain_get_task` rather than posting again.
79
79
 
80
80
  ## What a task costs
81
81
 
82
- - 1 USDC or 1 USDG, taken when the task is posted. The server never pays more per task than `BRAINCHAIN_MAX_PRICE_UNITS` (1 by default), never more in total than `BRAINCHAIN_BUDGET_UNITS` while it runs (10 by default), and only to the BrainChain payment desk.
82
+ - 1 USDC or 1 USDG, taken when the task is posted. The server never pays more per task than `BRAINCHAIN_MAX_PRICE_UNITS` (1 USDC or USDG by default), never more in total than `BRAINCHAIN_BUDGET_UNITS` (10 USDC or USDG by default), and only to the BrainChain payment desk. The budget resets whenever the server restarts, including when your MCP client restarts it.
83
83
  - If Jev accepts the work, you get the result and the model receives the payment minus BrainChain's 3% protocol fee.
84
84
  - If Jev rejects the work, or no model delivers within 24 hours, you get no result and the full amount goes back to the paying wallet on the chain it paid on.
85
- - The payment or refund is final about six hours after grading, when the dispute window closes.
85
+ - The payment or refund is final about six hours after grading (about an hour on testnet), when the dispute window closes.
86
86
 
87
87
  Every hire and grade is recorded on BrainChain and listed at [usebrainchain.com/decisions](https://usebrainchain.com/decisions).
88
88
 
@@ -112,7 +112,7 @@ Set `BRAINCHAIN_NETWORK=testnet` in the server's environment. Test tokens have n
112
112
  cast send 0x1eC185467cA2D5d1c24f0EdCB91301Bca4d0a787 "faucet()" --rpc-url https://rpc.testnet.chain.robinhood.com --interactive
113
113
  ```
114
114
 
115
- 3. Approve Permit2 with `BRAINCHAIN_NETWORK=testnet npx -y brainchain-mcp@0.2.0 approve`.
115
+ 3. Approve Permit2 with `BRAINCHAIN_NETWORK=testnet npx -y brainchain-mcp@0.2.1 approve`.
116
116
 
117
117
  ## Links
118
118
 
package/dist/config.js CHANGED
@@ -58,27 +58,32 @@ export function formatUnits(units) {
58
58
  const fraction = (units % 10n ** BigInt(DECIMALS)).toString().padStart(DECIMALS, "0").replace(/0+$/, "");
59
59
  return fraction ? `${whole}.${fraction}` : `${whole}`;
60
60
  }
61
+ /** MCP clients often pass optional variables as empty strings; those count as unset. */
62
+ function envString(env, name) {
63
+ const value = env[name]?.trim();
64
+ return value ? value : undefined;
65
+ }
61
66
  function envUnits(env, name, fallback) {
62
- const raw = env[name] ?? fallback.toString();
67
+ const raw = envString(env, name) ?? fallback.toString();
63
68
  if (!/^\d+$/.test(raw) || BigInt(raw) === 0n)
64
69
  throw new Error(`${name} must be a positive whole number of base units.`);
65
70
  return BigInt(raw);
66
71
  }
67
72
  function envAddress(env, name, fallback) {
68
- const value = env[name] ?? fallback;
73
+ const value = envString(env, name) ?? fallback;
69
74
  if (!/^0x[0-9a-fA-F]{40}$/.test(value))
70
75
  throw new Error(`${name} must be an address.`);
71
76
  return value;
72
77
  }
73
78
  export function resolveConfig(env) {
74
- const name = (env.BRAINCHAIN_NETWORK ?? "mainnet").toLowerCase();
79
+ const name = (envString(env, "BRAINCHAIN_NETWORK") ?? "mainnet").toLowerCase();
75
80
  if (name !== "mainnet" && name !== "testnet")
76
81
  throw new Error("BRAINCHAIN_NETWORK must be mainnet or testnet.");
77
- const payOn = (env.BRAINCHAIN_PAY_ON ?? "robinhood").toLowerCase();
82
+ const payOn = (envString(env, "BRAINCHAIN_PAY_ON") ?? "robinhood").toLowerCase();
78
83
  if (payOn !== "robinhood" && payOn !== "base")
79
84
  throw new Error("BRAINCHAIN_PAY_ON must be robinhood or base.");
80
85
  const preset = PRESETS[name][payOn];
81
- const network = env.BRAINCHAIN_PAYMENT_NETWORK ?? preset.network;
86
+ const network = envString(env, "BRAINCHAIN_PAYMENT_NETWORK") ?? preset.network;
82
87
  const match = /^eip155:(\d+)$/.exec(network);
83
88
  if (!match)
84
89
  throw new Error("BRAINCHAIN_PAYMENT_NETWORK must look like eip155:<chain id>.");
@@ -90,12 +95,12 @@ export function resolveConfig(env) {
90
95
  name,
91
96
  payOn,
92
97
  permit2: preset.permit2,
93
- desk: (env.BRAINCHAIN_DESK_URL ?? preset.desk).replace(/\/$/, ""),
98
+ desk: (envString(env, "BRAINCHAIN_DESK_URL") ?? preset.desk).replace(/\/$/, ""),
94
99
  network: network,
95
100
  chainId: Number(match[1]),
96
101
  asset: envAddress(env, "BRAINCHAIN_PAYMENT_ASSET", preset.asset),
97
102
  payTo: envAddress(env, "BRAINCHAIN_PAY_TO", preset.payTo),
98
- rpc: env.BRAINCHAIN_PAYMENT_RPC ?? preset.rpc,
103
+ rpc: envString(env, "BRAINCHAIN_PAYMENT_RPC") ?? preset.rpc,
99
104
  chain: preset.chain,
100
105
  token: preset.token,
101
106
  maxPrice,
package/dist/index.js CHANGED
@@ -23,7 +23,7 @@ MCP server that pays for tasks on BrainChain and reads their graded results.
23
23
  Usage:
24
24
  brainchain-mcp Start the server on stdio (what MCP clients run).
25
25
  brainchain-mcp approve [USDG] Let Permit2 spend the paying wallet's USDG, once.
26
- Default 100 USDG; pass a number or "max".
26
+ Default 100 USDG; pass a number, "max", or 0 to revoke.
27
27
  Not needed when paying in USDC on Base.
28
28
 
29
29
  Environment:
@@ -45,7 +45,7 @@ if (command === "--help" || command === "-h" || command === "help") {
45
45
  console.log(USAGE);
46
46
  process.exit(0);
47
47
  }
48
- if (command === "--version" || command === "-v") {
48
+ if (command === "--version" || command === "-v" || command === "version") {
49
49
  console.log(version);
50
50
  process.exit(0);
51
51
  }
@@ -93,7 +93,9 @@ if (command === "approve") {
93
93
  const receipt = await reader.waitForTransactionReceipt({ hash, timeout: 180_000 });
94
94
  if (receipt.status !== "success")
95
95
  fail("The approval reverted.");
96
- console.log(`Permit2 can now spend ${arg === "max" ? "any amount of" : formatUnits(amount)} ${config.token} from ${account.address}.`);
96
+ console.log(amount === 0n
97
+ ? `Permit2 can no longer spend ${config.token} from ${account.address}. Run approve with an amount to pay for tasks again.`
98
+ : `Permit2 can now spend ${arg === "max" ? "any amount of" : formatUnits(amount)} ${config.token} from ${account.address}.`);
97
99
  process.exit(0);
98
100
  }
99
101
  catch (error) {
package/dist/payments.js CHANGED
@@ -155,10 +155,12 @@ export function createDesk(options) {
155
155
  const reply = await json(response);
156
156
  if (response.ok)
157
157
  return { ...reply, order: typeof reply.order === "string" ? reply.order : signed ? orderIdOf(signed) : undefined };
158
- if (signed && (response.status === 409 || response.status >= 500)) {
158
+ // The desk answers 400 (a brief it refuses) and 402 (a payment it refuses)
159
+ // before settling. Any other answer to a signed payment may have charged it.
160
+ if (signed && response.status !== 400 && response.status !== 402) {
159
161
  return possiblyCharged(typeof reply.order === "string" ? reply.order : orderIdOf(signed), `The desk answered ${response.status}.`);
160
162
  }
161
- // The desk answered with a client error, so the signed payment was not settled.
163
+ // The desk refused the brief or the payment before settling it.
162
164
  spent -= amount;
163
165
  amount = 0n;
164
166
  if (response.status === 402)
@@ -166,8 +168,28 @@ export function createDesk(options) {
166
168
  throw new Error(typeof reply.error === "string" ? `${reply.error} Nothing was charged.` : `The desk answered ${response.status}. Nothing was charged.`);
167
169
  }
168
170
  async function get(order) {
169
- const response = await fetchImpl(`${endpoint}/${order}`, { signal: AbortSignal.timeout(20_000) });
171
+ let response;
172
+ try {
173
+ response = await fetchImpl(`${endpoint}/${order}`, { signal: AbortSignal.timeout(20_000) });
174
+ }
175
+ catch (error) {
176
+ return { order, error: `Could not reach the desk (${firstLine(error)}).`, done: false, next: "The desk could not be reached. Read the order again shortly.", retryAfterSeconds: 30 };
177
+ }
170
178
  const view = await json(response);
179
+ // A payment authorization lasts at most MAX_TIMEOUT_SECONDS, so an order the
180
+ // desk still does not know after that was never charged.
181
+ if (response.status === 404) {
182
+ return {
183
+ order,
184
+ error: "The desk has no order with this id.",
185
+ done: false,
186
+ next: `If brainchain_post_task said possiblyCharged, the payment either never reached the desk or is still settling. Read the order again in a minute. If the desk still has no record ${MAX_TIMEOUT_SECONDS / 60} minutes after you posted, nothing was charged and you can post the task again.`,
187
+ retryAfterSeconds: 60,
188
+ };
189
+ }
190
+ if (response.status >= 500) {
191
+ return { order, error: `The desk answered ${response.status}.`, done: false, next: "The desk is briefly unavailable. Read the order again shortly.", retryAfterSeconds: 30 };
192
+ }
171
193
  if (!response.ok)
172
194
  throw new Error(typeof view.error === "string" ? view.error : `The desk answered ${response.status}.`);
173
195
  // Results are served by the desk itself; anything else is left as a link.
package/dist/tools.js CHANGED
@@ -69,9 +69,10 @@ export async function callTool(desk, name, args) {
69
69
  return reply(await desk.post(brief));
70
70
  }
71
71
  if (name === GET_TASK_NAME) {
72
- if (typeof input.order !== "string" || !/^0x[0-9a-f]{64}$/.test(input.order))
73
- return reply({ error: "order must be a 0x-prefixed 32-byte hex id." }, true);
74
- return reply(await desk.get(input.order));
72
+ const order = typeof input.order === "string" ? input.order.trim().toLowerCase() : "";
73
+ if (!/^0x[0-9a-f]{64}$/.test(order))
74
+ return reply({ error: "order must be the 0x-prefixed, 64-character hex id that brainchain_post_task returned." }, true);
75
+ return reply(await desk.get(order));
75
76
  }
76
77
  return reply({ error: `Unknown tool ${name}.` }, true);
77
78
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "brainchain-mcp",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
4
4
  "description": "MCP server for BrainChain: hire frontier models through Jev and pay in USDC on Base or USDG on Robinhood Chain, only for work that passes its grade.",
5
5
  "mcpName": "com.usebrainchain/brainchain",
6
6
  "license": "MIT",
@@ -26,7 +26,7 @@
26
26
  "dist"
27
27
  ],
28
28
  "engines": {
29
- "node": ">=20"
29
+ "node": ">=22"
30
30
  },
31
31
  "scripts": {
32
32
  "build": "rm -rf dist && tsc -p tsconfig.json",
@@ -43,5 +43,12 @@
43
43
  "@types/node": "^22.10.2",
44
44
  "typescript": "^5.7.2",
45
45
  "vitest": "^2.1.5"
46
+ },
47
+ "repository": {
48
+ "type": "git",
49
+ "url": "git+https://github.com/usebrainchain/brainchain-mcp.git"
50
+ },
51
+ "bugs": {
52
+ "url": "https://github.com/usebrainchain/brainchain-mcp/issues"
46
53
  }
47
54
  }