@codespar/mcp-moonpay 0.1.0 → 0.2.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/dist/index.js CHANGED
@@ -9,7 +9,7 @@
9
9
  * option for agents paying out in crypto or end users buying crypto with
10
10
  * local currency.
11
11
  *
12
- * Tools (10):
12
+ * Tools (20):
13
13
  * get_buy_quote — preview a fiat -> crypto exchange before committing
14
14
  * create_buy_transaction — create a buy transaction (fiat -> crypto)
15
15
  * get_buy_transaction — retrieve a buy transaction by id
@@ -17,18 +17,35 @@
17
17
  * get_sell_quote — preview a crypto -> fiat exchange
18
18
  * create_sell_transaction — create a sell transaction (crypto -> fiat)
19
19
  * get_sell_transaction — retrieve a sell transaction by id
20
+ * refund_sell_transaction — request a refund on an off-ramp transaction
20
21
  * create_customer — create a KYC'd end user
21
22
  * get_customer — retrieve a customer by id
23
+ * get_customer_kyc_status — fetch KYC verification status for a customer
24
+ * list_customer_transactions — list all (buy + sell) transactions tied to a customer
25
+ * get_transaction_receipt — fetch a tax-/audit-grade receipt for a completed transaction
22
26
  * list_currencies — list supported fiat + crypto assets (dynamic discovery)
27
+ * get_currency — retrieve metadata for a single currency by code
28
+ * list_countries — list supported countries with allowed flows (buy/sell) per geography
29
+ * list_payment_methods — list payment methods supported for a fiat / country pair
30
+ * get_user_country — IP-based geolocation + alpha-3 country code (compliance helper)
31
+ * sign_buy_url — HMAC-sign a hosted-checkout buy widget URL with apiKey + params
32
+ * sign_sell_url — HMAC-sign a hosted-checkout sell widget URL with apiKey + params
23
33
  *
24
34
  * Authentication
25
- * Every request carries:
35
+ * REST API requests carry:
26
36
  * Authorization: Api-Key <API_KEY>
27
37
  * Sandbox vs production is selected by which key you pass; the base URL is the same.
28
38
  *
39
+ * Hosted widget URLs (buy.moonpay.com / sell.moonpay.com) are HMAC-SHA256
40
+ * signed using the publishable key + secret key (see sign_buy_url / sign_sell_url).
41
+ *
29
42
  * Environment
30
- * MOONPAY_API_KEY — API key (required, secret)
31
- * MOONPAY_BASE_URL — optional; defaults to https://api.moonpay.com
43
+ * MOONPAY_API_KEY REST API key (required, secret)
44
+ * MOONPAY_PUBLISHABLE_KEY publishable key for widget URLs (optional, used by sign_*_url)
45
+ * MOONPAY_SECRET_KEY — secret key for HMAC signing widget URLs (optional, used by sign_*_url)
46
+ * MOONPAY_BASE_URL — optional; defaults to https://api.moonpay.com
47
+ * MOONPAY_BUY_WIDGET_URL — optional; defaults to https://buy.moonpay.com
48
+ * MOONPAY_SELL_WIDGET_URL — optional; defaults to https://sell.moonpay.com
32
49
  *
33
50
  * Docs: https://dev.moonpay.com
34
51
  */
@@ -37,8 +54,13 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
37
54
  import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
38
55
  import { isInitializeRequest } from "@modelcontextprotocol/sdk/types.js";
39
56
  import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
57
+ import { createHmac } from "node:crypto";
40
58
  const API_KEY = process.env.MOONPAY_API_KEY || "";
59
+ const PUBLISHABLE_KEY = process.env.MOONPAY_PUBLISHABLE_KEY || "";
60
+ const SECRET_KEY = process.env.MOONPAY_SECRET_KEY || "";
41
61
  const BASE_URL = process.env.MOONPAY_BASE_URL || "https://api.moonpay.com";
