base-escrow-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/README.md ADDED
@@ -0,0 +1,60 @@
1
+ # base-escrow-mcp
2
+
3
+ MCP server for AI agents to create and manage P2P escrow deals on Base (ETH or ERC-20).
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ npx -y base-escrow-mcp
9
+ ```
10
+
11
+ Or add to your MCP config:
12
+
13
+ ```json
14
+ {
15
+ "mcpServers": {
16
+ "base-escrow": {
17
+ "command": "npx",
18
+ "args": ["-y", "base-escrow-mcp"],
19
+ "env": {
20
+ "DEPLOYER_PRIVATE_KEY": "your-private-key",
21
+ "RPC_URL": "https://mainnet.base.org"
22
+ }
23
+ }
24
+ }
25
+ }
26
+ ```
27
+
28
+ ## Tools (8)
29
+
30
+ | Tool | Description | Auth |
31
+ |------|-------------|------|
32
+ | `create_escrow` | Create a new escrow deal (ETH or ERC-20) | Key |
33
+ | `release_escrow` | Release funds to seller (buyer only) | Key |
34
+ | `refund_escrow` | Refund funds to buyer | Key |
35
+ | `dispute_escrow` | Flag a dispute on a deal | Key |
36
+ | `get_deal` | Get deal details by ID | None |
37
+ | `get_buyer_deals` | Get all deals for a buyer address | None |
38
+ | `get_seller_deals` | Get all deals for a seller address | None |
39
+ | `get_escrow_info` | Get contract info (fee, total deals, treasury) | None |
40
+
41
+ ## How It Works
42
+
43
+ 1. **Buyer creates** an escrow deal, depositing ETH or tokens into the contract
44
+ 2. **Seller delivers** the goods/services off-chain
45
+ 3. **Buyer releases** funds to seller, or **refunds** if seller fails to deliver
46
+ 4. Either party can **dispute** — treasury resolves disputed deals
47
+
48
+ ## Environment Variables
49
+
50
+ - `DEPLOYER_PRIVATE_KEY` — Required for write operations (create, release, refund, dispute)
51
+ - `RPC_URL` — Base RPC endpoint (default: `https://mainnet.base.org`)
52
+
53
+ ## Chain
54
+
55
+ - **Network:** Base mainnet (chainId 8453)
56
+ - **Explorer:** https://basescan.org
57
+
58
+ ## License
59
+
60
+ MIT
@@ -0,0 +1,15 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * base-escrow-mcp — MCP server for AI agents to manage P2P escrow deals on Base
4
+ *
5
+ * Tools:
6
+ * create_escrow — Create a new escrow deal (ETH or ERC-20)
7
+ * release_escrow — Release funds to seller (buyer only)
8
+ * refund_escrow — Refund funds to buyer
9
+ * dispute_escrow — Flag a dispute
10
+ * get_deal — Get deal details by ID
11
+ * get_buyer_deals — Get all deals for a buyer address
12
+ * get_seller_deals — Get all deals for a seller address
13
+ * get_escrow_info — Get contract info (fee, total deals, treasury)
14
+ */
15
+ export {};
package/dist/index.js ADDED
@@ -0,0 +1,324 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * base-escrow-mcp — MCP server for AI agents to manage P2P escrow deals on Base
4
+ *
5
+ * Tools:
6
+ * create_escrow — Create a new escrow deal (ETH or ERC-20)
7
+ * release_escrow — Release funds to seller (buyer only)
8
+ * refund_escrow — Refund funds to buyer
9
+ * dispute_escrow — Flag a dispute
10
+ * get_deal — Get deal details by ID
11
+ * get_buyer_deals — Get all deals for a buyer address
12
+ * get_seller_deals — Get all deals for a seller address
13
+ * get_escrow_info — Get contract info (fee, total deals, treasury)
14
+ */
15
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
16
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
17
+ import { z } from "zod";
18
+ import { ethers } from "ethers";
19
+ // ── Config ──────────────────────────────────────────────────────────
20
+ const RPC_URL = process.env.RPC_URL || "https://mainnet.base.org";
21
+ const CHAIN_ID = 8453;
22
+ const ESCROW_ADDRESS = "0x5Ee9f1e1dB3366D79B5fB59Dc012aD7D71a20F2C";
23
+ const ESCROW_ABI = [
24
+ "function create(address seller, address token, uint256 amount, string description) payable returns (uint256)",
25
+ "function release(uint256 dealId)",
26
+ "function refund(uint256 dealId)",
27
+ "function dispute(uint256 dealId)",
28
+ "function resolve(uint256 dealId, address winner)",
29
+ "function getDeal(uint256 dealId) view returns (address buyer, address seller, address token, uint256 amount, uint8 status, uint256 createdAt, string description)",
30
+ "function getBuyerDeals(address buyer) view returns (uint256[])",
31
+ "function getSellerDeals(address seller) view returns (uint256[])",
32
+ "function escrowFee() view returns (uint256)",
33
+ "function nextDealId() view returns (uint256)",
34
+ "function treasury() view returns (address)",
35
+ "function setFee(uint256 newFee)",
36
+ ];
37
+ const ERC20_ABI = [
38
+ "function approve(address spender, uint256 amount) returns (bool)",
39
+ "function allowance(address owner, address spender) view returns (uint256)",
40
+ "function balanceOf(address) view returns (uint256)",
41
+ "function decimals() view returns (uint8)",
42
+ "function symbol() view returns (string)",
43
+ "function name() view returns (string)",
44
+ ];
45
+ const STATUS_LABELS = ["Active", "Released", "Refunded", "Disputed"];
46
+ // ── Provider & Contract ─────────────────────────────────────────────
47
+ const provider = new ethers.JsonRpcProvider(RPC_URL);
48
+ const escrow = new ethers.Contract(ESCROW_ADDRESS, ESCROW_ABI, provider);
49
+ // ── Helpers ─────────────────────────────────────────────────────────
50
+ function ok(data) {
51
+ return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
52
+ }
53
+ function fail(msg) {
54
+ return { content: [{ type: "text", text: JSON.stringify({ error: msg }, null, 2) }], isError: true };
55
+ }
56
+ function getWallet() {
57
+ const pk = process.env.DEPLOYER_PRIVATE_KEY;
58
+ if (!pk)
59
+ return null;
60
+ return new ethers.Wallet(pk, provider);
61
+ }
62
+ function formatDeal(dealId, raw) {
63
+ const [buyer, seller, token, amount, status, createdAt, description] = raw;
64
+ const isETH = token === ethers.ZeroAddress;
65
+ return {
66
+ dealId: Number(dealId),
67
+ buyer,
68
+ seller,
69
+ token: isETH ? "ETH" : token,
70
+ tokenAddress: token,
71
+ amount: isETH ? ethers.formatEther(amount) : amount.toString(),
72
+ amountRaw: amount.toString(),
73
+ status: STATUS_LABELS[status] ?? "Unknown",
74
+ statusCode: status,
75
+ createdAt: Number(createdAt),
76
+ createdAtISO: new Date(Number(createdAt) * 1000).toISOString(),
77
+ description,
78
+ };
79
+ }
80
+ // ── Server ──────────────────────────────────────────────────────────
81
+ const server = new McpServer({
82
+ name: "base-escrow",
83
+ version: "1.0.0",
84
+ });
85
+ // --- create_escrow ---
86
+ server.tool("create_escrow", "Create a new P2P escrow deal on Base. For ETH deals, the ETH is locked in the contract. For ERC-20 deals, tokens are transferred from your wallet (approval handled automatically). A flat escrow fee is charged in ETH. Requires DEPLOYER_PRIVATE_KEY env var.", {
87
+ seller: z.string().describe("Seller wallet address — receives funds when buyer releases"),
88
+ amount: z.string().describe("Amount to escrow (ETH in ether e.g. '0.01', or token amount in human-readable units)"),
89
+ description: z.string().describe("Description of the deal / what is being traded"),
90
+ token: z.string().optional().describe("ERC-20 token address. Omit or set to '0x0000000000000000000000000000000000000000' for ETH escrow"),
91
+ }, async ({ seller, amount, description, token }) => {
92
+ try {
93
+ const wallet = getWallet();
94
+ if (!wallet)
95
+ return fail("DEPLOYER_PRIVATE_KEY env var required");
96
+ const isETH = !token || token === ethers.ZeroAddress;
97
+ const fee = await escrow.escrowFee();
98
+ let amountWei;
99
+ let msgValue;
100
+ const tokenAddress = isETH ? ethers.ZeroAddress : token;
101
+ if (isETH) {
102
+ amountWei = ethers.parseEther(amount);
103
+ msgValue = amountWei + fee;
104
+ }
105
+ else {
106
+ // Get token decimals for parsing
107
+ const tokenContract = new ethers.Contract(tokenAddress, ERC20_ABI, provider);
108
+ const decimals = await tokenContract.decimals();
109
+ amountWei = ethers.parseUnits(amount, decimals);
110
+ msgValue = fee;
111
+ // Handle approval
112
+ const tokenSigned = tokenContract.connect(wallet);
113
+ const currentAllowance = await tokenContract.allowance(wallet.address, ESCROW_ADDRESS);
114
+ if (currentAllowance < amountWei) {
115
+ const approveTx = await tokenSigned.approve(ESCROW_ADDRESS, ethers.MaxUint256);
116
+ await approveTx.wait();
117
+ }
118
+ }
119
+ const signed = escrow.connect(wallet);
120
+ const tx = await signed.create(seller, tokenAddress, amountWei, description, { value: msgValue });
121
+ const receipt = await tx.wait();
122
+ // Parse DealCreated event to get the dealId
123
+ const iface = new ethers.Interface(ESCROW_ABI);
124
+ let dealId;
125
+ for (const log of receipt.logs) {
126
+ try {
127
+ const parsed = iface.parseLog({ topics: log.topics, data: log.data });
128
+ if (parsed && parsed.name === "DealCreated") {
129
+ dealId = parsed.args[0].toString();
130
+ }
131
+ }
132
+ catch {
133
+ // skip non-matching logs
134
+ }
135
+ }
136
+ return ok({
137
+ status: "created",
138
+ dealId: dealId ?? "check tx logs",
139
+ buyer: wallet.address,
140
+ seller,
141
+ token: isETH ? "ETH" : tokenAddress,
142
+ amount,
143
+ fee: ethers.formatEther(fee) + " ETH",
144
+ description,
145
+ txHash: receipt.hash,
146
+ explorer: `https://basescan.org/tx/${receipt.hash}`,
147
+ gasUsed: receipt.gasUsed.toString(),
148
+ });
149
+ }
150
+ catch (err) {
151
+ return fail(`Create escrow failed: ${err instanceof Error ? err.message : String(err)}`);
152
+ }
153
+ });
154
+ // --- release_escrow ---
155
+ server.tool("release_escrow", "Release escrowed funds to the seller. Only the buyer can call this. Marks the deal as complete. Requires DEPLOYER_PRIVATE_KEY env var.", {
156
+ deal_id: z.number().describe("Deal ID to release"),
157
+ }, async ({ deal_id }) => {
158
+ try {
159
+ const wallet = getWallet();
160
+ if (!wallet)
161
+ return fail("DEPLOYER_PRIVATE_KEY env var required");
162
+ const signed = escrow.connect(wallet);
163
+ const tx = await signed.release(deal_id);
164
+ const receipt = await tx.wait();
165
+ return ok({
166
+ status: "released",
167
+ dealId: deal_id,
168
+ txHash: receipt.hash,
169
+ explorer: `https://basescan.org/tx/${receipt.hash}`,
170
+ gasUsed: receipt.gasUsed.toString(),
171
+ });
172
+ }
173
+ catch (err) {
174
+ return fail(`Release failed: ${err instanceof Error ? err.message : String(err)}`);
175
+ }
176
+ });
177
+ // --- refund_escrow ---
178
+ server.tool("refund_escrow", "Refund escrowed funds back to the buyer. Only the buyer can call this. Use when the seller fails to deliver. Requires DEPLOYER_PRIVATE_KEY env var.", {
179
+ deal_id: z.number().describe("Deal ID to refund"),
180
+ }, async ({ deal_id }) => {
181
+ try {
182
+ const wallet = getWallet();
183
+ if (!wallet)
184
+ return fail("DEPLOYER_PRIVATE_KEY env var required");
185
+ const signed = escrow.connect(wallet);
186
+ const tx = await signed.refund(deal_id);
187
+ const receipt = await tx.wait();
188
+ return ok({
189
+ status: "refunded",
190
+ dealId: deal_id,
191
+ txHash: receipt.hash,
192
+ explorer: `https://basescan.org/tx/${receipt.hash}`,
193
+ gasUsed: receipt.gasUsed.toString(),
194
+ });
195
+ }
196
+ catch (err) {
197
+ return fail(`Refund failed: ${err instanceof Error ? err.message : String(err)}`);
198
+ }
199
+ });
200
+ // --- dispute_escrow ---
201
+ server.tool("dispute_escrow", "Flag a dispute on an active escrow deal. Either buyer or seller can call this. Funds stay locked until the treasury resolves the dispute. Requires DEPLOYER_PRIVATE_KEY env var.", {
202
+ deal_id: z.number().describe("Deal ID to dispute"),
203
+ }, async ({ deal_id }) => {
204
+ try {
205
+ const wallet = getWallet();
206
+ if (!wallet)
207
+ return fail("DEPLOYER_PRIVATE_KEY env var required");
208
+ const signed = escrow.connect(wallet);
209
+ const tx = await signed.dispute(deal_id);
210
+ const receipt = await tx.wait();
211
+ return ok({
212
+ status: "disputed",
213
+ dealId: deal_id,
214
+ txHash: receipt.hash,
215
+ explorer: `https://basescan.org/tx/${receipt.hash}`,
216
+ gasUsed: receipt.gasUsed.toString(),
217
+ note: "Funds are locked. Treasury will resolve the dispute.",
218
+ });
219
+ }
220
+ catch (err) {
221
+ return fail(`Dispute failed: ${err instanceof Error ? err.message : String(err)}`);
222
+ }
223
+ });
224
+ // --- get_deal ---
225
+ server.tool("get_deal", "Get details of an escrow deal by its ID. Returns buyer, seller, token, amount, status, creation time, and description.", {
226
+ deal_id: z.number().describe("Deal ID to look up"),
227
+ }, async ({ deal_id }) => {
228
+ try {
229
+ const raw = await escrow.getDeal(deal_id);
230
+ const deal = formatDeal(deal_id, raw);
231
+ return ok({ deal });
232
+ }
233
+ catch (err) {
234
+ return fail(`Get deal failed: ${err instanceof Error ? err.message : String(err)}`);
235
+ }
236
+ });
237
+ // --- get_buyer_deals ---
238
+ server.tool("get_buyer_deals", "Get all escrow deal IDs where the given address is the buyer.", {
239
+ address: z.string().describe("Buyer wallet address"),
240
+ }, async ({ address }) => {
241
+ try {
242
+ const dealIds = await escrow.getBuyerDeals(address);
243
+ const ids = dealIds.map((id) => Number(id));
244
+ // Fetch details for each deal
245
+ const deals = await Promise.all(ids.map(async (id) => {
246
+ const raw = await escrow.getDeal(id);
247
+ return formatDeal(id, raw);
248
+ }));
249
+ return ok({
250
+ address,
251
+ role: "buyer",
252
+ totalDeals: ids.length,
253
+ dealIds: ids,
254
+ deals,
255
+ });
256
+ }
257
+ catch (err) {
258
+ return fail(`Get buyer deals failed: ${err instanceof Error ? err.message : String(err)}`);
259
+ }
260
+ });
261
+ // --- get_seller_deals ---
262
+ server.tool("get_seller_deals", "Get all escrow deal IDs where the given address is the seller.", {
263
+ address: z.string().describe("Seller wallet address"),
264
+ }, async ({ address }) => {
265
+ try {
266
+ const dealIds = await escrow.getSellerDeals(address);
267
+ const ids = dealIds.map((id) => Number(id));
268
+ const deals = await Promise.all(ids.map(async (id) => {
269
+ const raw = await escrow.getDeal(id);
270
+ return formatDeal(id, raw);
271
+ }));
272
+ return ok({
273
+ address,
274
+ role: "seller",
275
+ totalDeals: ids.length,
276
+ dealIds: ids,
277
+ deals,
278
+ });
279
+ }
280
+ catch (err) {
281
+ return fail(`Get seller deals failed: ${err instanceof Error ? err.message : String(err)}`);
282
+ }
283
+ });
284
+ // --- get_escrow_info ---
285
+ server.tool("get_escrow_info", "Get Escrow contract info — current fee, total deals created, treasury address, contract address. Use to verify the contract is live.", {}, async () => {
286
+ try {
287
+ const [fee, totalDeals, treasury] = await Promise.all([
288
+ escrow.escrowFee(),
289
+ escrow.nextDealId(),
290
+ escrow.treasury(),
291
+ ]);
292
+ return ok({
293
+ contract: ESCROW_ADDRESS,
294
+ chain: "Base",
295
+ chainId: CHAIN_ID,
296
+ escrowFee: ethers.formatEther(fee) + " ETH",
297
+ escrowFeeWei: fee.toString(),
298
+ totalDeals: Number(totalDeals),
299
+ treasury,
300
+ explorer: `https://basescan.org/address/${ESCROW_ADDRESS}`,
301
+ capabilities: [
302
+ "P2P escrow for ETH or any ERC-20 token",
303
+ "Buyer creates deal, deposits funds",
304
+ "Buyer releases funds to seller on delivery",
305
+ "Buyer can refund if seller fails to deliver",
306
+ "Either party can flag a dispute",
307
+ "Treasury resolves disputes",
308
+ "Flat fee per escrow creation",
309
+ ],
310
+ });
311
+ }
312
+ catch (err) {
313
+ return fail(`Info fetch failed: ${err instanceof Error ? err.message : String(err)}`);
314
+ }
315
+ });
316
+ // ── Start ───────────────────────────────────────────────────────────
317
+ async function main() {
318
+ const transport = new StdioServerTransport();
319
+ await server.connect(transport);
320
+ }
321
+ main().catch((err) => {
322
+ console.error("base-escrow-mcp error:", err);
323
+ process.exit(1);
324
+ });
package/package.json ADDED
@@ -0,0 +1,47 @@
1
+ {
2
+ "name": "base-escrow-mcp",
3
+ "version": "1.0.0",
4
+ "description": "MCP server for AI agents to create and manage P2P escrow deals (ETH or ERC-20) on Base. Trustless trades with dispute resolution — one tool call.",
5
+ "type": "module",
6
+ "main": "dist/index.js",
7
+ "bin": {
8
+ "base-escrow-mcp": "dist/index.js"
9
+ },
10
+ "engines": {
11
+ "node": ">=18"
12
+ },
13
+ "files": [
14
+ "dist/",
15
+ "README.md"
16
+ ],
17
+ "scripts": {
18
+ "build": "tsc",
19
+ "start": "node dist/index.js",
20
+ "dev": "tsc --watch"
21
+ },
22
+ "keywords": [
23
+ "mcp",
24
+ "base",
25
+ "escrow",
26
+ "p2p",
27
+ "erc20",
28
+ "eth",
29
+ "dispute",
30
+ "trade",
31
+ "ai-agent",
32
+ "claude",
33
+ "cursor",
34
+ "model-context-protocol",
35
+ "onchain"
36
+ ],
37
+ "license": "MIT",
38
+ "dependencies": {
39
+ "@modelcontextprotocol/sdk": "^1.0.0",
40
+ "ethers": "^6.13.0",
41
+ "zod": "^3.22.0"
42
+ },
43
+ "devDependencies": {
44
+ "@types/node": "^20.0.0",
45
+ "typescript": "^5.4.0"
46
+ }
47
+ }