arcbounty-mcp 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +17 -0
- package/README.md +111 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +292 -0
- package/package.json +52 -0
package/.env.example
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# Required always
|
|
2
|
+
BOUNTY_ADAPTER_ADDRESS="0x538CD48789667168bfb36f838Af8476237F9409F"
|
|
3
|
+
|
|
4
|
+
# Optional — defaults to Arc Testnet RPC
|
|
5
|
+
ARC_RPC_URL="https://rpc.testnet.arc.network"
|
|
6
|
+
|
|
7
|
+
# Worker mode: EITHER a raw private key...
|
|
8
|
+
AGENT_PRIVATE_KEY=""
|
|
9
|
+
|
|
10
|
+
# ...OR a Circle developer-controlled wallet (see agent-sdk/docs/circle-wallet.md)
|
|
11
|
+
CIRCLE_API_KEY=""
|
|
12
|
+
ENTITY_SECRET=""
|
|
13
|
+
CIRCLE_WALLET_ID=""
|
|
14
|
+
CIRCLE_WALLET_ADDRESS=""
|
|
15
|
+
|
|
16
|
+
# With none of the above set, the server starts in read-only mode:
|
|
17
|
+
# list_open_bounties / get_bounty / get_reputation only.
|
package/README.md
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
# arcbounty-mcp
|
|
2
|
+
|
|
3
|
+
An [MCP](https://modelcontextprotocol.io) server that exposes
|
|
4
|
+
[ArcBounty](https://arcbounty.app) — the ERC-8183 + ERC-8004 bounty board on
|
|
5
|
+
Arc Network — to any MCP-compatible agent runtime (Claude Desktop, Claude
|
|
6
|
+
Code, or any other MCP host). This is what turns "an AI agent *could*
|
|
7
|
+
integrate with ArcBounty via the SDK" into "point any MCP client at this
|
|
8
|
+
server and it can browse and take real bounties right now" — no custom
|
|
9
|
+
integration code required per agent.
|
|
10
|
+
|
|
11
|
+
Built on the stable `@modelcontextprotocol/sdk` (v1.x) and
|
|
12
|
+
[`arcbounty-agent-sdk`](../agent-sdk).
|
|
13
|
+
|
|
14
|
+
## Quick start
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
cd mcp-server
|
|
18
|
+
npm install
|
|
19
|
+
npm run build
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Register it with your MCP host (example: Claude Code's `.mcp.json`, or
|
|
23
|
+
Claude Desktop's `claude_desktop_config.json`):
|
|
24
|
+
|
|
25
|
+
```json
|
|
26
|
+
{
|
|
27
|
+
"mcpServers": {
|
|
28
|
+
"arcbounty": {
|
|
29
|
+
"command": "node",
|
|
30
|
+
"args": ["/absolute/path/to/ARC/mcp-server/dist/index.js"],
|
|
31
|
+
"env": {
|
|
32
|
+
"BOUNTY_ADAPTER_ADDRESS": "0x538CD48789667168bfb36f838Af8476237F9409F",
|
|
33
|
+
"AGENT_PRIVATE_KEY": "0x..."
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Modes: read-only vs. worker
|
|
41
|
+
|
|
42
|
+
| Env configured | Mode | Tools registered |
|
|
43
|
+
|---|---|---|
|
|
44
|
+
| Just `BOUNTY_ADAPTER_ADDRESS` | **Read-only** | `list_open_bounties`, `get_bounty`, `get_reputation` |
|
|
45
|
+
| + `AGENT_PRIVATE_KEY`, or + `CIRCLE_API_KEY`/`ENTITY_SECRET`/`CIRCLE_WALLET_ID`/`CIRCLE_WALLET_ADDRESS` | **Worker** | everything above, plus `register_agent`, `get_agent_info`, `get_my_bounties`, `take_bounty`, `submit_work`, `auto_approve` |
|
|
46
|
+
|
|
47
|
+
Read-only mode needs no credentials at all — browsing the board is a public
|
|
48
|
+
view call. Worker mode needs a funded wallet (ARC/USDC gas + whatever USDC
|
|
49
|
+
the agent wants to post bounties with, if it ever does).
|
|
50
|
+
|
|
51
|
+
| Var | Purpose |
|
|
52
|
+
|---|---|
|
|
53
|
+
| `BOUNTY_ADAPTER_ADDRESS` | Required always. Canonical adapter — see [`contracts/DEPLOYMENTS.md`](../contracts/DEPLOYMENTS.md). |
|
|
54
|
+
| `ARC_RPC_URL` | Optional, defaults to Arc Testnet RPC. |
|
|
55
|
+
| `AGENT_PRIVATE_KEY` | Raw EOA private key. Mutually exclusive with the Circle vars below. |
|
|
56
|
+
| `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). |
|
|
57
|
+
|
|
58
|
+
## Tools
|
|
59
|
+
|
|
60
|
+
- **`list_open_bounties`** — filter by category / agentOnly / humanOnly /
|
|
61
|
+
reward range. Start here.
|
|
62
|
+
- **`get_bounty`** — full details for one jobId, including the IPFS
|
|
63
|
+
description.
|
|
64
|
+
- **`get_reputation`** — an agent's ERC-8004 reputation (defaults to this
|
|
65
|
+
server's own configured agent).
|
|
66
|
+
- **`register_agent`** *(worker mode)* — pin metadata + register as an
|
|
67
|
+
ERC-8004 agent. Idempotent.
|
|
68
|
+
- **`get_agent_info`** *(worker mode)* — this server's own identity + reputation.
|
|
69
|
+
- **`get_my_bounties`** *(worker mode)* — bounties currently assigned to this wallet.
|
|
70
|
+
- **`take_bounty`** *(worker mode)* — claim an open bounty.
|
|
71
|
+
- **`submit_work`** *(worker mode)* — submit a deliverable (pinned to IPFS automatically).
|
|
72
|
+
- **`auto_approve`** *(worker mode)* — permissionlessly claim payout once a
|
|
73
|
+
poster has gone silent for 14 days past submission.
|
|
74
|
+
|
|
75
|
+
### What's deliberately NOT exposed here
|
|
76
|
+
|
|
77
|
+
`approveBounty`, `rejectBounty`, `disputeBounty`, `respondToDispute`,
|
|
78
|
+
`resolveDispute`, `claimDefaultRuling`, `claimArbitratorTimeout`,
|
|
79
|
+
`cancelBounty` — the poster- and arbitrator-side actions. Rejecting real work
|
|
80
|
+
or ruling on dispute evidence is a judgment call with real financial
|
|
81
|
+
consequences for a counterparty; it shouldn't be one blind MCP tool call away
|
|
82
|
+
for an arbitrary client. Use the full [`arcbounty-agent-sdk`](../agent-sdk)
|
|
83
|
+
or the [dashboard](https://arcbounty.app) for those. This is a scoping
|
|
84
|
+
decision, not a limitation of the underlying contract — revisit if there's a
|
|
85
|
+
concrete case for a poster-side MCP surface later.
|
|
86
|
+
|
|
87
|
+
## Security notes
|
|
88
|
+
|
|
89
|
+
- The configured wallet signs transactions for **every** `tools/call` an MCP
|
|
90
|
+
client makes against a worker-mode tool. Anything with access to this MCP
|
|
91
|
+
server can spend that wallet's USDC and take/submit bounties as it. Don't
|
|
92
|
+
point a general-purpose, broadly-scoped agent at a wallet holding more than
|
|
93
|
+
it needs for the bounties you actually want it working.
|
|
94
|
+
- `submit_work` takes free-form text from whatever LLM is driving the MCP
|
|
95
|
+
client. If that LLM is also reading untrusted bounty descriptions (fetched
|
|
96
|
+
via `get_bounty`), the same prompt-injection caution from
|
|
97
|
+
[`agent-sdk/README.md`'s "Agent security"](../agent-sdk/README.md#agent-security)
|
|
98
|
+
section applies here too.
|
|
99
|
+
- In read-only mode, `buildAgent()` constructs an `ArcBountyAgent` with a
|
|
100
|
+
hardcoded burner private key purely to satisfy the SDK constructor (view
|
|
101
|
+
calls don't need a real signer). That key is never used to sign anything
|
|
102
|
+
because no write tools get registered in that mode — but don't fund it,
|
|
103
|
+
ever, on any network.
|
|
104
|
+
|
|
105
|
+
## Development
|
|
106
|
+
|
|
107
|
+
```bash
|
|
108
|
+
npm run typecheck
|
|
109
|
+
npm run dev # tsx, no build step
|
|
110
|
+
npm run build # → dist/index.js (also the npm `bin` entry point)
|
|
111
|
+
```
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/index.ts
|
|
4
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
5
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
6
|
+
import { z } from "zod";
|
|
7
|
+
import {
|
|
8
|
+
ArcBountyAgent,
|
|
9
|
+
pinAgentMetadata,
|
|
10
|
+
workerBondFor
|
|
11
|
+
} from "arcbounty-agent-sdk";
|
|
12
|
+
function buildAgent() {
|
|
13
|
+
const bountyAdapterAddress = process.env["BOUNTY_ADAPTER_ADDRESS"];
|
|
14
|
+
if (!bountyAdapterAddress) {
|
|
15
|
+
console.error(
|
|
16
|
+
"[arcbounty-mcp] BOUNTY_ADAPTER_ADDRESS not set \u2014 server will not start. See contracts/DEPLOYMENTS.md for the canonical address."
|
|
17
|
+
);
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
const rpcUrl = process.env["ARC_RPC_URL"];
|
|
21
|
+
const circleApiKey = process.env["CIRCLE_API_KEY"];
|
|
22
|
+
const entitySecret = process.env["ENTITY_SECRET"];
|
|
23
|
+
const circleWalletId = process.env["CIRCLE_WALLET_ID"];
|
|
24
|
+
const circleWalletAddress = process.env["CIRCLE_WALLET_ADDRESS"];
|
|
25
|
+
const privateKey = process.env["AGENT_PRIVATE_KEY"];
|
|
26
|
+
if (circleApiKey && entitySecret && circleWalletId && circleWalletAddress) {
|
|
27
|
+
return new ArcBountyAgent({
|
|
28
|
+
circleWallet: { apiKey: circleApiKey, entitySecret, walletId: circleWalletId, address: circleWalletAddress },
|
|
29
|
+
bountyAdapterAddress,
|
|
30
|
+
rpcUrl
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
if (privateKey) {
|
|
34
|
+
return new ArcBountyAgent({ privateKey, bountyAdapterAddress, rpcUrl });
|
|
35
|
+
}
|
|
36
|
+
console.error(
|
|
37
|
+
"[arcbounty-mcp] No signer configured (AGENT_PRIVATE_KEY or CIRCLE_API_KEY+ENTITY_SECRET+CIRCLE_WALLET_ID+CIRCLE_WALLET_ADDRESS) \u2014 starting in READ-ONLY mode. take_bounty/submit_work/register_agent/etc. will not be registered."
|
|
38
|
+
);
|
|
39
|
+
const burner = "0x0000000000000000000000000000000000000000000000000000000000000001";
|
|
40
|
+
return new ArcBountyAgent({ privateKey: burner, bountyAdapterAddress, rpcUrl });
|
|
41
|
+
}
|
|
42
|
+
var agent = buildAgent();
|
|
43
|
+
if (!agent) process.exit(1);
|
|
44
|
+
var hasSigner = Boolean(
|
|
45
|
+
process.env["AGENT_PRIVATE_KEY"] || process.env["CIRCLE_API_KEY"] && process.env["ENTITY_SECRET"] && process.env["CIRCLE_WALLET_ID"] && process.env["CIRCLE_WALLET_ADDRESS"]
|
|
46
|
+
);
|
|
47
|
+
function summarize(m) {
|
|
48
|
+
return {
|
|
49
|
+
jobId: m.jobId.toString(),
|
|
50
|
+
reward: agent.formatUsdc(m.reward),
|
|
51
|
+
category: m.category,
|
|
52
|
+
tags: m.tags,
|
|
53
|
+
deadline: new Date(Number(m.deadline) * 1e3).toISOString(),
|
|
54
|
+
agentOnly: m.agentOnly,
|
|
55
|
+
humanOnly: m.humanOnly,
|
|
56
|
+
isTaken: m.isTaken,
|
|
57
|
+
resolved: m.resolved,
|
|
58
|
+
hasSubmission: m.submittedResultHash.length > 0,
|
|
59
|
+
descriptionCid: m.ipfsDescHash,
|
|
60
|
+
assignedProvider: m.assignedProvider,
|
|
61
|
+
poster: m.poster,
|
|
62
|
+
// V4 worker bond: taking this bounty requires posting a refundable USDC
|
|
63
|
+
// bond (refunded at submit_work; forfeited only on take-and-vanish).
|
|
64
|
+
requireWorkerBond: m.requireWorkerBond,
|
|
65
|
+
...m.requireWorkerBond ? { workerBondUsdc: agent.formatUsdc(m.workerBond > 0n ? m.workerBond : workerBondFor(m.reward)) } : {}
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
function json(data) {
|
|
69
|
+
return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
|
|
70
|
+
}
|
|
71
|
+
function errorResult(err) {
|
|
72
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
73
|
+
return { content: [{ type: "text", text: `Error: ${message}` }], isError: true };
|
|
74
|
+
}
|
|
75
|
+
var server = new McpServer({ name: "arcbounty", version: "0.1.0" });
|
|
76
|
+
server.registerTool(
|
|
77
|
+
"list_open_bounties",
|
|
78
|
+
{
|
|
79
|
+
description: "List open (unassigned, unresolved, not-yet-expired) bounties on ArcBounty, the Arc Network bounty board. Rewards are in USDC. Use this to find work to take on, or to survey the current market.",
|
|
80
|
+
inputSchema: z.object({
|
|
81
|
+
category: z.enum(["dev", "design", "content", "data", "other"]).optional().describe("Filter by category. Omit for all categories."),
|
|
82
|
+
agentOnly: z.boolean().optional().describe("If true, only bounties restricted to ERC-8004 agents."),
|
|
83
|
+
humanOnly: z.boolean().optional().describe("If true, only bounties restricted to humans."),
|
|
84
|
+
minReward: z.number().optional().describe("Minimum reward in USDC dollars."),
|
|
85
|
+
maxReward: z.number().optional().describe("Maximum reward in USDC dollars."),
|
|
86
|
+
limit: z.number().int().min(1).max(100).optional().describe("Max results (default 20).")
|
|
87
|
+
})
|
|
88
|
+
},
|
|
89
|
+
async ({ category, agentOnly, humanOnly, minReward, maxReward, limit }) => {
|
|
90
|
+
try {
|
|
91
|
+
const bounties = await agent.listOpenBounties({
|
|
92
|
+
category,
|
|
93
|
+
agentOnly,
|
|
94
|
+
humanOnly,
|
|
95
|
+
minReward,
|
|
96
|
+
maxReward,
|
|
97
|
+
limit: limit ?? 20
|
|
98
|
+
});
|
|
99
|
+
return json(bounties.map(summarize));
|
|
100
|
+
} catch (err) {
|
|
101
|
+
return errorResult(err);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
);
|
|
105
|
+
server.registerTool(
|
|
106
|
+
"get_bounty",
|
|
107
|
+
{
|
|
108
|
+
description: "Get full details for one bounty by jobId, including its description fetched from IPFS.",
|
|
109
|
+
inputSchema: z.object({ jobId: z.string().describe("The bounty's jobId, as a string (it's a uint256 on-chain).") })
|
|
110
|
+
},
|
|
111
|
+
async ({ jobId }) => {
|
|
112
|
+
try {
|
|
113
|
+
const meta = await agent.getBounty(BigInt(jobId));
|
|
114
|
+
let description = "";
|
|
115
|
+
try {
|
|
116
|
+
description = await agent.getBountyDescription(BigInt(jobId));
|
|
117
|
+
} catch {
|
|
118
|
+
description = "(failed to fetch description from IPFS gateways)";
|
|
119
|
+
}
|
|
120
|
+
return json({ ...summarize(meta), description });
|
|
121
|
+
} catch (err) {
|
|
122
|
+
return errorResult(err);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
);
|
|
126
|
+
server.registerTool(
|
|
127
|
+
"get_reputation",
|
|
128
|
+
{
|
|
129
|
+
description: "Get an ERC-8004 agent's on-chain reputation score (average score, total feedbacks, total jobs).",
|
|
130
|
+
inputSchema: z.object({
|
|
131
|
+
agentId: z.string().optional().describe("Agent's ERC-8004 id. Omit to use this server's own configured agent.")
|
|
132
|
+
})
|
|
133
|
+
},
|
|
134
|
+
async ({ agentId }) => {
|
|
135
|
+
try {
|
|
136
|
+
const rep = await agent.getReputation(agentId !== void 0 ? BigInt(agentId) : void 0);
|
|
137
|
+
return json({
|
|
138
|
+
averageScore: rep.averageScore.toString(),
|
|
139
|
+
totalFeedbacks: rep.totalFeedbacks.toString(),
|
|
140
|
+
totalJobs: rep.totalJobs.toString()
|
|
141
|
+
});
|
|
142
|
+
} catch (err) {
|
|
143
|
+
return errorResult(err);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
);
|
|
147
|
+
if (hasSigner) {
|
|
148
|
+
server.registerTool(
|
|
149
|
+
"register_agent",
|
|
150
|
+
{
|
|
151
|
+
description: "Register this server's configured wallet as an ERC-8004 agent on Arc, pinning the given metadata to IPFS first. Idempotent \u2014 if this wallet already has an agentId, returns the existing one without a new on-chain transaction.",
|
|
152
|
+
inputSchema: z.object({
|
|
153
|
+
name: z.string(),
|
|
154
|
+
description: z.string(),
|
|
155
|
+
agent_type: z.string().optional(),
|
|
156
|
+
capabilities: z.array(z.string()).optional(),
|
|
157
|
+
preferred_categories: z.array(z.enum(["dev", "design", "content", "data", "other"])).optional(),
|
|
158
|
+
min_reward_usdc: z.number().optional(),
|
|
159
|
+
max_reward_usdc: z.number().optional()
|
|
160
|
+
})
|
|
161
|
+
},
|
|
162
|
+
async (args) => {
|
|
163
|
+
try {
|
|
164
|
+
const metadata = {
|
|
165
|
+
name: args.name,
|
|
166
|
+
description: args.description,
|
|
167
|
+
agent_type: args.agent_type,
|
|
168
|
+
capabilities: args.capabilities,
|
|
169
|
+
arcbounty: {
|
|
170
|
+
preferred_categories: args.preferred_categories,
|
|
171
|
+
min_reward_usdc: args.min_reward_usdc,
|
|
172
|
+
max_reward_usdc: args.max_reward_usdc
|
|
173
|
+
}
|
|
174
|
+
};
|
|
175
|
+
const metadataURI = await pinAgentMetadata(metadata);
|
|
176
|
+
const agentId = await agent.register(metadataURI);
|
|
177
|
+
return json({ agentId: agentId.toString(), metadataURI, address: agent.address });
|
|
178
|
+
} catch (err) {
|
|
179
|
+
return errorResult(err);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
);
|
|
183
|
+
server.registerTool(
|
|
184
|
+
"get_agent_info",
|
|
185
|
+
{ description: "Get this server's own configured agent identity, address, and reputation." },
|
|
186
|
+
async () => {
|
|
187
|
+
try {
|
|
188
|
+
const info = await agent.getAgentInfo();
|
|
189
|
+
return json({
|
|
190
|
+
agentId: info.agentId.toString(),
|
|
191
|
+
address: info.address,
|
|
192
|
+
metadataURI: info.metadataURI,
|
|
193
|
+
reputation: {
|
|
194
|
+
averageScore: info.reputation.averageScore.toString(),
|
|
195
|
+
totalFeedbacks: info.reputation.totalFeedbacks.toString(),
|
|
196
|
+
totalJobs: info.reputation.totalJobs.toString()
|
|
197
|
+
}
|
|
198
|
+
});
|
|
199
|
+
} catch (err) {
|
|
200
|
+
return errorResult(err);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
);
|
|
204
|
+
server.registerTool(
|
|
205
|
+
"get_my_bounties",
|
|
206
|
+
{ description: "List bounties currently assigned to this server's configured wallet as worker." },
|
|
207
|
+
async () => {
|
|
208
|
+
try {
|
|
209
|
+
const mine = await agent.getMyBounties();
|
|
210
|
+
return json(mine.map(summarize));
|
|
211
|
+
} catch (err) {
|
|
212
|
+
return errorResult(err);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
);
|
|
216
|
+
server.registerTool(
|
|
217
|
+
"get_pending_actions",
|
|
218
|
+
{
|
|
219
|
+
description: "Check this wallet's own bounties for anything needing attention RIGHT NOW: a dispute opened against it with no response yet, a rejection not yet challenged, or funds it can claim permissionlessly (auto-approve after the poster went silent, or a default arbitrator ruling after a timeout). Read-only \u2014 reports, never acts. This server has no background watchdog: if this bounty board matters to you, call this at the start of every session (or on a timer) so a dispute doesn't quietly expire while you weren't looking. An empty list means nothing needs you right now."
|
|
220
|
+
},
|
|
221
|
+
async () => {
|
|
222
|
+
try {
|
|
223
|
+
const actions = await agent.getPendingActions();
|
|
224
|
+
return json(actions.map((a) => ({
|
|
225
|
+
kind: a.kind,
|
|
226
|
+
jobId: a.jobId.toString(),
|
|
227
|
+
message: a.message,
|
|
228
|
+
bounty: summarize(a.meta)
|
|
229
|
+
})));
|
|
230
|
+
} catch (err) {
|
|
231
|
+
return errorResult(err);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
);
|
|
235
|
+
server.registerTool(
|
|
236
|
+
"take_bounty",
|
|
237
|
+
{
|
|
238
|
+
description: "Claim an open bounty as this server's configured wallet. On-chain and atomic \u2014 fails if someone else already took it. Do this only after reviewing the bounty with get_bounty. If the bounty has requireWorkerBond, a refundable USDC bond (workerBondUsdc) is approved and pulled automatically \u2014 it is returned in full at submit_work, so only take bonded bounties you intend to finish.",
|
|
239
|
+
inputSchema: z.object({ jobId: z.string() })
|
|
240
|
+
},
|
|
241
|
+
async ({ jobId }) => {
|
|
242
|
+
try {
|
|
243
|
+
const result = await agent.takeBounty(BigInt(jobId));
|
|
244
|
+
return json({ txHash: result.hash });
|
|
245
|
+
} catch (err) {
|
|
246
|
+
return errorResult(err);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
);
|
|
250
|
+
server.registerTool(
|
|
251
|
+
"submit_work",
|
|
252
|
+
{
|
|
253
|
+
description: "Submit completed work for a bounty this wallet has taken. The text is pinned to IPFS automatically. This starts the poster's review window \u2014 the poster can approve, reject (with a 48h challenge window), or the payout becomes claimable permissionlessly after 14 days if the poster never responds.",
|
|
254
|
+
inputSchema: z.object({
|
|
255
|
+
jobId: z.string(),
|
|
256
|
+
text: z.string().describe("The deliverable, as markdown/plain text.")
|
|
257
|
+
})
|
|
258
|
+
},
|
|
259
|
+
async ({ jobId, text }) => {
|
|
260
|
+
try {
|
|
261
|
+
const result = await agent.submitWork(BigInt(jobId), { text });
|
|
262
|
+
return json({ txHash: result.hash });
|
|
263
|
+
} catch (err) {
|
|
264
|
+
return errorResult(err);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
);
|
|
268
|
+
server.registerTool(
|
|
269
|
+
"auto_approve",
|
|
270
|
+
{
|
|
271
|
+
description: "Permissionlessly claim payout for a bounty this wallet submitted work for, once the poster has gone silent for 14 days past submission (APPROVAL_TIMEOUT). Fails harmlessly if the window hasn't elapsed.",
|
|
272
|
+
inputSchema: z.object({ jobId: z.string() })
|
|
273
|
+
},
|
|
274
|
+
async ({ jobId }) => {
|
|
275
|
+
try {
|
|
276
|
+
const result = await agent.autoApprove(BigInt(jobId));
|
|
277
|
+
return json({ txHash: result.hash });
|
|
278
|
+
} catch (err) {
|
|
279
|
+
return errorResult(err);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
);
|
|
283
|
+
}
|
|
284
|
+
async function main() {
|
|
285
|
+
const transport = new StdioServerTransport();
|
|
286
|
+
await server.connect(transport);
|
|
287
|
+
console.error(`[arcbounty-mcp] running on stdio${hasSigner ? "" : " (read-only mode \u2014 no signer configured)"}`);
|
|
288
|
+
}
|
|
289
|
+
main().catch((err) => {
|
|
290
|
+
console.error("[arcbounty-mcp] fatal:", err);
|
|
291
|
+
process.exit(1);
|
|
292
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "arcbounty-mcp",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "MCP server exposing ArcBounty (Arc Network bounty board, ERC-8183 + ERC-8004) to any MCP-compatible agent runtime — browse, take, and submit bounties directly from Claude or any other MCP client.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"arcbounty-mcp": "./dist/index.js"
|
|
8
|
+
},
|
|
9
|
+
"main": "./dist/index.js",
|
|
10
|
+
"files": [
|
|
11
|
+
"dist",
|
|
12
|
+
"README.md",
|
|
13
|
+
".env.example"
|
|
14
|
+
],
|
|
15
|
+
"repository": {
|
|
16
|
+
"type": "git",
|
|
17
|
+
"url": "git+https://github.com/Sofiia7/ARC.git",
|
|
18
|
+
"directory": "mcp-server"
|
|
19
|
+
},
|
|
20
|
+
"homepage": "https://arcbounty.app",
|
|
21
|
+
"bugs": {
|
|
22
|
+
"url": "https://github.com/Sofiia7/ARC/issues"
|
|
23
|
+
},
|
|
24
|
+
"scripts": {
|
|
25
|
+
"build": "tsup src/index.ts --format esm --dts --clean",
|
|
26
|
+
"dev": "tsx src/index.ts",
|
|
27
|
+
"typecheck": "tsc --noEmit",
|
|
28
|
+
"start": "node dist/index.js"
|
|
29
|
+
},
|
|
30
|
+
"keywords": [
|
|
31
|
+
"mcp",
|
|
32
|
+
"model-context-protocol",
|
|
33
|
+
"arc",
|
|
34
|
+
"bounty",
|
|
35
|
+
"erc-8183",
|
|
36
|
+
"erc-8004",
|
|
37
|
+
"ai-agent"
|
|
38
|
+
],
|
|
39
|
+
"license": "MIT",
|
|
40
|
+
"dependencies": {
|
|
41
|
+
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
42
|
+
"arcbounty-agent-sdk": "^0.4.3",
|
|
43
|
+
"viem": "^2.0.0",
|
|
44
|
+
"zod": "^3.23.0"
|
|
45
|
+
},
|
|
46
|
+
"devDependencies": {
|
|
47
|
+
"@types/node": "^25.6.0",
|
|
48
|
+
"tsup": "^8.0.0",
|
|
49
|
+
"tsx": "^4.0.0",
|
|
50
|
+
"typescript": "^5.0.0"
|
|
51
|
+
}
|
|
52
|
+
}
|