62
+ const BUY_WIDGET_URL = process.env.MOONPAY_BUY_WIDGET_URL || "https://buy.moonpay.com";
63
+ const SELL_WIDGET_URL = process.env.MOONPAY_SELL_WIDGET_URL || "https://sell.moonpay.com";
42
64
  async function moonpayRequest(method, path, body) {
43
65
  const res = await fetch(`${BASE_URL}${path}`, {
44
66
  method,
@@ -72,7 +94,36 @@ function qs(params) {
72
94
  search.set(k, String(v));
73
95
  return `?${search.toString()}`;
74
96
  }
75
- const server = new Server({ name: "mcp-moonpay", version: "0.1.0" }, { capabilities: { tools: {} } });
97
+ /**
98
+ * HMAC-SHA256 sign a MoonPay widget URL.
99
+ *
100
+ * MoonPay's hosted widget (buy.moonpay.com / sell.moonpay.com) requires that
101
+ * the query string be signed with the merchant's secret key. The signature is
102
+ * computed over the URL's query portion (including the leading `?`) and
103
+ * appended as `&signature=<base64>`.
104
+ *
105
+ * Requires MOONPAY_PUBLISHABLE_KEY (added as `apiKey` param) and
106
+ * MOONPAY_SECRET_KEY (used as the HMAC key) to be set.
107
+ */
108
+ function signWidgetUrl(widgetBase, params) {
109
+ if (!PUBLISHABLE_KEY)
110
+ throw new Error("MOONPAY_PUBLISHABLE_KEY is not set; cannot sign widget URL.");
111
+ if (!SECRET_KEY)
112
+ throw new Error("MOONPAY_SECRET_KEY is not set; cannot sign widget URL.");
113
+ const merged = { apiKey: PUBLISHABLE_KEY, ...params };
114
+ const entries = Object.entries(merged).filter(([, v]) => v !== undefined && v !== null && v !== "");
115
+ const search = new URLSearchParams();
116
+ for (const [k, v] of entries) {
117
+ if (typeof v === "object")
118
+ search.set(k, JSON.stringify(v));
119
+ else
120
+ search.set(k, String(v));
121
+ }
122
+ const query = `?${search.toString()}`;
123
+ const signature = createHmac("sha256", SECRET_KEY).update(query).digest("base64");
124
+ return `${widgetBase}${query}&signature=${encodeURIComponent(signature)}`;
125
+ }
126
+ const server = new Server({ name: "mcp-moonpay", version: "0.2.0" }, { capabilities: { tools: {} } });
76
127
  server.setRequestHandler(ListToolsRequestSchema, async () => ({
77
128
  tools: [
78
129
  {
@@ -182,6 +233,19 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
182
233
  required: ["id"],
183
234
  },
184
235
  },
236
+ {
237
+ name: "refund_sell_transaction",
238
+ description: "Request a refund on an off-ramp (sell) transaction. Used when the destination bank rejects payout or the user disputes the trade. Reason codes are MoonPay-defined.",
239
+ inputSchema: {
240
+ type: "object",
241
+ properties: {
242
+ id: { type: "string", description: "MoonPay sell transaction id to refund" },
243
+ reason: { type: "string", description: "Reason code or free-text justification for the refund" },
244
+ amount: { type: "number", description: "Optional partial refund amount (in the transaction's base/crypto currency). Omit for full refund." },
245
+ },
246
+ required: ["id"],
247
+ },
248
+ },
185
249
  {
186
250
  name: "create_customer",
187
251
  description: "Create a MoonPay customer (KYC'd end user). Required before creating transactions that must be tied to an identified individual.",
@@ -220,6 +284,42 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
220
284
  required: ["id"],
221
285
  },
222
286
  },
287
+ {
288
+ name: "get_customer_kyc_status",
289
+ description: "Fetch KYC verification status (and any pending document requirements) for a MoonPay customer. Use to gate flows that require an approved customer before transacting.",
290
+ inputSchema: {
291
+ type: "object",
292
+ properties: {
293
+ id: { type: "string", description: "MoonPay customer id" },
294
+ },
295
+ required: ["id"],
296
+ },
297
+ },
298
+ {
299
+ name: "list_customer_transactions",
300
+ description: "List all transactions (buy + sell) tied to a single MoonPay customer. Convenience wrapper for unified history / reconciliation by user.",
301
+ inputSchema: {
302
+ type: "object",
303
+ properties: {
304
+ customerId: { type: "string", description: "MoonPay customer id" },
305
+ status: { type: "string", description: "Optional status filter (e.g. pending, completed, failed)" },
306
+ limit: { type: "number", description: "Max results to return" },
307
+ },
308
+ required: ["customerId"],
309
+ },
310
+ },
311
+ {
312
+ name: "get_transaction_receipt",
313
+ description: "Fetch a tax-/audit-grade receipt for a completed buy or sell transaction. Useful for end-user reporting or accounting export.",
314
+ inputSchema: {
315
+ type: "object",
316
+ properties: {
317
+ id: { type: "string", description: "MoonPay transaction id (buy or sell)" },
318
+ type: { type: "string", enum: ["buy", "sell"], description: "Whether the id refers to a buy or sell transaction. Defaults to buy." },
319
+ },
320
+ required: ["id"],
321
+ },
322
+ },
223
323
  {
224
324
  name: "list_currencies",
225
325
  description: "List supported currencies (fiat + crypto). Essential for agents: use this to discover currency codes dynamically rather than hard-coding, and to check which assets/fiats are currently enabled.",
@@ -230,6 +330,96 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
230
330
  },
231
331
  },
232
332
  },
333
+ {
334
+ name: "get_currency",
335
+ description: "Retrieve metadata for a single currency (fiat or crypto) by its MoonPay code. Returns network, decimals, min/max amounts, fee structure.",
336
+ inputSchema: {
337
+ type: "object",
338
+ properties: {
339
+ currencyCode: { type: "string", description: "Currency code (e.g. btc, usdc, brl, usd)" },
340
+ },
341
+ required: ["currencyCode"],
342
+ },
343
+ },
344
+ {
345
+ name: "list_countries",
346
+ description: "List countries supported by MoonPay along with which flows (buy / sell / NFT) are allowed per geography. Use this to gate UI before initiating a quote.",
347
+ inputSchema: {
348
+ type: "object",
349
+ properties: {},
350
+ },
351
+ },
352
+ {
353
+ name: "list_payment_methods",
354
+ description: "List payment methods supported for a given fiat currency / country combination (e.g. credit_debit_card, sepa_bank_transfer, pix). Use to populate checkout selectors dynamically.",
355
+ inputSchema: {
356
+ type: "object",
357
+ properties: {
358
+ currencyCode: { type: "string", description: "Fiat currency code (e.g. brl, usd, eur)" },
359
+ country: { type: "string", description: "ISO-3166 alpha-2 country code (e.g. BR, US, MX)" },
360
+ },
361
+ },
362
+ },
363
+ {
364
+ name: "get_user_country",
365
+ description: "Resolve the caller's (or a given IP's) country via MoonPay's IP-address geolocation endpoint. Returns ISO alpha-2 + alpha-3 country, plus state for US. Compliance helper to gate flows by jurisdiction before quoting or creating a transaction.",
366
+ inputSchema: {
367
+ type: "object",
368
+ properties: {
369
+ ipAddress: { type: "string", description: "Optional IP to check. If omitted, MoonPay resolves from the request origin." },
370
+ },
371
+ },
372
+ },
373
+ {
374
+ name: "sign_buy_url",
375
+ description: "Build and HMAC-SHA256 sign a MoonPay buy widget URL (buy.moonpay.com). Returns a ready-to-redirect URL with the merchant's apiKey + signature appended. Requires MOONPAY_PUBLISHABLE_KEY and MOONPAY_SECRET_KEY in the environment. Use when embedding the hosted onramp in your own UI.",
376
+ inputSchema: {
377
+ type: "object",
378
+ properties: {
379
+ currencyCode: { type: "string", description: "Crypto currency code to buy (e.g. btc, usdc)" },
380
+ baseCurrencyCode: { type: "string", description: "Fiat currency code (e.g. usd, brl)" },
381
+ baseCurrencyAmount: { type: "number", description: "Pre-fill fiat amount" },
382
+ quoteCurrencyAmount: { type: "number", description: "Pre-fill crypto amount" },
383
+ walletAddress: { type: "string", description: "Pre-fill destination wallet address" },
384
+ walletAddressTag: { type: "string", description: "Destination tag / memo for chains that require it" },
385
+ email: { type: "string", description: "Pre-fill end-user email" },
386
+ externalCustomerId: { type: "string", description: "Your internal user id, propagated to MoonPay" },
387
+ externalTransactionId: { type: "string", description: "Your internal transaction reference" },
388
+ redirectURL: { type: "string", description: "Where to redirect after the hosted flow completes" },
389
+ paymentMethod: { type: "string", description: "Pre-select payment method (e.g. credit_debit_card, pix)" },
390
+ theme: { type: "string", description: "Widget theme (light / dark)" },
391
+ colorCode: { type: "string", description: "Hex accent color for the widget" },
392
+ language: { type: "string", description: "BCP-47 language tag (e.g. en, pt-BR)" },
393
+ showWalletAddressForm: { type: "boolean", description: "If true, force the widget to show the wallet form even when prefilled" },
394
+ extraParams: { type: "object", description: "Additional widget parameters passed through as-is (object values are JSON-stringified)" },
395
+ },
396
+ required: ["currencyCode"],
397
+ },
398
+ },
399
+ {
400
+ name: "sign_sell_url",
401
+ description: "Build and HMAC-SHA256 sign a MoonPay sell widget URL (sell.moonpay.com). Returns a ready-to-redirect URL with apiKey + signature appended. Requires MOONPAY_PUBLISHABLE_KEY and MOONPAY_SECRET_KEY in the environment.",
402
+ inputSchema: {
403
+ type: "object",
404
+ properties: {
405
+ baseCurrencyCode: { type: "string", description: "Crypto currency code being sold (e.g. btc, usdc)" },
406
+ quoteCurrencyCode: { type: "string", description: "Fiat currency to receive (e.g. usd, brl)" },
407
+ baseCurrencyAmount: { type: "number", description: "Pre-fill crypto amount" },
408
+ quoteCurrencyAmount: { type: "number", description: "Pre-fill fiat amount" },
409
+ refundWalletAddress: { type: "string", description: "Wallet to refund crypto to if the sell fails" },
410
+ email: { type: "string", description: "Pre-fill end-user email" },
411
+ externalCustomerId: { type: "string", description: "Your internal user id" },
412
+ externalTransactionId: { type: "string", description: "Your internal transaction reference" },
413
+ redirectURL: { type: "string", description: "Where to redirect after the hosted flow completes" },
414
+ payoutMethod: { type: "string", description: "Pre-select payout method (e.g. sepa_bank_transfer, pix)" },
415
+ theme: { type: "string", description: "Widget theme (light / dark)" },
416
+ colorCode: { type: "string", description: "Hex accent color for the widget" },
417
+ language: { type: "string", description: "BCP-47 language tag (e.g. en, pt-BR)" },
418
+ extraParams: { type: "object", description: "Additional widget parameters passed through as-is (object values are JSON-stringified)" },
419
+ },
420
+ required: ["baseCurrencyCode"],
421
+ },
422
+ },
233
423
  ],
234
424
  }));
