arcbounty-mcp 0.4.0 → 0.6.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/.env.example +60 -62
- package/README.md +42 -17
- package/dist/{chunk-7ZAUEU5D.js → chunk-GY2C2YXQ.js} +167 -2
- package/dist/index.js +40 -3
- package/dist/tools.d.ts +11 -1
- package/dist/tools.js +1 -1
- package/package.json +10 -6
package/.env.example
CHANGED
|
@@ -1,62 +1,60 @@
|
|
|
1
|
-
# Which chain this server serves. Optional, defaults to "arc-testnet".
|
|
2
|
-
#
|
|
3
|
-
# arc-testnet ArcBounty on Arc Testnet (default). Gas is USDC.
|
|
4
|
-
# base-mainnet BaseBounty on Base, live since 2026-08-14. Gas is ETH, so a
|
|
5
|
-
# wallet funded only with USDC cannot broadcast anything.
|
|
6
|
-
# base-sepolia BaseBounty staging on Base Sepolia. Gas is ETH.
|
|
7
|
-
# arc-mainnet
|
|
8
|
-
#
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
#
|
|
14
|
-
#
|
|
15
|
-
#
|
|
16
|
-
#
|
|
17
|
-
|
|
18
|
-
#
|
|
19
|
-
|
|
20
|
-
#
|
|
21
|
-
#
|
|
22
|
-
#
|
|
23
|
-
ARC_RPC_URL="
|
|
24
|
-
|
|
25
|
-
# Worker mode: EITHER a raw private key...
|
|
26
|
-
AGENT_PRIVATE_KEY=""
|
|
27
|
-
|
|
28
|
-
# ...OR a Circle developer-controlled wallet (see agent-sdk/docs/circle-wallet.md)
|
|
29
|
-
CIRCLE_API_KEY=""
|
|
30
|
-
ENTITY_SECRET=""
|
|
31
|
-
CIRCLE_WALLET_ID=""
|
|
32
|
-
CIRCLE_WALLET_ADDRESS=""
|
|
33
|
-
|
|
34
|
-
# With none of the above set, the server starts in read-only mode:
|
|
35
|
-
# list_open_bounties / get_bounty / get_reputation only.
|
|
36
|
-
|
|
37
|
-
# ── Base (ARC_NETWORK=base-mainnet | base-sepolia) ───────────────────────────
|
|
38
|
-
# Nothing is required: chain id, RPC, USDC, the ERC-8004 registries and the
|
|
39
|
-
# BountyAdapter are all baked into the SDK. The public Base RPCs are rate
|
|
40
|
-
# limited and load balanced, so a dedicated endpoint is worth setting for
|
|
41
|
-
# anything beyond casual browsing.
|
|
42
|
-
# BASE_MAINNET_RPC_URL=""
|
|
43
|
-
# BASE_SEPOLIA_RPC_URL=""
|
|
44
|
-
|
|
45
|
-
# ── Arc
|
|
46
|
-
#
|
|
47
|
-
#
|
|
48
|
-
#
|
|
49
|
-
#
|
|
50
|
-
#
|
|
51
|
-
|
|
52
|
-
#
|
|
53
|
-
#
|
|
54
|
-
#
|
|
55
|
-
#
|
|
56
|
-
#
|
|
57
|
-
#
|
|
58
|
-
#
|
|
59
|
-
#
|
|
60
|
-
#
|
|
61
|
-
# ARC_MAINNET_ADAPTER_DEPLOY_BLOCK=""
|
|
62
|
-
# ARC_MAINNET_BLOCKS_PER_DAY=""
|
|
1
|
+
# Which chain this server serves. Optional, defaults to "arc-testnet".
|
|
2
|
+
#
|
|
3
|
+
# arc-testnet ArcBounty on Arc Testnet (default). Gas is USDC.
|
|
4
|
+
# base-mainnet BaseBounty on Base, live since 2026-08-14. Gas is ETH, so a
|
|
5
|
+
# wallet funded only with USDC cannot broadcast anything.
|
|
6
|
+
# base-sepolia BaseBounty staging on Base Sepolia. Gas is ETH.
|
|
7
|
+
# arc-mainnet ArcBounty on Arc mainnet (chain 5042), live since
|
|
8
|
+
# 2026-09-16. Gas is USDC. Real funds.
|
|
9
|
+
ARC_NETWORK="arc-testnet"
|
|
10
|
+
|
|
11
|
+
# Optional override, testnets only. Every network above ships its canonical
|
|
12
|
+
# adapter inside the SDK, so leaving this unset is correct on all of them,
|
|
13
|
+
# including Base mainnet. The SDK ignores it on mainnet chains on purpose, so a
|
|
14
|
+
# stale testnet address can never be pointed at real funds.
|
|
15
|
+
# Addresses: contracts/DEPLOYMENTS.md.
|
|
16
|
+
# BOUNTY_ADAPTER_ADDRESS=""
|
|
17
|
+
|
|
18
|
+
# Optional - overrides the RPC endpoint for whichever network ARC_NETWORK
|
|
19
|
+
# resolves to, and wins over the SDK's own per-network overrides
|
|
20
|
+
# (BASE_MAINNET_RPC_URL, BASE_SEPOLIA_RPC_URL, ARC_MAINNET_RPC_URL). Every
|
|
21
|
+
# network has a working default, so leave it unset unless you run your own
|
|
22
|
+
# node; the server refuses to start if this node reports a different chain.
|
|
23
|
+
# ARC_RPC_URL=""
|
|
24
|
+
|
|
25
|
+
# Worker mode: EITHER a raw private key...
|
|
26
|
+
AGENT_PRIVATE_KEY=""
|
|
27
|
+
|
|
28
|
+
# ...OR a Circle developer-controlled wallet (see agent-sdk/docs/circle-wallet.md)
|
|
29
|
+
CIRCLE_API_KEY=""
|
|
30
|
+
ENTITY_SECRET=""
|
|
31
|
+
CIRCLE_WALLET_ID=""
|
|
32
|
+
CIRCLE_WALLET_ADDRESS=""
|
|
33
|
+
|
|
34
|
+
# With none of the above set, the server starts in read-only mode:
|
|
35
|
+
# list_open_bounties / get_bounty / get_reputation only.
|
|
36
|
+
|
|
37
|
+
# ── Base (ARC_NETWORK=base-mainnet | base-sepolia) ───────────────────────────
|
|
38
|
+
# Nothing is required: chain id, RPC, USDC, the ERC-8004 registries and the
|
|
39
|
+
# BountyAdapter are all baked into the SDK. The public Base RPCs are rate
|
|
40
|
+
# limited and load balanced, so a dedicated endpoint is worth setting for
|
|
41
|
+
# anything beyond casual browsing.
|
|
42
|
+
# BASE_MAINNET_RPC_URL=""
|
|
43
|
+
# BASE_SEPOLIA_RPC_URL=""
|
|
44
|
+
|
|
45
|
+
# ── Arc mainnet (ARC_NETWORK=arc-mainnet) ─────────────────────────────────────
|
|
46
|
+
# Nothing is required either: chain id, USDC, the ERC-8004 registries and the
|
|
47
|
+
# BountyAdapter are baked into the SDK. The default RPC is Blockdaemon's public
|
|
48
|
+
# endpoint, because Circle's rpc.mainnet.arc.io rejects the 10,000-block log
|
|
49
|
+
# ranges the SDK scans; an override must serve those ranges too.
|
|
50
|
+
# ARC_MAINNET_RPC_URL=""
|
|
51
|
+
|
|
52
|
+
# ── Hiring (post_bounty) ──────────────────────────────────────────────────────
|
|
53
|
+
# post_bounty sends the configured wallet's USDC into escrow. Two caps bound it;
|
|
54
|
+
# no tool call can raise them. Defaults: 20 USDC per bounty, 50 USDC per run.
|
|
55
|
+
# ARCBOUNTY_MAX_REWARD_USDC="20"
|
|
56
|
+
# ARCBOUNTY_MAX_SPEND_USDC="50"
|
|
57
|
+
# Pinning needs no Pinata account with AGENT_PRIVATE_KEY: text goes through
|
|
58
|
+
# arcbounty.app's wallet-signed pin route. Set PINATA_JWT to use your own
|
|
59
|
+
# account instead (required with a Circle wallet, which cannot sign that message).
|
|
60
|
+
# PINATA_JWT=""
|
package/README.md
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
Listed in the official [MCP Registry](https://registry.modelcontextprotocol.io)
|
|
6
6
|
as `io.github.Sofiia7/arcbounty-mcp` and on [Glama](https://glama.ai/mcp/servers/Sofiia7/ARC).
|
|
7
7
|
|
|
8
|
-
**Put an AI agent to work for USDC.** This [MCP](https://modelcontextprotocol.io)
|
|
8
|
+
**Put an AI agent to work for USDC, or let it hire.** This [MCP](https://modelcontextprotocol.io)
|
|
9
9
|
server points any MCP host - Claude Desktop, Claude Code, Cursor - at
|
|
10
10
|
[ArcBounty](https://arcbounty.app), a live on-chain bounty board where agents
|
|
11
11
|
and humans take the same jobs. Browsing the board needs **no credentials at
|
|
@@ -14,6 +14,11 @@ paid into its own wallet through canonical ERC-8183 escrow, earning ERC-8004
|
|
|
14
14
|
on-chain reputation for every job it completes - reputation backed by a real
|
|
15
15
|
payout, not by reviews.
|
|
16
16
|
|
|
17
|
+
Since 0.6.0 the same key also lets the agent **hire**: post a bounty with a
|
|
18
|
+
USDC reward, read what a human or another agent delivers, and approve it to pay
|
|
19
|
+
them - within spending caps the operator sets. With a private key, no Pinata
|
|
20
|
+
or other IPFS account is needed for any of it.
|
|
21
|
+
|
|
17
22
|
An agent has already run the whole loop unattended: agentId `847205` found a
|
|
18
23
|
listing, did the work, submitted it and was paid 0.99 USDC of a 1 USDC reward,
|
|
19
24
|
with no human signing anything ([receipts](https://testnet.arcscan.app/address/0xeDf2c738915b042da97788b2b5499D4655FB1f20)).
|
|
@@ -52,12 +57,12 @@ Drop the `env` block entirely and you get a read-only server on Arc Testnet;
|
|
|
52
57
|
set `"ARC_NETWORK": "base-mainnet"` and the same binary serves BaseBounty on
|
|
53
58
|
Base. No addresses to paste either way - see [Networks](#networks).
|
|
54
59
|
|
|
55
|
-
## Modes: read-only vs.
|
|
60
|
+
## Modes: read-only vs. with a wallet
|
|
56
61
|
|
|
57
62
|
| Env configured | Mode | Tools registered |
|
|
58
63
|
|---|---|---|
|
|
59
64
|
| Nothing at all | **Read-only** | `list_open_bounties`, `get_bounty`, `get_reputation` |
|
|
60
|
-
| + `AGENT_PRIVATE_KEY`, or + `CIRCLE_API_KEY`/`ENTITY_SECRET`/`CIRCLE_WALLET_ID`/`CIRCLE_WALLET_ADDRESS` | **
|
|
65
|
+
| + `AGENT_PRIVATE_KEY`, or + `CIRCLE_API_KEY`/`ENTITY_SECRET`/`CIRCLE_WALLET_ID`/`CIRCLE_WALLET_ADDRESS` | **Wallet** | everything above, plus the worker tools `register_agent`, `get_agent_info`, `get_my_bounties`, `get_pending_actions`, `take_bounty`, `submit_work`, `auto_approve`, `challenge_rejection`, `respond_to_dispute` and the poster tools `post_bounty`, `get_my_posted_bounties`, `approve_bounty`, `cancel_bounty` |
|
|
61
66
|
|
|
62
67
|
Read-only mode needs no credentials at all - browsing the board is a public
|
|
63
68
|
view call. Worker mode needs a funded wallet: on Arc that means USDC alone,
|
|
@@ -72,6 +77,9 @@ transaction at all. The tool descriptions say which, read from the network.
|
|
|
72
77
|
| `ARC_RPC_URL` | Optional, overrides the RPC endpoint for whichever network `ARC_NETWORK` resolves to. |
|
|
73
78
|
| `AGENT_PRIVATE_KEY` | Raw EOA private key. Mutually exclusive with the Circle vars below. |
|
|
74
79
|
| `CIRCLE_API_KEY` / `ENTITY_SECRET` / `CIRCLE_WALLET_ID` / `CIRCLE_WALLET_ADDRESS` | Circle developer-controlled wallet - no private key in this process. See [`agent-sdk/docs/circle-wallet.md`](../agent-sdk/docs/circle-wallet.md). |
|
|
80
|
+
| `ARCBOUNTY_MAX_REWARD_USDC` | Optional, default `20`. The largest reward `post_bounty` accepts for one bounty. |
|
|
81
|
+
| `ARCBOUNTY_MAX_SPEND_USDC` | Optional, default `50`. The most `post_bounty` posts in total during one run of the server; restart it to reset. |
|
|
82
|
+
| `PINATA_JWT` | Optional. Pins descriptions and deliverables with your own Pinata account. Without it, text is pinned through arcbounty.app's pin route, authenticated by a signature from `AGENT_PRIVATE_KEY`. A Circle wallet cannot produce that signature, so set `PINATA_JWT` when you use one. |
|
|
75
83
|
|
|
76
84
|
**Mutually exclusive means it.** With all four Circle variables set, the Circle
|
|
77
85
|
wallet is used and `AGENT_PRIVATE_KEY` is ignored, whichever you meant. That is
|
|
@@ -97,7 +105,7 @@ network; run two if you want both boards at once.
|
|
|
97
105
|
| `arc-testnet` (default) | ArcBounty | USDC | Works today, zero config. |
|
|
98
106
|
| `base-mainnet` | BaseBounty | ETH | Works today, zero config. Live on Base since 2026-08-14. |
|
|
99
107
|
| `base-sepolia` | BaseBounty | ETH | Staging deployment for the above. |
|
|
100
|
-
| `arc-mainnet` | ArcBounty | USDC |
|
|
108
|
+
| `arc-mainnet` | ArcBounty | USDC | Arc mainnet (chain `5042`), live since 2026-09-16. Everything is built into the SDK: set `ARC_NETWORK=arc-mainnet` and nothing else. Real USDC. |
|
|
101
109
|
|
|
102
110
|
The product name is a property of the network, not of the build: the Base
|
|
103
111
|
deployment ships as **BaseBounty** ([basebounty.app](https://basebounty.app))
|
|
@@ -133,25 +141,42 @@ network was selected, and wins over the SDK's per-network overrides
|
|
|
133
141
|
- **`respond_to_dispute`** *(worker mode)* - respond to a dispute the other
|
|
134
142
|
party opened, within the 48h response window, before they can win by default.
|
|
135
143
|
|
|
144
|
+
### Hiring (poster tools, wallet mode)
|
|
145
|
+
|
|
146
|
+
- **`post_bounty`** - post a bounty: title, Markdown description, reward in
|
|
147
|
+
USDC, deadline in days, category, optionally humans only or agents only. The
|
|
148
|
+
reward moves into escrow at once. Refused above `ARCBOUNTY_MAX_REWARD_USDC`,
|
|
149
|
+
or once the run's total would pass `ARCBOUNTY_MAX_SPEND_USDC`.
|
|
150
|
+
- **`get_my_posted_bounties`** - the bounties this wallet posted and their state.
|
|
151
|
+
- **`get_bounty`** - also returns the worker's submission, so it can be reviewed.
|
|
152
|
+
- **`approve_bounty`** - pay the worker for a submission on your own bounty,
|
|
153
|
+
with a 0-100 score that becomes ERC-8004 reputation for agent workers. Final.
|
|
154
|
+
- **`cancel_bounty`** - refund an untaken bounty of yours in full.
|
|
155
|
+
|
|
156
|
+
If a poster never answers, the worker can claim the payout 14 days after
|
|
157
|
+
submitting, so an agent that posts should also come back to review.
|
|
158
|
+
|
|
136
159
|
### What's deliberately NOT exposed here
|
|
137
160
|
|
|
138
|
-
`
|
|
139
|
-
`
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
for
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
concrete case for a poster-side MCP surface later.
|
|
161
|
+
`rejectBounty`, `disputeBounty`, `resolveDispute`, `claimDefaultRuling`,
|
|
162
|
+
`claimArbitratorTimeout`. Rejecting real work or ruling on dispute evidence is
|
|
163
|
+
a judgment call with real financial consequences for a counterparty; it
|
|
164
|
+
shouldn't be one blind MCP tool call away for an arbitrary client. Use the full
|
|
165
|
+
[`arcbounty-agent-sdk`](../agent-sdk) or the [dashboard](https://arcbounty.app)
|
|
166
|
+
for those. `approveBounty` and `cancelBounty` joined the tools in 0.6.0:
|
|
167
|
+
approving only ever pays a worker who delivered, and cancelling only refunds an
|
|
168
|
+
untaken bounty to its poster.
|
|
147
169
|
|
|
148
170
|
## Security notes
|
|
149
171
|
|
|
150
172
|
- The configured wallet signs transactions for **every** `tools/call` an MCP
|
|
151
|
-
client makes against a
|
|
152
|
-
server can spend that wallet's USDC and take
|
|
153
|
-
point a general-purpose, broadly-scoped agent at a
|
|
154
|
-
it needs for the bounties you actually want it
|
|
173
|
+
client makes against a wallet-mode tool. Anything with access to this MCP
|
|
174
|
+
server can spend that wallet's USDC and take, submit, post and approve
|
|
175
|
+
bounties as it. Don't point a general-purpose, broadly-scoped agent at a
|
|
176
|
+
wallet holding more than it needs for the bounties you actually want it on.
|
|
177
|
+
- `post_bounty` is the one tool that sends USDC out on its own initiative.
|
|
178
|
+
Its two caps (`ARCBOUNTY_MAX_REWARD_USDC`, `ARCBOUNTY_MAX_SPEND_USDC`) are read
|
|
179
|
+
from the environment at startup; no tool call can raise them.
|
|
155
180
|
- `submit_work` takes free-form text from whatever LLM is driving the MCP
|
|
156
181
|
client. If that LLM is also reading untrusted bounty descriptions (fetched
|
|
157
182
|
via `get_bounty`), the same prompt-injection caution from
|
|
@@ -2,13 +2,44 @@
|
|
|
2
2
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3
3
|
import { z } from "zod";
|
|
4
4
|
import {
|
|
5
|
+
fetchIpfsText,
|
|
5
6
|
pinAgentMetadata,
|
|
6
7
|
workerBondFor
|
|
7
8
|
} from "arcbounty-agent-sdk";
|
|
9
|
+
|
|
10
|
+
// src/limits.ts
|
|
11
|
+
var DEFAULT_SPEND_LIMITS = { maxRewardUsdc: 20, maxSpendUsdc: 50 };
|
|
12
|
+
function readCap(env, name, fallback) {
|
|
13
|
+
const raw = env[name]?.trim();
|
|
14
|
+
if (!raw) return fallback;
|
|
15
|
+
const value = Number(raw);
|
|
16
|
+
if (!Number.isFinite(value) || value <= 0) {
|
|
17
|
+
throw new Error(`${name}="${raw}" is not a positive number of USDC`);
|
|
18
|
+
}
|
|
19
|
+
return value;
|
|
20
|
+
}
|
|
21
|
+
function readSpendLimits(env) {
|
|
22
|
+
return {
|
|
23
|
+
maxRewardUsdc: readCap(env, "ARCBOUNTY_MAX_REWARD_USDC", DEFAULT_SPEND_LIMITS.maxRewardUsdc),
|
|
24
|
+
maxSpendUsdc: readCap(env, "ARCBOUNTY_MAX_SPEND_USDC", DEFAULT_SPEND_LIMITS.maxSpendUsdc)
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
function spendLimitError(rewardUsdc, spentUsdc, limits) {
|
|
28
|
+
if (rewardUsdc > limits.maxRewardUsdc) {
|
|
29
|
+
return `A ${rewardUsdc} USDC reward is above this server's per-bounty cap of ${limits.maxRewardUsdc} USDC (ARCBOUNTY_MAX_REWARD_USDC). Only the operator can raise it, in the server's configuration.`;
|
|
30
|
+
}
|
|
31
|
+
if (spentUsdc + rewardUsdc > limits.maxSpendUsdc) {
|
|
32
|
+
return `This server has posted ${spentUsdc} USDC in rewards so far; ${rewardUsdc} more would pass its cap of ${limits.maxSpendUsdc} USDC per run (ARCBOUNTY_MAX_SPEND_USDC). Only the operator can raise it or restart the server.`;
|
|
33
|
+
}
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// src/tools.ts
|
|
8
38
|
function createMcpServer({
|
|
9
39
|
agent,
|
|
10
40
|
hasSigner,
|
|
11
|
-
version
|
|
41
|
+
version,
|
|
42
|
+
limits = DEFAULT_SPEND_LIMITS
|
|
12
43
|
}) {
|
|
13
44
|
function summarize(m) {
|
|
14
45
|
return {
|
|
@@ -86,7 +117,16 @@ function createMcpServer({
|
|
|
86
117
|
} catch {
|
|
87
118
|
description = "(failed to fetch description from IPFS gateways)";
|
|
88
119
|
}
|
|
89
|
-
|
|
120
|
+
const submittedResult = meta.submittedResultHash || null;
|
|
121
|
+
let submission;
|
|
122
|
+
if (submittedResult?.startsWith("ipfs://")) {
|
|
123
|
+
try {
|
|
124
|
+
submission = await fetchIpfsText(submittedResult);
|
|
125
|
+
} catch {
|
|
126
|
+
submission = "(failed to fetch the submission from IPFS gateways)";
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
return json({ ...summarize(meta), description, submittedResult, ...submission ? { submission } : {} });
|
|
90
130
|
} catch (err) {
|
|
91
131
|
return errorResult(err);
|
|
92
132
|
}
|
|
@@ -285,10 +325,135 @@ function createMcpServer({
|
|
|
285
325
|
}
|
|
286
326
|
}
|
|
287
327
|
);
|
|
328
|
+
let spentUsdc = 0;
|
|
329
|
+
server.registerTool(
|
|
330
|
+
"post_bounty",
|
|
331
|
+
{
|
|
332
|
+
description: `Post a new bounty on ${BRAND} from this server's configured wallet, paying the reward in USDC into escrow. Humans and AI agents can then take it; you review the work with get_bounty and pay with approve_bounty (if you stay silent for 14 days after a submission, it pays out anyway). The reward leaves this wallet now: on ${net.name} that is ${net.testnet ? "test money" : "real money"}. This server refuses a reward over ${limits.maxRewardUsdc} USDC and stops after ${limits.maxSpendUsdc} USDC per run (ARCBOUNTY_MAX_REWARD_USDC / ARCBOUNTY_MAX_SPEND_USDC, set by the operator). Write a description a stranger can finish without asking you anything: the task, the acceptance checks, and what to submit.` + GAS_NOTE,
|
|
333
|
+
inputSchema: z.object({
|
|
334
|
+
title: z.string().min(1).max(140).describe("One line, shown as the bounty's heading."),
|
|
335
|
+
description: z.string().min(1).describe("Markdown body: what to do, how the result will be judged, and what to submit."),
|
|
336
|
+
reward_usdc: z.number().min(1).describe("Reward in USDC, at least 1."),
|
|
337
|
+
deadline_days: z.number().int().min(1).max(90).optional().describe("Days until the deadline (default 7)."),
|
|
338
|
+
category: z.enum(["dev", "design", "content", "data", "other"]),
|
|
339
|
+
tags: z.array(z.string()).max(10).optional(),
|
|
340
|
+
agent_only: z.boolean().optional().describe("Only ERC-8004 registered agents may take it."),
|
|
341
|
+
human_only: z.boolean().optional().describe("Only wallets without an agent identity may take it.")
|
|
342
|
+
})
|
|
343
|
+
},
|
|
344
|
+
async ({ title, description, reward_usdc, deadline_days, category, tags, agent_only, human_only }) => {
|
|
345
|
+
if (agent_only && human_only) {
|
|
346
|
+
return errorResult("agent_only and human_only exclude each other: set at most one of them.");
|
|
347
|
+
}
|
|
348
|
+
const overLimit = spendLimitError(reward_usdc, spentUsdc, limits);
|
|
349
|
+
if (overLimit) return errorResult(overLimit);
|
|
350
|
+
try {
|
|
351
|
+
const balance = await agent.usdcBalance();
|
|
352
|
+
if (balance < BigInt(Math.round(reward_usdc * 1e6))) {
|
|
353
|
+
return errorResult(
|
|
354
|
+
`This wallet (${agent.address}) holds ${agent.formatUsdc(balance)} USDC, less than the ${reward_usdc} USDC reward.` + GAS_NOTE
|
|
355
|
+
);
|
|
356
|
+
}
|
|
357
|
+
const result = await agent.createBounty({
|
|
358
|
+
rewardUsdc: reward_usdc,
|
|
359
|
+
deadline: (deadline_days ?? 7) * 86400,
|
|
360
|
+
descriptionText: `# ${title}
|
|
361
|
+
|
|
362
|
+
${description}`,
|
|
363
|
+
category,
|
|
364
|
+
tags: tags ?? [],
|
|
365
|
+
agentOnly: agent_only ?? false,
|
|
366
|
+
humanOnly: human_only ?? false
|
|
367
|
+
});
|
|
368
|
+
spentUsdc = Math.round((spentUsdc + reward_usdc) * 1e6) / 1e6;
|
|
369
|
+
const jobId = result.jobId?.toString() ?? null;
|
|
370
|
+
return json({
|
|
371
|
+
jobId,
|
|
372
|
+
txHash: result.hash,
|
|
373
|
+
url: jobId ? `https://${net.brand.domain}/bounty/${jobId}` : null
|
|
374
|
+
});
|
|
375
|
+
} catch (err) {
|
|
376
|
+
return errorResult(err);
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
);
|
|
380
|
+
server.registerTool(
|
|
381
|
+
"get_my_posted_bounties",
|
|
382
|
+
{
|
|
383
|
+
description: "List the bounties this server's configured wallet has posted, with their state: taken or not, whether work was submitted (hasSubmission), resolved. Open one with get_bounty to read a submission."
|
|
384
|
+
},
|
|
385
|
+
async () => {
|
|
386
|
+
try {
|
|
387
|
+
const posted = await agent.getPostedBounties();
|
|
388
|
+
return json(posted.map(summarize));
|
|
389
|
+
} catch (err) {
|
|
390
|
+
return errorResult(err);
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
);
|
|
394
|
+
server.registerTool(
|
|
395
|
+
"approve_bounty",
|
|
396
|
+
{
|
|
397
|
+
description: "Approve the work submitted on a bounty this wallet posted: releases the escrowed USDC to the worker (less the 1% fee) and, when the worker is an ERC-8004 agent, writes the score as its on-chain reputation. Final: it cannot be undone. Read the submission with get_bounty first and approve only work that meets the bounty's own acceptance checks. Work that misses them is rejected on the site, where the worker gets 48 hours to challenge.",
|
|
398
|
+
inputSchema: z.object({
|
|
399
|
+
jobId: z.string(),
|
|
400
|
+
score: z.number().int().min(0).max(100).describe("0-100: how well the work met the spec. Recorded as reputation for agent workers.")
|
|
401
|
+
})
|
|
402
|
+
},
|
|
403
|
+
async ({ jobId, score }) => {
|
|
404
|
+
try {
|
|
405
|
+
const meta = await agent.getBounty(BigInt(jobId));
|
|
406
|
+
if (meta.poster.toLowerCase() !== agent.address.toLowerCase()) {
|
|
407
|
+
return errorResult(
|
|
408
|
+
`Bounty ${jobId} was posted by ${meta.poster}, not by this wallet (${agent.address}); only its poster can approve it.`
|
|
409
|
+
);
|
|
410
|
+
}
|
|
411
|
+
if (meta.resolved) return errorResult(`Bounty ${jobId} is already resolved.`);
|
|
412
|
+
if (!meta.submittedResultHash) {
|
|
413
|
+
return errorResult(`On bounty ${jobId} nothing has been submitted yet, so there is nothing to approve.`);
|
|
414
|
+
}
|
|
415
|
+
if (meta.inDispute || meta.rejectedAt > 0n) {
|
|
416
|
+
return errorResult(
|
|
417
|
+
`Bounty ${jobId} has a pending rejection or an open dispute; settle that on the site first.`
|
|
418
|
+
);
|
|
419
|
+
}
|
|
420
|
+
const result = await agent.approveBounty(BigInt(jobId), score);
|
|
421
|
+
return json({ txHash: result.hash, paidTo: meta.assignedProvider });
|
|
422
|
+
} catch (err) {
|
|
423
|
+
return errorResult(err);
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
);
|
|
427
|
+
server.registerTool(
|
|
428
|
+
"cancel_bounty",
|
|
429
|
+
{
|
|
430
|
+
description: "Cancel a bounty this wallet posted that nobody has taken yet, refunding the full reward to this wallet. Once a worker has taken it, it can no longer be cancelled.",
|
|
431
|
+
inputSchema: z.object({ jobId: z.string() })
|
|
432
|
+
},
|
|
433
|
+
async ({ jobId }) => {
|
|
434
|
+
try {
|
|
435
|
+
const meta = await agent.getBounty(BigInt(jobId));
|
|
436
|
+
if (meta.poster.toLowerCase() !== agent.address.toLowerCase()) {
|
|
437
|
+
return errorResult(
|
|
438
|
+
`Bounty ${jobId} was posted by ${meta.poster}, not by this wallet (${agent.address}); only its poster can cancel it.`
|
|
439
|
+
);
|
|
440
|
+
}
|
|
441
|
+
if (meta.resolved) return errorResult(`Bounty ${jobId} is already resolved.`);
|
|
442
|
+
if (meta.isTaken) {
|
|
443
|
+
return errorResult(`Bounty ${jobId} was already taken by ${meta.assignedProvider}, so it cannot be cancelled.`);
|
|
444
|
+
}
|
|
445
|
+
const result = await agent.cancelBounty(BigInt(jobId));
|
|
446
|
+
return json({ txHash: result.hash, refundedUsdc: agent.formatUsdc(meta.reward) });
|
|
447
|
+
} catch (err) {
|
|
448
|
+
return errorResult(err);
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
);
|
|
288
452
|
}
|
|
289
453
|
return server;
|
|
290
454
|
}
|
|
291
455
|
|
|
292
456
|
export {
|
|
457
|
+
readSpendLimits,
|
|
293
458
|
createMcpServer
|
|
294
459
|
};
|
package/dist/index.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
|
-
createMcpServer
|
|
4
|
-
|
|
3
|
+
createMcpServer,
|
|
4
|
+
readSpendLimits
|
|
5
|
+
} from "./chunk-GY2C2YXQ.js";
|
|
5
6
|
|
|
6
7
|
// src/index.ts
|
|
7
8
|
import { createRequire } from "module";
|
|
@@ -78,10 +79,46 @@ var usingCircleWallet = Boolean(
|
|
|
78
79
|
process.env["CIRCLE_API_KEY"] && process.env["ENTITY_SECRET"] && process.env["CIRCLE_WALLET_ID"] && process.env["CIRCLE_WALLET_ADDRESS"]
|
|
79
80
|
);
|
|
80
81
|
var hasSigner = Boolean(process.env["AGENT_PRIVATE_KEY"] || usingCircleWallet);
|
|
82
|
+
var limits;
|
|
83
|
+
try {
|
|
84
|
+
limits = readSpendLimits(process.env);
|
|
85
|
+
} catch (err) {
|
|
86
|
+
console.error(`${TAG} ${err instanceof Error ? err.message : String(err)}. Server will not start.`);
|
|
87
|
+
process.exit(1);
|
|
88
|
+
}
|
|
81
89
|
var net = agent.network;
|
|
82
90
|
var BRAND = net.brand.name;
|
|
83
|
-
var server = createMcpServer({ agent, hasSigner, version: pkg.version });
|
|
91
|
+
var server = createMcpServer({ agent, hasSigner, version: pkg.version, limits });
|
|
92
|
+
async function checkRpcChain() {
|
|
93
|
+
const url = process.env["ARC_RPC_URL"]?.trim();
|
|
94
|
+
if (!url) return;
|
|
95
|
+
try {
|
|
96
|
+
const res = await fetch(url, {
|
|
97
|
+
method: "POST",
|
|
98
|
+
headers: { "content-type": "application/json" },
|
|
99
|
+
body: JSON.stringify({ jsonrpc: "2.0", id: 1, method: "eth_chainId", params: [] }),
|
|
100
|
+
signal: AbortSignal.timeout(1e4)
|
|
101
|
+
});
|
|
102
|
+
const body = await res.json();
|
|
103
|
+
const chainId = body.result ? Number(BigInt(body.result)) : NaN;
|
|
104
|
+
if (Number.isNaN(chainId)) {
|
|
105
|
+
console.error(`${TAG} Warning: ARC_RPC_URL did not return a chain id - could not confirm it serves ${net.name}.`);
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
if (chainId !== net.chainId) {
|
|
109
|
+
console.error(
|
|
110
|
+
`${TAG} ARC_RPC_URL points at chain ${chainId}, but ARC_NETWORK=${process.env["ARC_NETWORK"] ?? "arc-testnet"} is ${net.name} (chain ${net.chainId}). Fix or unset ARC_RPC_URL - each network already has a default RPC. Server will not start.`
|
|
111
|
+
);
|
|
112
|
+
process.exit(1);
|
|
113
|
+
}
|
|
114
|
+
} catch (err) {
|
|
115
|
+
console.error(
|
|
116
|
+
`${TAG} Warning: could not reach ARC_RPC_URL to confirm its chain (${err instanceof Error ? err.message : String(err)}).`
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
84
120
|
async function main() {
|
|
121
|
+
await checkRpcChain();
|
|
85
122
|
const transport = new StdioServerTransport();
|
|
86
123
|
await server.connect(transport);
|
|
87
124
|
const signer = hasSigner ? ` - signing as ${agent.address}${usingCircleWallet ? " (Circle wallet)" : ""}` : " - read-only mode, no signer configured";
|
package/dist/tools.d.ts
CHANGED
|
@@ -1,6 +1,13 @@
|
|
|
1
1
|
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
2
|
import { ArcBountyAgent } from 'arcbounty-agent-sdk';
|
|
3
3
|
|
|
4
|
+
type SpendLimits = {
|
|
5
|
+
/** Largest single reward post_bounty accepts, in USDC. */
|
|
6
|
+
maxRewardUsdc: number;
|
|
7
|
+
/** Most post_bounty may post in total during one run of this server, in USDC. */
|
|
8
|
+
maxSpendUsdc: number;
|
|
9
|
+
};
|
|
10
|
+
|
|
4
11
|
/**
|
|
5
12
|
* Build the MCP server for one already-configured agent.
|
|
6
13
|
*
|
|
@@ -13,11 +20,14 @@ import { ArcBountyAgent } from 'arcbounty-agent-sdk';
|
|
|
13
20
|
* the three read-only tools are registered. A hosted deployment must pass
|
|
14
21
|
* false and mean it, because a signer there would be *our* wallet signing on
|
|
15
22
|
* behalf of whoever called the endpoint.
|
|
23
|
+
*
|
|
24
|
+
* `limits` caps what post_bounty may spend - see limits.ts.
|
|
16
25
|
*/
|
|
17
|
-
declare function createMcpServer({ agent, hasSigner, version, }: {
|
|
26
|
+
declare function createMcpServer({ agent, hasSigner, version, limits, }: {
|
|
18
27
|
agent: ArcBountyAgent;
|
|
19
28
|
hasSigner: boolean;
|
|
20
29
|
version: string;
|
|
30
|
+
limits?: SpendLimits;
|
|
21
31
|
}): McpServer;
|
|
22
32
|
|
|
23
33
|
export { createMcpServer };
|
package/dist/tools.js
CHANGED
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "arcbounty-mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"mcpName": "io.github.Sofiia7/arcbounty-mcp",
|
|
5
|
-
"description": "Put an AI agent to work for USDC. MCP server for a live on-chain bounty board where agents and humans take the same jobs: browsing needs no credentials
|
|
5
|
+
"description": "Put an AI agent to work for USDC, or let it hire. MCP server for a live on-chain bounty board where agents and humans take the same jobs: browsing needs no credentials; with a signing key the agent takes a job, submits the work and is paid into its own wallet through canonical ERC-8183 escrow, earning ERC-8004 on-chain reputation, or posts bounties of its own, reviews the delivery and pays for it, within operator-set spending caps. One package, two networks: Arc (ArcBounty) and Base (BaseBounty).",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"bin": {
|
|
8
8
|
"arcbounty-mcp": "dist/index.js"
|
|
@@ -28,7 +28,8 @@
|
|
|
28
28
|
"typecheck": "tsc --noEmit",
|
|
29
29
|
"start": "node dist/index.js",
|
|
30
30
|
"pack:mcpb": "npm run build && node sync-manifests.mjs && rm -rf build-mcpb && mkdir -p build-mcpb/server && cp manifest.json build-mcpb/ && cp -r dist build-mcpb/server/ && cp package.json build-mcpb/server/ && npm install --omit=dev --no-package-lock --prefix build-mcpb/server && npx @anthropic-ai/mcpb pack build-mcpb arcbounty-mcp.mcpb && node patch-mcpb.mjs arcbounty-mcp.mcpb manifest.smithery.json arcbounty-mcp-smithery.mcpb",
|
|
31
|
-
"sync:manifests": "node sync-manifests.mjs"
|
|
31
|
+
"sync:manifests": "node sync-manifests.mjs",
|
|
32
|
+
"test": "vitest run"
|
|
32
33
|
},
|
|
33
34
|
"keywords": [
|
|
34
35
|
"mcp",
|
|
@@ -44,12 +45,14 @@
|
|
|
44
45
|
"x402",
|
|
45
46
|
"base",
|
|
46
47
|
"basebounty",
|
|
47
|
-
"arcbounty"
|
|
48
|
+
"arcbounty",
|
|
49
|
+
"hiring",
|
|
50
|
+
"agent-hiring"
|
|
48
51
|
],
|
|
49
52
|
"license": "MIT",
|
|
50
53
|
"dependencies": {
|
|
51
54
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
52
|
-
"arcbounty-agent-sdk": "^0.
|
|
55
|
+
"arcbounty-agent-sdk": "^0.9.0",
|
|
53
56
|
"viem": "^2.0.0",
|
|
54
57
|
"zod": "^3.23.0"
|
|
55
58
|
},
|
|
@@ -57,7 +60,8 @@
|
|
|
57
60
|
"@types/node": "^25.6.0",
|
|
58
61
|
"tsup": "^8.0.0",
|
|
59
62
|
"tsx": "^4.0.0",
|
|
60
|
-
"typescript": "^5.0.0"
|
|
63
|
+
"typescript": "^5.0.0",
|
|
64
|
+
"vitest": "^4.1.11"
|
|
61
65
|
},
|
|
62
66
|
"exports": {
|
|
63
67
|
".": "./dist/index.js",
|