@codespar/mcp-coinbase-commerce 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/README.md ADDED
@@ -0,0 +1,70 @@
1
+ # @codespar/mcp-coinbase-commerce
2
+
3
+ MCP server for [Coinbase Commerce](https://commerce.coinbase.com) — global crypto merchant payments.
4
+
5
+ Coinbase Commerce is the **merchant-accept** side of crypto. Your store prices an order in local fiat (USD, BRL, EUR, MXN, ...), the buyer settles in BTC / ETH / USDC / and other supported assets, and Coinbase settles to you in the crypto or fiat of your choice.
6
+
7
+ ## Positioning vs the rest of the catalog
8
+
9
+ | Server | Use case | Direction |
10
+ |--------|----------|-----------|
11
+ | `@codespar/mcp-coinbase-commerce` | **Merchants accept crypto at checkout** | Buyer pays merchant |
12
+ | `@codespar/mcp-unblockpay` | BRL / MXN <-> USDC corridor | Value transfer |
13
+ | `@codespar/mcp-moonpay` | End-user fiat <-> crypto (100+ assets) | Onramp / offramp |
14
+ | `@codespar/mcp-transak` | End-user fiat <-> crypto (broad geo) | Onramp / offramp |
15
+
16
+ Use Coinbase Commerce when an agent needs to **bill a buyer in crypto** — hosted charge page, reusable checkout, or directed invoice.
17
+
18
+ ## Tools
19
+
20
+ | Tool | Purpose |
21
+ |------|---------|
22
+ | `create_charge` | Create a one-time crypto charge priced in fiat |
23
+ | `retrieve_charge` | Look up a charge by id or short code |
24
+ | `list_charges` | List charges (paginated) |
25
+ | `cancel_charge` | Cancel an unpaid charge |
26
+ | `resolve_charge` | Manually mark a charge as paid |
27
+ | `create_checkout` | Create a reusable hosted checkout (product page) |
28
+ | `retrieve_checkout` | Look up a checkout by id |
29
+ | `list_events` | List lifecycle events (same payload as webhooks) |
30
+ | `create_invoice` | Create an invoice directed at a named recipient |
31
+
32
+ ## Install
33
+
34
+ ```bash
35
+ npm install @codespar/mcp-coinbase-commerce
36
+ ```
37
+
38
+ ## Environment
39
+
40
+ ```bash
41
+ COINBASE_COMMERCE_API_KEY="..." # API key (required, secret)
42
+ COINBASE_COMMERCE_API_VERSION="..." # Optional. Defaults to 2018-03-22.
43
+ ```
44
+
45
+ Create an API key at <https://beta.commerce.coinbase.com/settings/security>.
46
+
47
+ ## Authentication
48
+
49
+ Every request carries two headers:
50
+
51
+ ```
52
+ X-CC-Api-Key: <COINBASE_COMMERCE_API_KEY>
53
+ X-CC-Version: 2018-03-22
54
+ ```
55
+
56
+ The version header is required. Pin it so future API changes don't silently break your integration.
57
+
58
+ ## Run
59
+
60
+ ```bash
61
+ # stdio (default — for Claude Desktop, Cursor, etc)
62
+ npx @codespar/mcp-coinbase-commerce
63
+
64
+ # HTTP (for server-to-server testing)
65
+ MCP_HTTP=true MCP_PORT=3000 npx @codespar/mcp-coinbase-commerce
66
+ ```
67
+
68
+ ## License
69
+
70
+ MIT
package/dist/index.js ADDED
@@ -0,0 +1,317 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * MCP Server for Coinbase Commerce — global crypto merchant payments.
4
+ *
5
+ * Coinbase Commerce is the merchant-accept side of crypto: a store creates a
6
+ * charge (or hosted checkout / invoice) priced in local fiat, the buyer pays
7
+ * in BTC / ETH / USDC / etc., and Coinbase settles to the merchant in the
8
+ * chosen crypto or fiat. This complements rather than overlaps the rest of
9
+ * the CodeSpar crypto catalog:
10
+ * - UnblockPay — BRL/MXN <-> USDC corridor for agents moving value
11
+ * - MoonPay — end-user fiat <-> crypto on/off-ramp (100+ assets)
12
+ * - Transak — end-user on/off-ramp (broad geo)
13
+ * - Coinbase — merchants ACCEPT crypto from buyers at checkout
14
+ * Commerce (this package)
15
+ *
16
+ * Tools (9):
17
+ * create_charge — create a crypto charge (merchant invoice)
18
+ * retrieve_charge — look up a charge by id or short code
19
+ * list_charges — list charges (paginated)
20
+ * cancel_charge — cancel a no-longer-needed charge (before payment)
21
+ * resolve_charge — manually mark a charge as paid
22
+ * create_checkout — create a reusable hosted checkout (product page)
23
+ * retrieve_checkout — look up a checkout by id
24
+ * list_events — list webhook-like events (charge:* lifecycle)
25
+ * create_invoice — create an invoice for a known recipient
26
+ *
27
+ * Authentication
28
+ * Every request carries two headers:
29
+ * X-CC-Api-Key: <COINBASE_COMMERCE_API_KEY>
30
+ * X-CC-Version: 2018-03-22 (version header is required)
31
+ *
32
+ * Environment
33
+ * COINBASE_COMMERCE_API_KEY — API key (required, secret)
34
+ * COINBASE_COMMERCE_API_VERSION — optional; defaults to 2018-03-22
35
+ *
36
+ * Docs: https://docs.cdp.coinbase.com/commerce (base: https://api.commerce.coinbase.com)
37
+ */
38
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
39
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
40
+ import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
41
+ import { isInitializeRequest } from "@modelcontextprotocol/sdk/types.js";
42
+ import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
43
+ const API_KEY = process.env.COINBASE_COMMERCE_API_KEY || "";
44
+ const API_VERSION = process.env.COINBASE_COMMERCE_API_VERSION || "2018-03-22";
45
+ const BASE_URL = "https://api.commerce.coinbase.com";
46
+ async function coinbaseRequest(method, path, body) {
47
+ const res = await fetch(`${BASE_URL}${path}`, {
48
+ method,
49
+ headers: {
50
+ "Content-Type": "application/json",
51
+ "X-CC-Api-Key": API_KEY,
52
+ "X-CC-Version": API_VERSION,
53
+ },
54
+ body: body ? JSON.stringify(body) : undefined,
55
+ });
56
+ if (!res.ok) {
57
+ const err = await res.text();
58
+ throw new Error(`Coinbase Commerce API ${res.status}: ${err}`);
59
+ }
60
+ const text = await res.text();
61
+ if (!text)
62
+ return {};
63
+ try {
64
+ return JSON.parse(text);
65
+ }
66
+ catch {
67
+ return { raw: text };
68
+ }
69
+ }
70
+ function qs(params) {
71
+ const entries = Object.entries(params).filter(([, v]) => v !== undefined && v !== null && v !== "");
72
+ if (entries.length === 0)
73
+ return "";
74
+ const search = new URLSearchParams();
75
+ for (const [k, v] of entries)
76
+ search.set(k, String(v));
77
+ return `?${search.toString()}`;
78
+ }
79
+ const server = new Server({ name: "mcp-coinbase-commerce", version: "0.1.0" }, { capabilities: { tools: {} } });
80
+ server.setRequestHandler(ListToolsRequestSchema, async () => ({
81
+ tools: [
82
+ {
83
+ name: "create_charge",
84
+ description: "Create a crypto charge — a one-time merchant invoice priced in local fiat that a buyer can settle in BTC, ETH, USDC, and other supported assets. Returns a hosted_url the buyer can be redirected to, plus per-asset payment addresses.",
85
+ inputSchema: {
86
+ type: "object",
87
+ properties: {
88
+ name: { type: "string", description: "Short product/order name shown on the hosted payment page" },
89
+ description: { type: "string", description: "Longer human-readable description of what the buyer is paying for" },
90
+ pricing_type: { type: "string", enum: ["fixed_price", "no_price"], description: "fixed_price: exact amount in local_price. no_price: buyer chooses (donations)." },
91
+ local_price: {
92
+ type: "object",
93
+ description: "Fiat-denominated price the charge is quoted in. Required when pricing_type is fixed_price.",
94
+ properties: {
95
+ amount: { type: "string", description: "Amount as a decimal string (e.g. \"29.90\")" },
96
+ currency: { type: "string", description: "ISO-4217 fiat currency code (e.g. USD, BRL, EUR, MXN)" },
97
+ },
98
+ required: ["amount", "currency"],
99
+ },
100
+ metadata: { type: "object", description: "Arbitrary JSON you want echoed back on events (customer_id, order_id, etc.)" },
101
+ redirect_url: { type: "string", description: "Browser redirect after a successful payment" },
102
+ cancel_url: { type: "string", description: "Browser redirect if the buyer abandons the hosted page" },
103
+ },
104
+ required: ["name", "description", "pricing_type"],
105
+ },
106
+ },
107
+ {
108
+ name: "retrieve_charge",
109
+ description: "Retrieve a charge by its Coinbase Commerce id OR its short code (the 8-character code embedded in the hosted URL). Returns current status, timeline, and payments.",
110
+ inputSchema: {
111
+ type: "object",
112
+ properties: {
113
+ code_or_id: { type: "string", description: "Charge id or short code" },
114
+ },
115
+ required: ["code_or_id"],
116
+ },
117
+ },
118
+ {
119
+ name: "list_charges",
120
+ description: "List charges, newest first. Supports cursor pagination via starting_after / ending_before.",
121
+ inputSchema: {
122
+ type: "object",
123
+ properties: {
124
+ limit: { type: "number", description: "Max results per page (default 25, max 100)" },
125
+ starting_after: { type: "string", description: "Cursor: return results after this charge id" },
126
+ ending_before: { type: "string", description: "Cursor: return results before this charge id" },
127
+ order: { type: "string", enum: ["asc", "desc"], description: "Sort order by created_at. Defaults to desc." },
128
+ },
129
+ },
130
+ },
131
+ {
132
+ name: "cancel_charge",
133
+ description: "Cancel a charge that has not yet been paid. Only charges in NEW status can be cancelled; once pending or completed the call will fail.",
134
+ inputSchema: {
135
+ type: "object",
136
+ properties: {
137
+ code: { type: "string", description: "Charge short code" },
138
+ },
139
+ required: ["code"],
140
+ },
141
+ },
142
+ {
143
+ name: "resolve_charge",
144
+ description: "Manually resolve a charge as paid. Used for out-of-band settlement (e.g. underpayment you accept, delayed confirmation you want to honour).",
145
+ inputSchema: {
146
+ type: "object",
147
+ properties: {
148
+ code: { type: "string", description: "Charge short code" },
149
+ },
150
+ required: ["code"],
151
+ },
152
+ },
153
+ {
154
+ name: "create_checkout",
155
+ description: "Create a reusable hosted checkout — think product-page-style link that can be paid multiple times. Good for evergreen SKUs and donation pages.",
156
+ inputSchema: {
157
+ type: "object",
158
+ properties: {
159
+ name: { type: "string", description: "Product / checkout name" },
160
+ description: { type: "string", description: "Longer description shown to buyers" },
161
+ pricing_type: { type: "string", enum: ["fixed_price", "no_price"], description: "fixed_price: exact amount in local_price. no_price: buyer chooses." },
162
+ local_price: {
163
+ type: "object",
164
+ description: "Fiat-denominated price. Required when pricing_type is fixed_price.",
165
+ properties: {
166
+ amount: { type: "string", description: "Amount as decimal string" },
167
+ currency: { type: "string", description: "ISO-4217 fiat currency code" },
168
+ },
169
+ required: ["amount", "currency"],
170
+ },
171
+ requested_info: {
172
+ type: "array",
173
+ description: "Buyer fields Coinbase should collect on the hosted page (e.g. [\"name\", \"email\"])",
174
+ items: { type: "string" },
175
+ },
176
+ },
177
+ required: ["name", "description", "pricing_type"],
178
+ },
179
+ },
180
+ {
181
+ name: "retrieve_checkout",
182
+ description: "Retrieve a checkout by id.",
183
+ inputSchema: {
184
+ type: "object",
185
+ properties: {
186
+ id: { type: "string", description: "Checkout id" },
187
+ },
188
+ required: ["id"],
189
+ },
190
+ },
191
+ {
192
+ name: "list_events",
193
+ description: "List events — the lifecycle signals (charge:created, charge:confirmed, charge:failed, charge:delayed, charge:pending, charge:resolved) that Coinbase Commerce also delivers via webhook. Useful for reconciliation and agent polling.",
194
+ inputSchema: {
195
+ type: "object",
196
+ properties: {
197
+ limit: { type: "number", description: "Max results per page (default 25, max 100)" },
198
+ starting_after: { type: "string", description: "Cursor: return results after this event id" },
199
+ ending_before: { type: "string", description: "Cursor: return results before this event id" },
200
+ order: { type: "string", enum: ["asc", "desc"], description: "Sort order by created_at. Defaults to desc." },
201
+ },
202
+ },
203
+ },
204
+ {
205
+ name: "create_invoice",
206
+ description: "Create an invoice — a directed bill sent to a specific named recipient. Unlike a charge, an invoice captures who it was issued to and has its own draft / viewed / paid lifecycle.",
207
+ inputSchema: {
208
+ type: "object",
209
+ properties: {
210
+ business_name: { type: "string", description: "Your business name shown on the invoice" },
211
+ customer_email: { type: "string", description: "Email of the invoice recipient" },
212
+ customer_name: { type: "string", description: "Display name of the invoice recipient" },
213
+ memo: { type: "string", description: "Free-form note to the recipient (appears on invoice)" },
214
+ local_price: {
215
+ type: "object",
216
+ description: "Fiat-denominated amount the invoice is quoted in",
217
+ properties: {
218
+ amount: { type: "string", description: "Amount as decimal string" },
219
+ currency: { type: "string", description: "ISO-4217 fiat currency code" },
220
+ },
221
+ required: ["amount", "currency"],
222
+ },
223
+ },
224
+ required: ["business_name", "customer_email", "customer_name", "local_price"],
225
+ },
226
+ },
227
+ ],
228
+ }));
229
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
230
+ const { name, arguments: args } = request.params;
231
+ const a = (args ?? {});
232
+ try {
233
+ switch (name) {
234
+ case "create_charge":
235
+ return { content: [{ type: "text", text: JSON.stringify(await coinbaseRequest("POST", "/charges", a), null, 2) }] };
236
+ case "retrieve_charge":
237
+ return { content: [{ type: "text", text: JSON.stringify(await coinbaseRequest("GET", `/charges/${encodeURIComponent(String(a.code_or_id ?? ""))}`), null, 2) }] };
238
+ case "list_charges": {
239
+ const query = qs({
240
+ limit: a.limit,
241
+ starting_after: a.starting_after,
242
+ ending_before: a.ending_before,
243
+ order: a.order,
244
+ });
245
+ return { content: [{ type: "text", text: JSON.stringify(await coinbaseRequest("GET", `/charges${query}`), null, 2) }] };
246
+ }
247
+ case "cancel_charge":
248
+ return { content: [{ type: "text", text: JSON.stringify(await coinbaseRequest("POST", `/charges/${encodeURIComponent(String(a.code ?? ""))}/cancel`), null, 2) }] };
249
+ case "resolve_charge":
250
+ return { content: [{ type: "text", text: JSON.stringify(await coinbaseRequest("POST", `/charges/${encodeURIComponent(String(a.code ?? ""))}/resolve`), null, 2) }] };
251
+ case "create_checkout":
252
+ return { content: [{ type: "text", text: JSON.stringify(await coinbaseRequest("POST", "/checkouts", a), null, 2) }] };
253
+ case "retrieve_checkout":
254
+ return { content: [{ type: "text", text: JSON.stringify(await coinbaseRequest("GET", `/checkouts/${encodeURIComponent(String(a.id ?? ""))}`), null, 2) }] };
255
+ case "list_events": {
256
+ const query = qs({
257
+ limit: a.limit,
258
+ starting_after: a.starting_after,
259
+ ending_before: a.ending_before,
260
+ order: a.order,
261
+ });
262
+ return { content: [{ type: "text", text: JSON.stringify(await coinbaseRequest("GET", `/events${query}`), null, 2) }] };
263
+ }
264
+ case "create_invoice":
265
+ return { content: [{ type: "text", text: JSON.stringify(await coinbaseRequest("POST", "/invoices", a), null, 2) }] };
266
+ default:
267
+ return { content: [{ type: "text", text: `Unknown tool: ${name}` }], isError: true };
268
+ }
269
+ }
270
+ catch (err) {
271
+ return { content: [{ type: "text", text: `Error: ${err instanceof Error ? err.message : String(err)}` }], isError: true };
272
+ }
273
+ });
274
+ async function main() {
275
+ if (process.argv.includes("--http") || process.env.MCP_HTTP === "true") {
276
+ const { default: express } = await import("express");
277
+ const { randomUUID } = await import("node:crypto");
278
+ const app = express();
279
+ app.use(express.json());
280
+ const transports = new Map();
281
+ app.get("/health", (_req, res) => res.json({ status: "ok", sessions: transports.size }));
282
+ app.post("/mcp", async (req, res) => {
283
+ const sid = req.headers["mcp-session-id"];
284
+ if (sid && transports.has(sid)) {
285
+ await transports.get(sid).handleRequest(req, res, req.body);
286
+ return;
287
+ }
288
+ if (!sid && isInitializeRequest(req.body)) {
289
+ const t = new StreamableHTTPServerTransport({ sessionIdGenerator: () => randomUUID(), onsessioninitialized: (id) => { transports.set(id, t); } });
290
+ t.onclose = () => { if (t.sessionId)
291
+ transports.delete(t.sessionId); };
292
+ const s = new Server({ name: "mcp-coinbase-commerce", version: "0.1.0" }, { capabilities: { tools: {} } });
293
+ server._requestHandlers.forEach((v, k) => s._requestHandlers.set(k, v));
294
+ server._notificationHandlers?.forEach((v, k) => s._notificationHandlers.set(k, v));
295
+ await s.connect(t);
296
+ await t.handleRequest(req, res, req.body);
297
+ return;
298
+ }
299
+ res.status(400).json({ jsonrpc: "2.0", error: { code: -32000, message: "Bad Request" }, id: null });
300
+ });
301
+ app.get("/mcp", async (req, res) => { const sid = req.headers["mcp-session-id"]; if (sid && transports.has(sid))
302
+ await transports.get(sid).handleRequest(req, res);
303
+ else
304
+ res.status(400).send("Invalid session"); });
305
+ app.delete("/mcp", async (req, res) => { const sid = req.headers["mcp-session-id"]; if (sid && transports.has(sid))
306
+ await transports.get(sid).handleRequest(req, res);
307
+ else
308
+ res.status(400).send("Invalid session"); });
309
+ const port = Number(process.env.MCP_PORT) || 3000;
310
+ app.listen(port, () => { console.error(`MCP HTTP server on http://localhost:${port}/mcp`); });
311
+ }
312
+ else {
313
+ const transport = new StdioServerTransport();
314
+ await server.connect(transport);
315
+ }
316
+ }
317
+ main().catch(console.error);
package/package.json ADDED
@@ -0,0 +1,36 @@
1
+ {
2
+ "name": "@codespar/mcp-coinbase-commerce",
3
+ "version": "0.1.0",
4
+ "description": "MCP server for Coinbase Commerce — global crypto merchant payments. Accept BTC, ETH, USDC and more at checkout with hosted charges, checkouts, and invoices.",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "bin": {
8
+ "mcp-coinbase-commerce": "./dist/index.js"
9
+ },
10
+ "scripts": {
11
+ "build": "tsc",
12
+ "start": "node dist/index.js"
13
+ },
14
+ "dependencies": {
15
+ "@modelcontextprotocol/sdk": "^1.0.0"
16
+ },
17
+ "devDependencies": {
18
+ "@types/node": "^25.5.0",
19
+ "typescript": "^5.8.0"
20
+ },
21
+ "license": "MIT",
22
+ "keywords": [
23
+ "mcp",
24
+ "coinbase",
25
+ "coinbase-commerce",
26
+ "crypto",
27
+ "merchant",
28
+ "payments",
29
+ "checkout",
30
+ "btc",
31
+ "eth",
32
+ "usdc",
33
+ "stablecoin"
34
+ ],
35
+ "mcpName": "io.github.codespar/mcp-coinbase-commerce"
36
+ }
package/server.json ADDED
@@ -0,0 +1,37 @@
1
+ {
2
+ "$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json",
3
+ "name": "io.github.codespar/mcp-coinbase-commerce",
4
+ "description": "MCP server for Coinbase Commerce — global crypto merchant payments. Create charges, checkouts, and invoices so buyers can pay in BTC, ETH, USDC and more; Coinbase settles to crypto or fiat.",
5
+ "repository": {
6
+ "url": "https://github.com/codespar/mcp-dev-brasil",
7
+ "source": "github",
8
+ "subfolder": "packages/crypto/coinbase-commerce"
9
+ },
10
+ "version": "0.1.0",
11
+ "packages": [
12
+ {
13
+ "registryType": "npm",
14
+ "identifier": "@codespar/mcp-coinbase-commerce",
15
+ "version": "0.1.0",
16
+ "transport": {
17
+ "type": "stdio"
18
+ },
19
+ "environmentVariables": [
20
+ {
21
+ "name": "COINBASE_COMMERCE_API_KEY",
22
+ "description": "Coinbase Commerce API key. Create one at https://beta.commerce.coinbase.com/settings/security.",
23
+ "isRequired": true,
24
+ "format": "string",
25
+ "isSecret": true
26
+ },
27
+ {
28
+ "name": "COINBASE_COMMERCE_API_VERSION",
29
+ "description": "Coinbase Commerce API version pin (X-CC-Version header). Defaults to 2018-03-22.",
30
+ "isRequired": false,
31
+ "format": "string",
32
+ "isSecret": false
33
+ }
34
+ ]
35
+ }
36
+ ]
37
+ }
package/src/index.ts ADDED
@@ -0,0 +1,317 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * MCP Server for Coinbase Commerce — global crypto merchant payments.
5
+ *
6
+ * Coinbase Commerce is the merchant-accept side of crypto: a store creates a
7
+ * charge (or hosted checkout / invoice) priced in local fiat, the buyer pays
8
+ * in BTC / ETH / USDC / etc., and Coinbase settles to the merchant in the
9
+ * chosen crypto or fiat. This complements rather than overlaps the rest of
10
+ * the CodeSpar crypto catalog:
11
+ * - UnblockPay — BRL/MXN <-> USDC corridor for agents moving value
12
+ * - MoonPay — end-user fiat <-> crypto on/off-ramp (100+ assets)
13
+ * - Transak — end-user on/off-ramp (broad geo)
14
+ * - Coinbase — merchants ACCEPT crypto from buyers at checkout
15
+ * Commerce (this package)
16
+ *
17
+ * Tools (9):
18
+ * create_charge — create a crypto charge (merchant invoice)
19
+ * retrieve_charge — look up a charge by id or short code
20
+ * list_charges — list charges (paginated)
21
+ * cancel_charge — cancel a no-longer-needed charge (before payment)
22
+ * resolve_charge — manually mark a charge as paid
23
+ * create_checkout — create a reusable hosted checkout (product page)
24
+ * retrieve_checkout — look up a checkout by id
25
+ * list_events — list webhook-like events (charge:* lifecycle)
26
+ * create_invoice — create an invoice for a known recipient
27
+ *
28
+ * Authentication
29
+ * Every request carries two headers:
30
+ * X-CC-Api-Key: <COINBASE_COMMERCE_API_KEY>
31
+ * X-CC-Version: 2018-03-22 (version header is required)
32
+ *
33
+ * Environment
34
+ * COINBASE_COMMERCE_API_KEY — API key (required, secret)
35
+ * COINBASE_COMMERCE_API_VERSION — optional; defaults to 2018-03-22
36
+ *
37
+ * Docs: https://docs.cdp.coinbase.com/commerce (base: https://api.commerce.coinbase.com)
38
+ */
39
+
40
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
41
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
42
+ import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
43
+ import { isInitializeRequest } from "@modelcontextprotocol/sdk/types.js";
44
+ import {
45
+ CallToolRequestSchema,
46
+ ListToolsRequestSchema,
47
+ } from "@modelcontextprotocol/sdk/types.js";
48
+
49
+ const API_KEY = process.env.COINBASE_COMMERCE_API_KEY || "";
50
+ const API_VERSION = process.env.COINBASE_COMMERCE_API_VERSION || "2018-03-22";
51
+ const BASE_URL = "https://api.commerce.coinbase.com";
52
+
53
+ async function coinbaseRequest(method: string, path: string, body?: unknown): Promise<unknown> {
54
+ const res = await fetch(`${BASE_URL}${path}`, {
55
+ method,
56
+ headers: {
57
+ "Content-Type": "application/json",
58
+ "X-CC-Api-Key": API_KEY,
59
+ "X-CC-Version": API_VERSION,
60
+ },
61
+ body: body ? JSON.stringify(body) : undefined,
62
+ });
63
+ if (!res.ok) {
64
+ const err = await res.text();
65
+ throw new Error(`Coinbase Commerce API ${res.status}: ${err}`);
66
+ }
67
+ const text = await res.text();
68
+ if (!text) return {};
69
+ try {
70
+ return JSON.parse(text);
71
+ } catch {
72
+ return { raw: text };
73
+ }
74
+ }
75
+
76
+ function qs(params: Record<string, unknown>): string {
77
+ const entries = Object.entries(params).filter(([, v]) => v !== undefined && v !== null && v !== "");
78
+ if (entries.length === 0) return "";
79
+ const search = new URLSearchParams();
80
+ for (const [k, v] of entries) search.set(k, String(v));
81
+ return `?${search.toString()}`;
82
+ }
83
+
84
+ const server = new Server(
85
+ { name: "mcp-coinbase-commerce", version: "0.1.0" },
86
+ { capabilities: { tools: {} } }
87
+ );
88
+
89
+ server.setRequestHandler(ListToolsRequestSchema, async () => ({
90
+ tools: [
91
+ {
92
+ name: "create_charge",
93
+ description: "Create a crypto charge — a one-time merchant invoice priced in local fiat that a buyer can settle in BTC, ETH, USDC, and other supported assets. Returns a hosted_url the buyer can be redirected to, plus per-asset payment addresses.",
94
+ inputSchema: {
95
+ type: "object",
96
+ properties: {
97
+ name: { type: "string", description: "Short product/order name shown on the hosted payment page" },
98
+ description: { type: "string", description: "Longer human-readable description of what the buyer is paying for" },
99
+ pricing_type: { type: "string", enum: ["fixed_price", "no_price"], description: "fixed_price: exact amount in local_price. no_price: buyer chooses (donations)." },
100
+ local_price: {
101
+ type: "object",
102
+ description: "Fiat-denominated price the charge is quoted in. Required when pricing_type is fixed_price.",
103
+ properties: {
104
+ amount: { type: "string", description: "Amount as a decimal string (e.g. \"29.90\")" },
105
+ currency: { type: "string", description: "ISO-4217 fiat currency code (e.g. USD, BRL, EUR, MXN)" },
106
+ },
107
+ required: ["amount", "currency"],
108
+ },
109
+ metadata: { type: "object", description: "Arbitrary JSON you want echoed back on events (customer_id, order_id, etc.)" },
110
+ redirect_url: { type: "string", description: "Browser redirect after a successful payment" },
111
+ cancel_url: { type: "string", description: "Browser redirect if the buyer abandons the hosted page" },
112
+ },
113
+ required: ["name", "description", "pricing_type"],
114
+ },
115
+ },
116
+ {
117
+ name: "retrieve_charge",
118
+ description: "Retrieve a charge by its Coinbase Commerce id OR its short code (the 8-character code embedded in the hosted URL). Returns current status, timeline, and payments.",
119
+ inputSchema: {
120
+ type: "object",
121
+ properties: {
122
+ code_or_id: { type: "string", description: "Charge id or short code" },
123
+ },
124
+ required: ["code_or_id"],
125
+ },
126
+ },
127
+ {
128
+ name: "list_charges",
129
+ description: "List charges, newest first. Supports cursor pagination via starting_after / ending_before.",
130
+ inputSchema: {
131
+ type: "object",
132
+ properties: {
133
+ limit: { type: "number", description: "Max results per page (default 25, max 100)" },
134
+ starting_after: { type: "string", description: "Cursor: return results after this charge id" },
135
+ ending_before: { type: "string", description: "Cursor: return results before this charge id" },
136
+ order: { type: "string", enum: ["asc", "desc"], description: "Sort order by created_at. Defaults to desc." },
137
+ },
138
+ },
139
+ },
140
+ {
141
+ name: "cancel_charge",
142
+ description: "Cancel a charge that has not yet been paid. Only charges in NEW status can be cancelled; once pending or completed the call will fail.",
143
+ inputSchema: {
144
+ type: "object",
145
+ properties: {
146
+ code: { type: "string", description: "Charge short code" },
147
+ },
148
+ required: ["code"],
149
+ },
150
+ },
151
+ {
152
+ name: "resolve_charge",
153
+ description: "Manually resolve a charge as paid. Used for out-of-band settlement (e.g. underpayment you accept, delayed confirmation you want to honour).",
154
+ inputSchema: {
155
+ type: "object",
156
+ properties: {
157
+ code: { type: "string", description: "Charge short code" },
158
+ },
159
+ required: ["code"],
160
+ },
161
+ },
162
+ {
163
+ name: "create_checkout",
164
+ description: "Create a reusable hosted checkout — think product-page-style link that can be paid multiple times. Good for evergreen SKUs and donation pages.",
165
+ inputSchema: {
166
+ type: "object",
167
+ properties: {
168
+ name: { type: "string", description: "Product / checkout name" },
169
+ description: { type: "string", description: "Longer description shown to buyers" },
170
+ pricing_type: { type: "string", enum: ["fixed_price", "no_price"], description: "fixed_price: exact amount in local_price. no_price: buyer chooses." },
171
+ local_price: {
172
+ type: "object",
173
+ description: "Fiat-denominated price. Required when pricing_type is fixed_price.",
174
+ properties: {
175
+ amount: { type: "string", description: "Amount as decimal string" },
176
+ currency: { type: "string", description: "ISO-4217 fiat currency code" },
177
+ },
178
+ required: ["amount", "currency"],
179
+ },
180
+ requested_info: {
181
+ type: "array",
182
+ description: "Buyer fields Coinbase should collect on the hosted page (e.g. [\"name\", \"email\"])",
183
+ items: { type: "string" },
184
+ },
185
+ },
186
+ required: ["name", "description", "pricing_type"],
187
+ },
188
+ },
189
+ {
190
+ name: "retrieve_checkout",
191
+ description: "Retrieve a checkout by id.",
192
+ inputSchema: {
193
+ type: "object",
194
+ properties: {
195
+ id: { type: "string", description: "Checkout id" },
196
+ },
197
+ required: ["id"],
198
+ },
199
+ },
200
+ {
201
+ name: "list_events",
202
+ description: "List events — the lifecycle signals (charge:created, charge:confirmed, charge:failed, charge:delayed, charge:pending, charge:resolved) that Coinbase Commerce also delivers via webhook. Useful for reconciliation and agent polling.",
203
+ inputSchema: {
204
+ type: "object",
205
+ properties: {
206
+ limit: { type: "number", description: "Max results per page (default 25, max 100)" },
207
+ starting_after: { type: "string", description: "Cursor: return results after this event id" },
208
+ ending_before: { type: "string", description: "Cursor: return results before this event id" },
209
+ order: { type: "string", enum: ["asc", "desc"], description: "Sort order by created_at. Defaults to desc." },
210
+ },
211
+ },
212
+ },
213
+ {
214
+ name: "create_invoice",
215
+ description: "Create an invoice — a directed bill sent to a specific named recipient. Unlike a charge, an invoice captures who it was issued to and has its own draft / viewed / paid lifecycle.",
216
+ inputSchema: {
217
+ type: "object",
218
+ properties: {
219
+ business_name: { type: "string", description: "Your business name shown on the invoice" },
220
+ customer_email: { type: "string", description: "Email of the invoice recipient" },
221
+ customer_name: { type: "string", description: "Display name of the invoice recipient" },
222
+ memo: { type: "string", description: "Free-form note to the recipient (appears on invoice)" },
223
+ local_price: {
224
+ type: "object",
225
+ description: "Fiat-denominated amount the invoice is quoted in",
226
+ properties: {
227
+ amount: { type: "string", description: "Amount as decimal string" },
228
+ currency: { type: "string", description: "ISO-4217 fiat currency code" },
229
+ },
230
+ required: ["amount", "currency"],
231
+ },
232
+ },
233
+ required: ["business_name", "customer_email", "customer_name", "local_price"],
234
+ },
235
+ },
236
+ ],
237
+ }));
238
+
239
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
240
+ const { name, arguments: args } = request.params;
241
+ const a = (args ?? {}) as Record<string, unknown>;
242
+
243
+ try {
244
+ switch (name) {
245
+ case "create_charge":
246
+ return { content: [{ type: "text", text: JSON.stringify(await coinbaseRequest("POST", "/charges", a), null, 2) }] };
247
+ case "retrieve_charge":
248
+ return { content: [{ type: "text", text: JSON.stringify(await coinbaseRequest("GET", `/charges/${encodeURIComponent(String(a.code_or_id ?? ""))}`), null, 2) }] };
249
+ case "list_charges": {
250
+ const query = qs({
251
+ limit: a.limit,
252
+ starting_after: a.starting_after,
253
+ ending_before: a.ending_before,
254
+ order: a.order,
255
+ });
256
+ return { content: [{ type: "text", text: JSON.stringify(await coinbaseRequest("GET", `/charges${query}`), null, 2) }] };
257
+ }
258
+ case "cancel_charge":
259
+ return { content: [{ type: "text", text: JSON.stringify(await coinbaseRequest("POST", `/charges/${encodeURIComponent(String(a.code ?? ""))}/cancel`), null, 2) }] };
260
+ case "resolve_charge":
261
+ return { content: [{ type: "text", text: JSON.stringify(await coinbaseRequest("POST", `/charges/${encodeURIComponent(String(a.code ?? ""))}/resolve`), null, 2) }] };
262
+ case "create_checkout":
263
+ return { content: [{ type: "text", text: JSON.stringify(await coinbaseRequest("POST", "/checkouts", a), null, 2) }] };
264
+ case "retrieve_checkout":
265
+ return { content: [{ type: "text", text: JSON.stringify(await coinbaseRequest("GET", `/checkouts/${encodeURIComponent(String(a.id ?? ""))}`), null, 2) }] };
266
+ case "list_events": {
267
+ const query = qs({
268
+ limit: a.limit,
269
+ starting_after: a.starting_after,
270
+ ending_before: a.ending_before,
271
+ order: a.order,
272
+ });
273
+ return { content: [{ type: "text", text: JSON.stringify(await coinbaseRequest("GET", `/events${query}`), null, 2) }] };
274
+ }
275
+ case "create_invoice":
276
+ return { content: [{ type: "text", text: JSON.stringify(await coinbaseRequest("POST", "/invoices", a), null, 2) }] };
277
+ default:
278
+ return { content: [{ type: "text", text: `Unknown tool: ${name}` }], isError: true };
279
+ }
280
+ } catch (err) {
281
+ return { content: [{ type: "text", text: `Error: ${err instanceof Error ? err.message : String(err)}` }], isError: true };
282
+ }
283
+ });
284
+
285
+ async function main() {
286
+ if (process.argv.includes("--http") || process.env.MCP_HTTP === "true") {
287
+ const { default: express } = await import("express");
288
+ const { randomUUID } = await import("node:crypto");
289
+ const app = express();
290
+ app.use(express.json());
291
+ const transports = new Map<string, StreamableHTTPServerTransport>();
292
+ app.get("/health", (_req: unknown, res: { json: (body: unknown) => unknown }) => res.json({ status: "ok", sessions: transports.size }));
293
+ app.post("/mcp", async (req: { headers: Record<string, string | string[] | undefined>; body: unknown }, res: { status: (code: number) => { json: (body: unknown) => unknown } }) => {
294
+ const sid = req.headers["mcp-session-id"] as string | undefined;
295
+ if (sid && transports.has(sid)) { await transports.get(sid)!.handleRequest(req as never, res as never, req.body); return; }
296
+ if (!sid && isInitializeRequest(req.body)) {
297
+ const t = new StreamableHTTPServerTransport({ sessionIdGenerator: () => randomUUID(), onsessioninitialized: (id) => { transports.set(id, t); } });
298
+ t.onclose = () => { if (t.sessionId) transports.delete(t.sessionId); };
299
+ const s = new Server({ name: "mcp-coinbase-commerce", version: "0.1.0" }, { capabilities: { tools: {} } });
300
+ (server as unknown as { _requestHandlers: Map<unknown, unknown> })._requestHandlers.forEach((v, k) => (s as unknown as { _requestHandlers: Map<unknown, unknown> })._requestHandlers.set(k, v));
301
+ (server as unknown as { _notificationHandlers?: Map<unknown, unknown> })._notificationHandlers?.forEach((v, k) => (s as unknown as { _notificationHandlers: Map<unknown, unknown> })._notificationHandlers.set(k, v));
302
+ await s.connect(t);
303
+ await t.handleRequest(req as never, res as never, req.body); return;
304
+ }
305
+ res.status(400).json({ jsonrpc: "2.0", error: { code: -32000, message: "Bad Request" }, id: null });
306
+ });
307
+ app.get("/mcp", async (req: { headers: Record<string, string | string[] | undefined> }, res: { status: (code: number) => { send: (body: string) => unknown } }) => { const sid = req.headers["mcp-session-id"] as string; if (sid && transports.has(sid)) await transports.get(sid)!.handleRequest(req as never, res as never); else res.status(400).send("Invalid session"); });
308
+ app.delete("/mcp", async (req: { headers: Record<string, string | string[] | undefined> }, res: { status: (code: number) => { send: (body: string) => unknown } }) => { const sid = req.headers["mcp-session-id"] as string; if (sid && transports.has(sid)) await transports.get(sid)!.handleRequest(req as never, res as never); else res.status(400).send("Invalid session"); });
309
+ const port = Number(process.env.MCP_PORT) || 3000;
310
+ app.listen(port, () => { console.error(`MCP HTTP server on http://localhost:${port}/mcp`); });
311
+ } else {
312
+ const transport = new StdioServerTransport();
313
+ await server.connect(transport);
314
+ }
315
+ }
316
+
317
+ main().catch(console.error);
package/tsconfig.json ADDED
@@ -0,0 +1,14 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "NodeNext",
5
+ "moduleResolution": "NodeNext",
6
+ "outDir": "./dist",
7
+ "rootDir": "./src",
8
+ "strict": true,
9
+ "skipLibCheck": true,
10
+ "esModuleInterop": true,
11
+ "declaration": true
12
+ },
13
+ "include": ["src"]
14
+ }