235
425
  server.setRequestHandler(CallToolRequestSchema, async (request) => {
@@ -275,14 +465,62 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
275
465
  return { content: [{ type: "text", text: JSON.stringify(await moonpayRequest("POST", "/v3/sell_transactions", a), null, 2) }] };
276
466
  case "get_sell_transaction":
277
467
  return { content: [{ type: "text", text: JSON.stringify(await moonpayRequest("GET", `/v3/sell_transactions/${encodeURIComponent(String(a.id ?? ""))}`), null, 2) }] };
468
+ case "refund_sell_transaction": {
469
+ const id = encodeURIComponent(String(a.id ?? ""));
470
+ const body = {};
471
+ if (a.reason !== undefined)
472
+ body.reason = a.reason;
473
+ if (a.amount !== undefined)
474
+ body.amount = a.amount;
475
+ return { content: [{ type: "text", text: JSON.stringify(await moonpayRequest("POST", `/v3/sell_transactions/${id}/refund`, body), null, 2) }] };
476
+ }
278
477
  case "create_customer":
279
478
  return { content: [{ type: "text", text: JSON.stringify(await moonpayRequest("POST", "/v1/customers", a), null, 2) }] };
280
479
  case "get_customer":
281
480
  return { content: [{ type: "text", text: JSON.stringify(await moonpayRequest("GET", `/v1/customers/${encodeURIComponent(String(a.id ?? ""))}`), null, 2) }] };
481
+ case "get_customer_kyc_status": {
482
+ const id = encodeURIComponent(String(a.id ?? ""));
483
+ return { content: [{ type: "text", text: JSON.stringify(await moonpayRequest("GET", `/v1/customers/${id}/kyc_status`), null, 2) }] };
484
+ }
485
+ case "list_customer_transactions": {
486
+ const id = encodeURIComponent(String(a.customerId ?? ""));
487
+ const query = qs({ status: a.status, limit: a.limit });
488
+ return { content: [{ type: "text", text: JSON.stringify(await moonpayRequest("GET", `/v1/customers/${id}/transactions${query}`), null, 2) }] };
489
+ }
490
+ case "get_transaction_receipt": {
491
+ const id = encodeURIComponent(String(a.id ?? ""));
492
+ const type = String(a.type ?? "buy");
493
+ const path = type === "sell" ? `/v3/sell_transactions/${id}/receipt` : `/v1/transactions/${id}/receipt`;
494
+ return { content: [{ type: "text", text: JSON.stringify(await moonpayRequest("GET", path), null, 2) }] };
495
+ }
282
496
  case "list_currencies": {
283
497
  const query = qs({ show: a.show });
284
498
  return { content: [{ type: "text", text: JSON.stringify(await moonpayRequest("GET", `/v3/currencies${query}`), null, 2) }] };
285
499
  }
500
+ case "get_currency": {
501
+ const code = encodeURIComponent(String(a.currencyCode ?? ""));
502
+ return { content: [{ type: "text", text: JSON.stringify(await moonpayRequest("GET", `/v3/currencies/${code}`), null, 2) }] };
503
+ }
504
+ case "list_countries":
505
+ return { content: [{ type: "text", text: JSON.stringify(await moonpayRequest("GET", `/v3/countries`), null, 2) }] };
506
+ case "list_payment_methods": {
507
+ const query = qs({ currencyCode: a.currencyCode, country: a.country });
508
+ return { content: [{ type: "text", text: JSON.stringify(await moonpayRequest("GET", `/v3/payment_methods${query}`), null, 2) }] };
509
+ }
510
+ case "get_user_country": {
511
+ const query = qs({ ipAddress: a.ipAddress });
512
+ return { content: [{ type: "text", text: JSON.stringify(await moonpayRequest("GET", `/v4/ip_address${query}`), null, 2) }] };
513
+ }
514
+ case "sign_buy_url": {
515
+ const { extraParams, ...rest } = a;
516
+ const url = signWidgetUrl(BUY_WIDGET_URL, { ...rest, ...(extraParams ?? {}) });
517
+ return { content: [{ type: "text", text: JSON.stringify({ url }, null, 2) }] };
518
+ }
519
+ case "sign_sell_url": {
520
+ const { extraParams, ...rest } = a;
521
+ const url = signWidgetUrl(SELL_WIDGET_URL, { ...rest, ...(extraParams ?? {}) });
522
+ return { content: [{ type: "text", text: JSON.stringify({ url }, null, 2) }] };
523
+ }
286
524
  default:
287
525
  return { content: [{ type: "text", text: `Unknown tool: ${name}` }], isError: true };
288
526
  }
@@ -309,7 +547,7 @@ async function main() {
309
547
  const t = new StreamableHTTPServerTransport({ sessionIdGenerator: () => randomUUID(), onsessioninitialized: (id) => { transports.set(id, t); } });
310
548
  t.onclose = () => { if (t.sessionId)
311
549
  transports.delete(t.sessionId); };
312
- const s = new Server({ name: "mcp-moonpay", version: "0.1.0" }, { capabilities: { tools: {} } });
550
+ const s = new Server({ name: "mcp-moonpay", version: "0.2.0" }, { capabilities: { tools: {} } });
313
551
  server._requestHandlers.forEach((v, k) => s._requestHandlers.set(k, v));
314
552
  server._notificationHandlers?.forEach((v, k) => s._notificationHandlers.set(k, v));
315
553
  await s.connect(t);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@codespar/mcp-moonpay",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "MCP server for MoonPay — fiat-to-crypto on/off-ramp covering 100+ crypto assets, multi-geography, Pix supported for Brazil",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
package/server.json CHANGED
@@ -7,29 +7,57 @@
7
7
  "source": "github",
8
8
  "subfolder": "packages/crypto/moonpay"
9
9
  },
10
- "version": "0.1.0",
10
+ "version": "0.2.0",
11
11
  "packages": [
12
12
  {
13
13
  "registryType": "npm",
14
14
  "identifier": "@codespar/mcp-moonpay",
15
- "version": "0.1.0",
15
+ "version": "0.2.0",
16
16
  "transport": {
17
17
  "type": "stdio"
18
18
  },
19
19
  "environmentVariables": [
20
20
  {
21
21
  "name": "MOONPAY_API_KEY",
22
- "description": "MoonPay API key (sandbox or production — the key selects the environment)",
22
+ "description": "MoonPay REST API key (sandbox or production — the key selects the environment)",
23
23
  "isRequired": true,
24
24
  "format": "string",
25
25
  "isSecret": true
26
26
  },
27
+ {
28
+ "name": "MOONPAY_PUBLISHABLE_KEY",
29
+ "description": "MoonPay publishable key used as `apiKey` in signed widget URLs (required for sign_buy_url / sign_sell_url).",
30
+ "isRequired": false,
31
+ "format": "string",
32
+ "isSecret": false
33
+ },
34
+ {
35
+ "name": "MOONPAY_SECRET_KEY",
36
+ "description": "MoonPay secret key used to HMAC-SHA256 sign widget URLs (required for sign_buy_url / sign_sell_url).",
37
+ "isRequired": false,
38
+ "format": "string",
39
+ "isSecret": true
40
+ },
27
41
  {
28
42
  "name": "MOONPAY_BASE_URL",
29
43
  "description": "MoonPay API base URL. Defaults to https://api.moonpay.com.",
30
44
  "isRequired": false,
31
45
  "format": "string",
32
46
  "isSecret": false
47
+ },
48
+ {
49
+ "name": "MOONPAY_BUY_WIDGET_URL",
50
+ "description": "MoonPay buy widget base URL. Defaults to https://buy.moonpay.com.",
51
+ "isRequired": false,
52
+ "format": "string",
53
+ "isSecret": false
54
+ },
55
+ {
56
+ "name": "MOONPAY_SELL_WIDGET_URL",
57
+ "description": "MoonPay sell widget base URL. Defaults to https://sell.moonpay.com.",
58
+ "isRequired": false,
59
+ "format": "string",
60
+ "isSecret": false
33
61
  }
34
62
  ]
35
63
  }
package/src/index.ts CHANGED
@@ -10,7 +10,7 @@
10
10
  * option for agents paying out in crypto or end users buying crypto with
11
11
  * local currency.
12
12
  *
13
- * Tools (10):
13
+ * Tools (20):
14
14
  * get_buy_quote — preview a fiat -> crypto exchange before committing
15
15
  * create_buy_transaction — create a buy transaction (fiat -> crypto)
16
16
  * get_buy_transaction — retrieve a buy transaction by id
@@ -18,18 +18,35 @@
18
18
  * get_sell_quote — preview a crypto -> fiat exchange
19
19
  * create_sell_transaction — create a sell transaction (crypto -> fiat)
20
20
  * get_sell_transaction — retrieve a sell transaction by id
21
+ * refund_sell_transaction — request a refund on an off-ramp transaction
21
22
  * create_customer — create a KYC'd end user
22
23
  * get_customer — retrieve a customer by id
24
+ * get_customer_kyc_status — fetch KYC verification status for a customer
25
+ * list_customer_transactions — list all (buy + sell) transactions tied to a customer
26
+ * get_transaction_receipt — fetch a tax-/audit-grade receipt for a completed transaction
23
27
  * list_currencies — list supported fiat + crypto assets (dynamic discovery)
28
+ * get_currency — retrieve metadata for a single currency by code
29
+ * list_countries — list supported countries with allowed flows (buy/sell) per geography
30
+ * list_payment_methods — list payment methods supported for a fiat / country pair
31
+ * get_user_country — IP-based geolocation + alpha-3 country code (compliance helper)
32
+ * sign_buy_url — HMAC-sign a hosted-checkout buy widget URL with apiKey + params
33
+ * sign_sell_url — HMAC-sign a hosted-checkout sell widget URL with apiKey + params
24
34
  *
25
35
  * Authentication
26
- * Every request carries:
36
+ * REST API requests carry:
27
37
  * Authorization: Api-Key <API_KEY>
28
38
  * Sandbox vs production is selected by which key you pass; the base URL is the same.
29
39
  *
40
+ * Hosted widget URLs (buy.moonpay.com / sell.moonpay.com) are HMAC-SHA256
41
+ * signed using the publishable key + secret key (see sign_buy_url / sign_sell_url).
42
+ *
30
43
  * Environment
31
- * MOONPAY_API_KEY — API key (required, secret)
32
- * MOONPAY_BASE_URL — optional; defaults to https://api.moonpay.com
44
+ * MOONPAY_API_KEY REST API key (required, secret)
45
+ * MOONPAY_PUBLISHABLE_KEY publishable key for widget URLs (optional, used by sign_*_url)
46
+ * MOONPAY_SECRET_KEY — secret key for HMAC signing widget URLs (optional, used by sign_*_url)
47
+ * MOONPAY_BASE_URL — optional; defaults to https://api.moonpay.com
48
+ * MOONPAY_BUY_WIDGET_URL — optional; defaults to https://buy.moonpay.com
49
+ * MOONPAY_SELL_WIDGET_URL — optional; defaults to https://sell.moonpay.com
33
50
  *
34
51
  * Docs: https://dev.moonpay.com
35
52
  */
@@ -42,9 +59,14 @@ import {
42
59
  CallToolRequestSchema,
43
60
  ListToolsRequestSchema,
44
61
  } from "@modelcontextprotocol/sdk/types.js";
62
+ import { createHmac } from "node:crypto";
45
63
 
46
64
  const API_KEY = process.env.MOONPAY_API_KEY || "";
65
+ const PUBLISHABLE_KEY = process.env.MOONPAY_PUBLISHABLE_KEY || "";
66
+ const SECRET_KEY = process.env.MOONPAY_SECRET_KEY || "";
47
67
  const BASE_URL = process.env.MOONPAY_BASE_URL || "https://api.moonpay.com";
68
+ const BUY_WIDGET_URL = process.env.MOONPAY_BUY_WIDGET_URL || "https://buy.moonpay.com";
69
+ const SELL_WIDGET_URL = process.env.MOONPAY_SELL_WIDGET_URL || "https://sell.moonpay.com";
48
70
 
49
71
  async function moonpayRequest(method: string, path: string, body?: unknown): Promise<unknown> {
50
72
  const res = await fetch(`${BASE_URL}${path}`, {
@@ -77,8 +99,34 @@ function qs(params: Record<string, unknown>): string {
77
99
  return `?${search.toString()}`;
78
100
  }
79
101
 
102
+ /**
103
+ * HMAC-SHA256 sign a MoonPay widget URL.
104
+ *
105
+ * MoonPay's hosted widget (buy.moonpay.com / sell.moonpay.com) requires that
106
+ * the query string be signed with the merchant's secret key. The signature is
107
+ * computed over the URL's query portion (including the leading `?`) and
108
+ * appended as `&signature=<base64>`.
109
+ *
110
+ * Requires MOONPAY_PUBLISHABLE_KEY (added as `apiKey` param) and
111
+ * MOONPAY_SECRET_KEY (used as the HMAC key) to be set.
112
+ */
113
+ function signWidgetUrl(widgetBase: string, params: Record<string, unknown>): string {
114
+ if (!PUBLISHABLE_KEY) throw new Error("MOONPAY_PUBLISHABLE_KEY is not set; cannot sign widget URL.");
115
+ if (!SECRET_KEY) throw new Error("MOONPAY_SECRET_KEY is not set; cannot sign widget URL.");
116
+ const merged: Record<string, unknown> = { apiKey: PUBLISHABLE_KEY, ...params };
117
+ const entries = Object.entries(merged).filter(([, v]) => v !== undefined && v !== null && v !== "");
118
+ const search = new URLSearchParams();
119
+ for (const [k, v] of entries) {
120
+ if (typeof v === "object") search.set(k, JSON.stringify(v));
121
+ else search.set(k, String(v));
122
+ }
123
+ const query = `?${search.toString()}`;
124
+ const signature = createHmac("sha256", SECRET_KEY).update(query).digest("base64");
125
+ return `${widgetBase}${query}&signature=${encodeURIComponent(signature)}`;
126
+ }
127
+
80
128
  const server = new Server(
81
- { name: "mcp-moonpay", version: "0.1.0" },
129
+ { name: "mcp-moonpay", version: "0.2.0" },
82
130
  { capabilities: { tools: {} } }
83
131
  );
84
132
 
@@ -191,6 +239,19 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
191
239
  required: ["id"],
192
240
  },
193
241
  },
242
+ {
243
+ name: "refund_sell_transaction",
244
+ description: "Request a refund on an off-ramp (sell) transaction. Used when the destination bank rejects payout or the user disputes the trade. Reason codes are MoonPay-defined.",
245
+ inputSchema: {
246
+ type: "object",
247
+ properties: {
248
+ id: { type: "string", description: "MoonPay sell transaction id to refund" },
249
+ reason: { type: "string", description: "Reason code or free-text justification for the refund" },
250
+ amount: { type: "number", description: "Optional partial refund amount (in the transaction's base/crypto currency). Omit for full refund." },
251
+ },
252
+ required: ["id"],
253
+ },
254
+ },
194
255
  {
195
256
  name: "create_customer",
196
257
  description: "Create a MoonPay customer (KYC'd end user). Required before creating transactions that must be tied to an identified individual.",
@@ -229,6 +290,42 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
229
290
  required: ["id"],
230
291
  },
231
292
  },
293
+ {
294
+ name: "get_customer_kyc_status",
295
+ description: "Fetch KYC verification status (and any pending document requirements) for a MoonPay customer. Use to gate flows that require an approved customer before transacting.",
296
+ inputSchema: {
297
+ type: "object",
298
+ properties: {
299
+ id: { type: "string", description: "MoonPay customer id" },
300
+ },
301
+ required: ["id"],
302
+ },
303
+ },
304
+ {
305
+ name: "list_customer_transactions",
306
+ description: "List all transactions (buy + sell) tied to a single MoonPay customer. Convenience wrapper for unified history / reconciliation by user.",
307
+ inputSchema: {
308
+ type: "object",
309
+ properties: {
310
+ customerId: { type: "string", description: "MoonPay customer id" },
311
+ status: { type: "string", description: "Optional status filter (e.g. pending, completed, failed)" },
312
+ limit: { type: "number", description: "Max results to return" },
313
+ },
314
+ required: ["customerId"],
315
+ },
316
+ },
317
+ {
318
+ name: "get_transaction_receipt",
319
+ description: "Fetch a tax-/audit-grade receipt for a completed buy or sell transaction. Useful for end-user reporting or accounting export.",
320
+ inputSchema: {
321
+ type: "object",
322
+ properties: {
323
+ id: { type: "string", description: "MoonPay transaction id (buy or sell)" },
324
+ type: { type: "string", enum: ["buy", "sell"], description: "Whether the id refers to a buy or sell transaction. Defaults to buy." },
325
+ },
326
+ required: ["id"],
327
+ },
328
+ },
232
329
  {
233
330
  name: "list_currencies",
234
331
  description: "List supported currencies (fiat + crypto). Essential for agents: use this to discover currency codes dynamically rather than hard-coding, and to check which assets/fiats are currently enabled.",
@@ -239,6 +336,96 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
239
336
  },
240
337
  },
241
338
  },
339
+ {
340
+ name: "get_currency",
341
+ description: "Retrieve metadata for a single currency (fiat or crypto) by its MoonPay code. Returns network, decimals, min/max amounts, fee structure.",
342
+ inputSchema: {
343
+ type: "object",
344
+ properties: {
345
+ currencyCode: { type: "string", description: "Currency code (e.g. btc, usdc, brl, usd)" },
346
+ },
347
+ required: ["currencyCode"],
348
+ },
349
+ },
350
+ {
351
+ name: "list_countries",
352
+ description: "List countries supported by MoonPay along with which flows (buy / sell / NFT) are allowed per geography. Use this to gate UI before initiating a quote.",
353
+ inputSchema: {
354
+ type: "object",
355
+ properties: {},
356
+ },
357
+ },
358
+ {
359
+ name: "list_payment_methods",
360
+ description: "List payment methods supported for a given fiat currency / country combination (e.g. credit_debit_card, sepa_bank_transfer, pix). Use to populate checkout selectors dynamically.",
361
+ inputSchema: {
362
+ type: "object",
363
+ properties: {
364
+ currencyCode: { type: "string", description: "Fiat currency code (e.g. brl, usd, eur)" },
365
+ country: { type: "string", description: "ISO-3166 alpha-2 country code (e.g. BR, US, MX)" },
366
+ },
367
+ },
368
+ },
369
+ {
370
+ name: "get_user_country",
371
+ description: "Resolve the caller's (or a given IP's) country via MoonPay's IP-address geolocation endpoint. Returns ISO alpha-2 + alpha-3 country, plus state for US. Compliance helper to gate flows by jurisdiction before quoting or creating a transaction.",
372
+ inputSchema: {
373
+ type: "object",
374
+ properties: {
375
+ ipAddress: { type: "string", description: "Optional IP to check. If omitted, MoonPay resolves from the request origin." },
376
+ },
377
+ },
378
+ },
379
+ {
380
+ name: "sign_buy_url",
381
+ description: "Build and HMAC-SHA256 sign a MoonPay buy widget URL (buy.moonpay.com). Returns a ready-to-redirect URL with the merchant's apiKey + signature appended. Requires MOONPAY_PUBLISHABLE_KEY and MOONPAY_SECRET_KEY in the environment. Use when embedding the hosted onramp in your own UI.",
382
+ inputSchema: {
383
+ type: "object",
384
+ properties: {
385
+ currencyCode: { type: "string", description: "Crypto currency code to buy (e.g. btc, usdc)" },
386
+ baseCurrencyCode: { type: "string", description: "Fiat currency code (e.g. usd, brl)" },
387
+ baseCurrencyAmount: { type: "number", description: "Pre-fill fiat amount" },
388
+ quoteCurrencyAmount: { type: "number", description: "Pre-fill crypto amount" },
389
+ walletAddress: { type: "string", description: "Pre-fill destination wallet address" },
390
+ walletAddressTag: { type: "string", description: "Destination tag / memo for chains that require it" },
391
+ email: { type: "string", description: "Pre-fill end-user email" },
392
+ externalCustomerId: { type: "string", description: "Your internal user id, propagated to MoonPay" },
393
+ externalTransactionId: { type: "string", description: "Your internal transaction reference" },
394
+ redirectURL: { type: "string", description: "Where to redirect after the hosted flow completes" },
395
+ paymentMethod: { type: "string", description: "Pre-select payment method (e.g. credit_debit_card, pix)" },
396
+ theme: { type: "string", description: "Widget theme (light / dark)" },
397
+ colorCode: { type: "string", description: "Hex accent color for the widget" },
398
+ language: { type: "string", description: "BCP-47 language tag (e.g. en, pt-BR)" },
399
+ showWalletAddressForm: { type: "boolean", description: "If true, force the widget to show the wallet form even when prefilled" },
400
+ extraParams: { type: "object", description: "Additional widget parameters passed through as-is (object values are JSON-stringified)" },
401
+ },
402
+ required: ["currencyCode"],
403
+ },
404
+ },
405
+ {
406
+ name: "sign_sell_url",
407
+ description: "Build and HMAC-SHA256 sign a MoonPay sell widget URL (sell.moonpay.com). Returns a ready-to-redirect URL with apiKey + signature appended. Requires MOONPAY_PUBLISHABLE_KEY and MOONPAY_SECRET_KEY in the environment.",
408
+ inputSchema: {
409
+ type: "object",
410
+ properties: {
411
+ baseCurrencyCode: { type: "string", description: "Crypto currency code being sold (e.g. btc, usdc)" },
412
+ quoteCurrencyCode: { type: "string", description: "Fiat currency to receive (e.g. usd, brl)" },
413
+ baseCurrencyAmount: { type: "number", description: "Pre-fill crypto amount" },
414
+ quoteCurrencyAmount: { type: "number", description: "Pre-fill fiat amount" },
415
+ refundWalletAddress: { type: "string", description: "Wallet to refund crypto to if the sell fails" },
416
+ email: { type: "string", description: "Pre-fill end-user email" },
417
+ externalCustomerId: { type: "string", description: "Your internal user id" },
418
+ externalTransactionId: { type: "string", description: "Your internal transaction reference" },
419
+ redirectURL: { type: "string", description: "Where to redirect after the hosted flow completes" },
420
+ payoutMethod: { type: "string", description: "Pre-select payout method (e.g. sepa_bank_transfer, pix)" },
421
+ theme: { type: "string", description: "Widget theme (light / dark)" },
422
+ colorCode: { type: "string", description: "Hex accent color for the widget" },
423
+ language: { type: "string", description: "BCP-47 language tag (e.g. en, pt-BR)" },
424
+ extraParams: { type: "object", description: "Additional widget parameters passed through as-is (object values are JSON-stringified)" },
425
+ },
426
+ required: ["baseCurrencyCode"],
427
+ },
428
+ },
242
429
  ],
243
430
  }));
244
431
 
@@ -286,14 +473,60 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
286
473
  return { content: [{ type: "text", text: JSON.stringify(await moonpayRequest("POST", "/v3/sell_transactions", a), null, 2) }] };
287
474
  case "get_sell_transaction":
288
475
  return { content: [{ type: "text", text: JSON.stringify(await moonpayRequest("GET", `/v3/sell_transactions/${encodeURIComponent(String(a.id ?? ""))}`), null, 2) }] };
476
+ case "refund_sell_transaction": {
477
+ const id = encodeURIComponent(String(a.id ?? ""));
478
+ const body: Record<string, unknown> = {};
479
+ if (a.reason !== undefined) body.reason = a.reason;
480
+ if (a.amount !== undefined) body.amount = a.amount;
481
+ return { content: [{ type: "text", text: JSON.stringify(await moonpayRequest("POST", `/v3/sell_transactions/${id}/refund`, body), null, 2) }] };
482
+ }
289
483
  case "create_customer":
290
484
  return { content: [{ type: "text", text: JSON.stringify(await moonpayRequest("POST", "/v1/customers", a), null, 2) }] };
291
485
  case "get_customer":
292
486
  return { content: [{ type: "text", text: JSON.stringify(await moonpayRequest("GET", `/v1/customers/${encodeURIComponent(String(a.id ?? ""))}`), null, 2) }] };
487
+ case "get_customer_kyc_status": {
488
+ const id = encodeURIComponent(String(a.id ?? ""));
489
+ return { content: [{ type: "text", text: JSON.stringify(await moonpayRequest("GET", `/v1/customers/${id}/kyc_status`), null, 2) }] };
490
+ }
491
+ case "list_customer_transactions": {
492
+ const id = encodeURIComponent(String(a.customerId ?? ""));
493
+ const query = qs({ status: a.status, limit: a.limit });
494
+ return { content: [{ type: "text", text: JSON.stringify(await moonpayRequest("GET", `/v1/customers/${id}/transactions${query}`), null, 2) }] };
495
+ }
496
+ case "get_transaction_receipt": {
497
+ const id = encodeURIComponent(String(a.id ?? ""));
498
+ const type = String(a.type ?? "buy");
499
+ const path = type === "sell" ? `/v3/sell_transactions/${id}/receipt` : `/v1/transactions/${id}/receipt`;
500
+ return { content: [{ type: "text", text: JSON.stringify(await moonpayRequest("GET", path), null, 2) }] };
501
+ }
293
502
  case "list_currencies": {
294
503
  const query = qs({ show: a.show });
295
504
  return { content: [{ type: "text", text: JSON.stringify(await moonpayRequest("GET", `/v3/currencies${query}`), null, 2) }] };
296
505
  }
506
+ case "get_currency": {
507
+ const code = encodeURIComponent(String(a.currencyCode ?? ""));
508
+ return { content: [{ type: "text", text: JSON.stringify(await moonpayRequest("GET", `/v3/currencies/${code}`), null, 2) }] };
509
+ }
510
+ case "list_countries":
511
+ return { content: [{ type: "text", text: JSON.stringify(await moonpayRequest("GET", `/v3/countries`), null, 2) }] };
512
+ case "list_payment_methods": {
513
+ const query = qs({ currencyCode: a.currencyCode, country: a.country });
514
+ return { content: [{ type: "text", text: JSON.stringify(await moonpayRequest("GET", `/v3/payment_methods${query}`), null, 2) }] };
515
+ }
516
+ case "get_user_country": {
517
+ const query = qs({ ipAddress: a.ipAddress });
518
+ return { content: [{ type: "text", text: JSON.stringify(await moonpayRequest("GET", `/v4/ip_address${query}`), null, 2) }] };
519
+ }
520
+ case "sign_buy_url": {
521
+ const { extraParams, ...rest } = a as { extraParams?: Record<string, unknown> } & Record<string, unknown>;
522
+ const url = signWidgetUrl(BUY_WIDGET_URL, { ...rest, ...(extraParams ?? {}) });
523
+ return { content: [{ type: "text", text: JSON.stringify({ url }, null, 2) }] };
524
+ }
525
+ case "sign_sell_url": {
526
+ const { extraParams, ...rest } = a as { extraParams?: Record<string, unknown> } & Record<string, unknown>;
527
+ const url = signWidgetUrl(SELL_WIDGET_URL, { ...rest, ...(extraParams ?? {}) });
528
+ return { content: [{ type: "text", text: JSON.stringify({ url }, null, 2) }] };
529
+ }
297
530
  default:
298
531
  return { content: [{ type: "text", text: `Unknown tool: ${name}` }], isError: true };
299
532
  }
@@ -316,7 +549,7 @@ async function main() {
316
549
  if (!sid && isInitializeRequest(req.body)) {
317
550
  const t = new StreamableHTTPServerTransport({ sessionIdGenerator: () => randomUUID(), onsessioninitialized: (id) => { transports.set(id, t); } });
318
551
  t.onclose = () => { if (t.sessionId) transports.delete(t.sessionId); };
319
- const s = new Server({ name: "mcp-moonpay", version: "0.1.0" }, { capabilities: { tools: {} } });
552
+ const s = new Server({ name: "mcp-moonpay", version: "0.2.0" }, { capabilities: { tools: {} } });
320
553
  (server as unknown as { _requestHandlers: Map<unknown, unknown> })._requestHandlers.forEach((v, k) => (s as unknown as { _requestHandlers: Map<unknown, unknown> })._requestHandlers.set(k, v));
321
554
  (server as unknown as { _notificationHandlers?: Map<unknown, unknown> })._notificationHandlers?.forEach((v, k) => (s as unknown as { _notificationHandlers: Map<unknown, unknown> })._notificationHandlers.set(k, v));
322
555
  await s.connect(t);