@codespar/mcp-moonpay 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,68 @@
1
+ # @codespar/mcp-moonpay
2
+
3
+ MCP server for [MoonPay](https://moonpay.com) — fiat-to-crypto on/off-ramp.
4
+
5
+ MoonPay spans 100+ crypto assets and many geographies, with both buy (fiat → crypto) and sell (crypto → fiat) flows. Pix is supported as a Brazil onramp rail.
6
+
7
+ ## Positioning vs the rest of the catalog
8
+
9
+ | Server | Coverage | Direction |
10
+ |--------|----------|-----------|
11
+ | `@codespar/mcp-unblockpay` | BRL / MXN ↔ USDC | Onramp + offramp, stablecoin only |
12
+ | `@codespar/mcp-moonpay` | 100+ crypto assets, multi-geo, Pix for BR | Onramp + offramp |
13
+ | `@codespar/mcp-mercado-bitcoin`, `@codespar/mcp-bitso` | Exchange order books | Trade |
14
+ | `@codespar/mcp-circle` | USDC native rails | Stablecoin infra |
15
+
16
+ Use MoonPay when an agent needs broader crypto coverage (beyond USDC), longer-tail geographies, or a sell-side flow that pays out to local fiat.
17
+
18
+ ## Tools
19
+
20
+ | Tool | Purpose |
21
+ |------|---------|
22
+ | `get_buy_quote` | Preview a fiat → crypto quote before committing |
23
+ | `create_buy_transaction` | Create a buy transaction (fiat → crypto) |
24
+ | `get_buy_transaction` | Retrieve a buy transaction by id |
25
+ | `list_buy_transactions` | List buy transactions with filters |
26
+ | `get_sell_quote` | Preview a crypto → fiat quote |
27
+ | `create_sell_transaction` | Create a sell transaction (crypto → fiat) |
28
+ | `get_sell_transaction` | Retrieve a sell transaction by id |
29
+ | `create_customer` | Create a KYC'd end user |
30
+ | `get_customer` | Retrieve a customer by id |
31
+ | `list_currencies` | List supported fiat + crypto assets (dynamic discovery) |
32
+
33
+ ## Install
34
+
35
+ ```bash
36
+ npm install @codespar/mcp-moonpay
37
+ ```
38
+
39
+ ## Environment
40
+
41
+ ```bash
42
+ MOONPAY_API_KEY="..." # API key (sandbox or production — the key selects the environment)
43
+ MOONPAY_BASE_URL="..." # Optional. Defaults to https://api.moonpay.com.
44
+ ```
45
+
46
+ ## Authentication
47
+
48
+ Every request uses a simple header:
49
+
50
+ ```
51
+ Authorization: Api-Key <MOONPAY_API_KEY>
52
+ ```
53
+
54
+ Sandbox vs production is selected by the key itself — the base URL stays the same.
55
+
56
+ ## Run
57
+
58
+ ```bash
59
+ # stdio (default — for Claude Desktop, Cursor, etc)
60
+ npx @codespar/mcp-moonpay
61
+
62
+ # HTTP (for server-to-server testing)
63
+ MCP_HTTP=true MCP_PORT=3000 npx @codespar/mcp-moonpay
64
+ ```
65
+
66
+ ## License
67
+
68
+ MIT
package/dist/index.js ADDED
@@ -0,0 +1,337 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * MCP Server for MoonPay — fiat-to-crypto on/off-ramp.
4
+ *
5
+ * MoonPay spans 100+ crypto assets and many geographies, with both buy
6
+ * (fiat -> crypto) and sell (crypto -> fiat) flows. For LatAm, Pix is
7
+ * supported as a BR onramp rail. Complementary to UnblockPay (BRL/MXN
8
+ * <-> USDC) in the catalog: MoonPay is the broader-coverage, longer-tail
9
+ * option for agents paying out in crypto or end users buying crypto with
10
+ * local currency.
11
+ *
12
+ * Tools (10):
13
+ * get_buy_quote — preview a fiat -> crypto exchange before committing
14
+ * create_buy_transaction — create a buy transaction (fiat -> crypto)
15
+ * get_buy_transaction — retrieve a buy transaction by id
16
+ * list_buy_transactions — list buy transactions with filters
17
+ * get_sell_quote — preview a crypto -> fiat exchange
18
+ * create_sell_transaction — create a sell transaction (crypto -> fiat)
19
+ * get_sell_transaction — retrieve a sell transaction by id
20
+ * create_customer — create a KYC'd end user
21
+ * get_customer — retrieve a customer by id
22
+ * list_currencies — list supported fiat + crypto assets (dynamic discovery)
23
+ *
24
+ * Authentication
25
+ * Every request carries:
26
+ * Authorization: Api-Key <API_KEY>
27
+ * Sandbox vs production is selected by which key you pass; the base URL is the same.
28
+ *
29
+ * Environment
30
+ * MOONPAY_API_KEY — API key (required, secret)
31
+ * MOONPAY_BASE_URL — optional; defaults to https://api.moonpay.com
32
+ *
33
+ * Docs: https://dev.moonpay.com
34
+ */
35
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
36
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
37
+ import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
38
+ import { isInitializeRequest } from "@modelcontextprotocol/sdk/types.js";
39
+ import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
40
+ const API_KEY = process.env.MOONPAY_API_KEY || "";
41
+ const BASE_URL = process.env.MOONPAY_BASE_URL || "https://api.moonpay.com";
42
+ async function moonpayRequest(method, path, body) {
43
+ const res = await fetch(`${BASE_URL}${path}`, {
44
+ method,
45
+ headers: {
46
+ "Content-Type": "application/json",
47
+ "Authorization": `Api-Key ${API_KEY}`,
48
+ },
49
+ body: body ? JSON.stringify(body) : undefined,
50
+ });
51
+ if (!res.ok) {
52
+ const err = await res.text();
53
+ throw new Error(`MoonPay API ${res.status}: ${err}`);
54
+ }
55
+ // Some endpoints (e.g. 204 No Content) may not return JSON.
56
+ const text = await res.text();
57
+ if (!text)
58
+ return {};
59
+ try {
60
+ return JSON.parse(text);
61
+ }
62
+ catch {
63
+ return { raw: text };
64
+ }
65
+ }
66
+ function qs(params) {
67
+ const entries = Object.entries(params).filter(([, v]) => v !== undefined && v !== null && v !== "");
68
+ if (entries.length === 0)
69
+ return "";
70
+ const search = new URLSearchParams();
71
+ for (const [k, v] of entries)
72
+ search.set(k, String(v));
73
+ return `?${search.toString()}`;
74
+ }
75
+ const server = new Server({ name: "mcp-moonpay", version: "0.1.0" }, { capabilities: { tools: {} } });
76
+ server.setRequestHandler(ListToolsRequestSchema, async () => ({
77
+ tools: [
78
+ {
79
+ name: "get_buy_quote",
80
+ description: "Preview a fiat -> crypto buy quote in real time. Use this before create_buy_transaction to show the end user the exact crypto amount, fees, and effective rate.",
81
+ inputSchema: {
82
+ type: "object",
83
+ properties: {
84
+ currencyCode: { type: "string", description: "Crypto currency code you want to buy (e.g. btc, eth, usdc, sol). Must be a code returned by list_currencies." },
85
+ baseCurrencyCode: { type: "string", description: "Fiat currency you are paying with (e.g. usd, eur, brl, mxn)" },
86
+ baseCurrencyAmount: { type: "number", description: "Amount in fiat to spend (major units). Either this or quoteCurrencyAmount must be supplied." },
87
+ quoteCurrencyAmount: { type: "number", description: "Amount of crypto to receive (major units). Either this or baseCurrencyAmount must be supplied." },
88
+ paymentMethod: { type: "string", description: "Optional payment method hint (e.g. credit_debit_card, sepa_bank_transfer, pix)" },
89
+ areFeesIncluded: { type: "boolean", description: "If true, baseCurrencyAmount includes MoonPay fees." },
90
+ },
91
+ required: ["currencyCode", "baseCurrencyCode"],
92
+ },
93
+ },
94
+ {
95
+ name: "create_buy_transaction",
96
+ description: "Create a buy transaction (fiat -> crypto). The returned object contains status plus — depending on method — redirect URL for hosted checkout, Pix QR data, or card auth next steps.",
97
+ inputSchema: {
98
+ type: "object",
99
+ properties: {
100
+ baseCurrencyAmount: { type: "number", description: "Fiat amount to charge (major units)" },
101
+ baseCurrencyCode: { type: "string", description: "Fiat currency code (e.g. brl, usd)" },
102
+ currencyCode: { type: "string", description: "Crypto currency code to receive (e.g. btc, usdc)" },
103
+ walletAddress: { type: "string", description: "Destination crypto wallet address" },
104
+ walletAddressTag: { type: "string", description: "Destination tag / memo (for chains that require it, e.g. XRP, XLM)" },
105
+ customerId: { type: "string", description: "Existing MoonPay customer id (use create_customer first)" },
106
+ externalCustomerId: { type: "string", description: "Your internal user id, propagated to MoonPay for reconciliation" },
107
+ externalTransactionId: { type: "string", description: "Your internal transaction reference" },
108
+ paymentMethod: { type: "string", description: "Payment method (e.g. credit_debit_card, sepa_bank_transfer, pix)" },
109
+ returnUrl: { type: "string", description: "Browser redirect after hosted flow completes" },
110
+ extraFields: { type: "object", description: "Additional provider-specific fields passed through as-is" },
111
+ },
112
+ required: ["baseCurrencyAmount", "baseCurrencyCode", "currencyCode", "walletAddress"],
113
+ },
114
+ },
115
+ {
116
+ name: "get_buy_transaction",
117
+ description: "Retrieve a buy transaction (fiat -> crypto) by its MoonPay id. Returns current status and settlement detail.",
118
+ inputSchema: {
119
+ type: "object",
120
+ properties: {
121
+ id: { type: "string", description: "MoonPay transaction id" },
122
+ },
123
+ required: ["id"],
124
+ },
125
+ },
126
+ {
127
+ name: "list_buy_transactions",
128
+ description: "List buy transactions with optional filters. Used for reconciliation and agent-driven monitoring.",
129
+ inputSchema: {
130
+ type: "object",
131
+ properties: {
132
+ customerId: { type: "string", description: "Filter to a single MoonPay customer id" },
133
+ externalCustomerId: { type: "string", description: "Filter to your internal user id" },
134
+ status: { type: "string", description: "Filter by transaction status (e.g. pending, completed, failed)" },
135
+ limit: { type: "number", description: "Max results to return" },
136
+ },
137
+ },
138
+ },
139
+ {
140
+ name: "get_sell_quote",
141
+ description: "Preview a crypto -> fiat sell quote in real time. Use this before create_sell_transaction to show the end user the exact fiat amount, fees, and effective rate.",
142
+ inputSchema: {
143
+ type: "object",
144
+ properties: {
145
+ currencyCode: { type: "string", description: "Crypto currency code you want to sell (e.g. btc, usdc)" },
146
+ quoteCurrencyCode: { type: "string", description: "Fiat currency to receive (e.g. usd, eur, brl)" },
147
+ baseCurrencyAmount: { type: "number", description: "Crypto amount to sell (major units). Either this or quoteCurrencyAmount must be supplied." },
148
+ quoteCurrencyAmount: { type: "number", description: "Fiat amount to receive (major units). Either this or baseCurrencyAmount must be supplied." },
149
+ payoutMethod: { type: "string", description: "Optional payout method hint (e.g. sepa_bank_transfer, credit_debit_card, pix)" },
150
+ },
151
+ required: ["currencyCode", "quoteCurrencyCode"],
152
+ },
153
+ },
154
+ {
155
+ name: "create_sell_transaction",
156
+ description: "Create a sell transaction (crypto -> fiat). Used for agents that need to pay out in local fiat after receiving crypto.",
157
+ inputSchema: {
158
+ type: "object",
159
+ properties: {
160
+ baseCurrencyAmount: { type: "number", description: "Crypto amount to sell (major units)" },
161
+ baseCurrencyCode: { type: "string", description: "Crypto currency code (e.g. btc, usdc)" },
162
+ quoteCurrencyCode: { type: "string", description: "Fiat currency to receive (e.g. usd, brl)" },
163
+ customerId: { type: "string", description: "MoonPay customer id receiving the fiat payout" },
164
+ externalCustomerId: { type: "string", description: "Your internal user id" },
165
+ externalTransactionId: { type: "string", description: "Your internal transaction reference" },
166
+ payoutMethod: { type: "string", description: "Fiat payout method (e.g. sepa_bank_transfer, pix)" },
167
+ bankAccount: { type: "object", description: "Destination bank account detail (country-specific fields)" },
168
+ returnUrl: { type: "string", description: "Browser redirect after hosted flow completes" },
169
+ extraFields: { type: "object", description: "Additional provider-specific fields passed through as-is" },
170
+ },
171
+ required: ["baseCurrencyAmount", "baseCurrencyCode", "quoteCurrencyCode"],
172
+ },
173
+ },
174
+ {
175
+ name: "get_sell_transaction",
176
+ description: "Retrieve a sell transaction (crypto -> fiat) by its MoonPay id.",
177
+ inputSchema: {
178
+ type: "object",
179
+ properties: {
180
+ id: { type: "string", description: "MoonPay sell transaction id" },
181
+ },
182
+ required: ["id"],
183
+ },
184
+ },
185
+ {
186
+ name: "create_customer",
187
+ description: "Create a MoonPay customer (KYC'd end user). Required before creating transactions that must be tied to an identified individual.",
188
+ inputSchema: {
189
+ type: "object",
190
+ properties: {
191
+ email: { type: "string", description: "Customer email (used for MoonPay communications + KYC)" },
192
+ firstName: { type: "string", description: "Legal first name" },
193
+ lastName: { type: "string", description: "Legal last name" },
194
+ dateOfBirth: { type: "string", description: "ISO date (YYYY-MM-DD)" },
195
+ externalCustomerId: { type: "string", description: "Your internal user id for correlation" },
196
+ address: {
197
+ type: "object",
198
+ description: "Residential address object",
199
+ properties: {
200
+ country: { type: "string", description: "ISO-3166 alpha-2 country code (e.g. BR, US, MX)" },
201
+ state: { type: "string", description: "State / region code" },
202
+ town: { type: "string", description: "City" },
203
+ postCode: { type: "string", description: "Postal / ZIP code" },
204
+ street: { type: "string", description: "Street address" },
205
+ subStreet: { type: "string", description: "Unit / apt / complement" },
206
+ },
207
+ },
208
+ },
209
+ required: ["email"],
210
+ },
211
+ },
212
+ {
213
+ name: "get_customer",
214
+ description: "Retrieve a MoonPay customer by id.",
215
+ inputSchema: {
216
+ type: "object",
217
+ properties: {
218
+ id: { type: "string", description: "MoonPay customer id" },
219
+ },
220
+ required: ["id"],
221
+ },
222
+ },
223
+ {
224
+ name: "list_currencies",
225
+ 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.",
226
+ inputSchema: {
227
+ type: "object",
228
+ properties: {
229
+ show: { type: "string", enum: ["enabled", "all"], description: "Filter to enabled currencies only, or return everything. Defaults to enabled on the API side." },
230
+ },
231
+ },
232
+ },
233
+ ],
234
+ }));
235
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
236
+ const { name, arguments: args } = request.params;
237
+ const a = (args ?? {});
238
+ try {
239
+ switch (name) {
240
+ case "get_buy_quote": {
241
+ const code = encodeURIComponent(String(a.currencyCode ?? ""));
242
+ const query = qs({
243
+ baseCurrencyCode: a.baseCurrencyCode,
244
+ baseCurrencyAmount: a.baseCurrencyAmount,
245
+ quoteCurrencyAmount: a.quoteCurrencyAmount,
246
+ paymentMethod: a.paymentMethod,
247
+ areFeesIncluded: a.areFeesIncluded,
248
+ });
249
+ return { content: [{ type: "text", text: JSON.stringify(await moonpayRequest("GET", `/v3/currencies/${code}/buy_quote${query}`), null, 2) }] };
250
+ }
251
+ case "create_buy_transaction":
252
+ return { content: [{ type: "text", text: JSON.stringify(await moonpayRequest("POST", "/v3/transactions", a), null, 2) }] };
253
+ case "get_buy_transaction":
254
+ return { content: [{ type: "text", text: JSON.stringify(await moonpayRequest("GET", `/v1/transactions/${encodeURIComponent(String(a.id ?? ""))}`), null, 2) }] };
255
+ case "list_buy_transactions": {
256
+ const query = qs({
257
+ customerId: a.customerId,
258
+ externalCustomerId: a.externalCustomerId,
259
+ status: a.status,
260
+ limit: a.limit,
261
+ });
262
+ return { content: [{ type: "text", text: JSON.stringify(await moonpayRequest("GET", `/v1/transactions${query}`), null, 2) }] };
263
+ }
264
+ case "get_sell_quote": {
265
+ const code = encodeURIComponent(String(a.currencyCode ?? ""));
266
+ const query = qs({
267
+ quoteCurrencyCode: a.quoteCurrencyCode,
268
+ baseCurrencyAmount: a.baseCurrencyAmount,
269
+ quoteCurrencyAmount: a.quoteCurrencyAmount,
270
+ payoutMethod: a.payoutMethod,
271
+ });
272
+ return { content: [{ type: "text", text: JSON.stringify(await moonpayRequest("GET", `/v3/currencies/${code}/sell_quote${query}`), null, 2) }] };
273
+ }
274
+ case "create_sell_transaction":
275
+ return { content: [{ type: "text", text: JSON.stringify(await moonpayRequest("POST", "/v3/sell_transactions", a), null, 2) }] };
276
+ case "get_sell_transaction":
277
+ return { content: [{ type: "text", text: JSON.stringify(await moonpayRequest("GET", `/v3/sell_transactions/${encodeURIComponent(String(a.id ?? ""))}`), null, 2) }] };
278
+ case "create_customer":
279
+ return { content: [{ type: "text", text: JSON.stringify(await moonpayRequest("POST", "/v1/customers", a), null, 2) }] };
280
+ case "get_customer":
281
+ return { content: [{ type: "text", text: JSON.stringify(await moonpayRequest("GET", `/v1/customers/${encodeURIComponent(String(a.id ?? ""))}`), null, 2) }] };
282
+ case "list_currencies": {
283
+ const query = qs({ show: a.show });
284
+ return { content: [{ type: "text", text: JSON.stringify(await moonpayRequest("GET", `/v3/currencies${query}`), null, 2) }] };
285
+ }
286
+ default:
287
+ return { content: [{ type: "text", text: `Unknown tool: ${name}` }], isError: true };
288
+ }
289
+ }
290
+ catch (err) {
291
+ return { content: [{ type: "text", text: `Error: ${err instanceof Error ? err.message : String(err)}` }], isError: true };
292
+ }
293
+ });
294
+ async function main() {
295
+ if (process.argv.includes("--http") || process.env.MCP_HTTP === "true") {
296
+ const { default: express } = await import("express");
297
+ const { randomUUID } = await import("node:crypto");
298
+ const app = express();
299
+ app.use(express.json());
300
+ const transports = new Map();
301
+ app.get("/health", (_req, res) => res.json({ status: "ok", sessions: transports.size }));
302
+ app.post("/mcp", async (req, res) => {
303
+ const sid = req.headers["mcp-session-id"];
304
+ if (sid && transports.has(sid)) {
305
+ await transports.get(sid).handleRequest(req, res, req.body);
306
+ return;
307
+ }
308
+ if (!sid && isInitializeRequest(req.body)) {
309
+ const t = new StreamableHTTPServerTransport({ sessionIdGenerator: () => randomUUID(), onsessioninitialized: (id) => { transports.set(id, t); } });
310
+ t.onclose = () => { if (t.sessionId)
311
+ transports.delete(t.sessionId); };
312
+ const s = new Server({ name: "mcp-moonpay", version: "0.1.0" }, { capabilities: { tools: {} } });
313
+ server._requestHandlers.forEach((v, k) => s._requestHandlers.set(k, v));
314
+ server._notificationHandlers?.forEach((v, k) => s._notificationHandlers.set(k, v));
315
+ await s.connect(t);
316
+ await t.handleRequest(req, res, req.body);
317
+ return;
318
+ }
319
+ res.status(400).json({ jsonrpc: "2.0", error: { code: -32000, message: "Bad Request" }, id: null });
320
+ });
321
+ app.get("/mcp", async (req, res) => { const sid = req.headers["mcp-session-id"]; if (sid && transports.has(sid))
322
+ await transports.get(sid).handleRequest(req, res);
323
+ else
324
+ res.status(400).send("Invalid session"); });
325
+ app.delete("/mcp", async (req, res) => { const sid = req.headers["mcp-session-id"]; if (sid && transports.has(sid))
326
+ await transports.get(sid).handleRequest(req, res);
327
+ else
328
+ res.status(400).send("Invalid session"); });
329
+ const port = Number(process.env.MCP_PORT) || 3000;
330
+ app.listen(port, () => { console.error(`MCP HTTP server on http://localhost:${port}/mcp`); });
331
+ }
332
+ else {
333
+ const transport = new StdioServerTransport();
334
+ await server.connect(transport);
335
+ }
336
+ }
337
+ main().catch(console.error);
package/package.json ADDED
@@ -0,0 +1,35 @@
1
+ {
2
+ "name": "@codespar/mcp-moonpay",
3
+ "version": "0.1.0",
4
+ "description": "MCP server for MoonPay — fiat-to-crypto on/off-ramp covering 100+ crypto assets, multi-geography, Pix supported for Brazil",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "bin": {
8
+ "mcp-moonpay": "./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
+ "moonpay",
25
+ "crypto",
26
+ "onramp",
27
+ "offramp",
28
+ "fiat-to-crypto",
29
+ "pix",
30
+ "brazil",
31
+ "latam",
32
+ "stablecoin"
33
+ ],
34
+ "mcpName": "io.github.codespar/mcp-moonpay"
35
+ }
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-moonpay",
4
+ "description": "MCP server for MoonPay — fiat-to-crypto on/off-ramp across 100+ crypto assets and multiple geographies. Pix supported for Brazil onramp.",
5
+ "repository": {
6
+ "url": "https://github.com/codespar/mcp-dev-brasil",
7
+ "source": "github",
8
+ "subfolder": "packages/crypto/moonpay"
9
+ },
10
+ "version": "0.1.0",
11
+ "packages": [
12
+ {
13
+ "registryType": "npm",
14
+ "identifier": "@codespar/mcp-moonpay",
15
+ "version": "0.1.0",
16
+ "transport": {
17
+ "type": "stdio"
18
+ },
19
+ "environmentVariables": [
20
+ {
21
+ "name": "MOONPAY_API_KEY",
22
+ "description": "MoonPay API key (sandbox or production — the key selects the environment)",
23
+ "isRequired": true,
24
+ "format": "string",
25
+ "isSecret": true
26
+ },
27
+ {
28
+ "name": "MOONPAY_BASE_URL",
29
+ "description": "MoonPay API base URL. Defaults to https://api.moonpay.com.",
30
+ "isRequired": false,
31
+ "format": "string",
32
+ "isSecret": false
33
+ }
34
+ ]
35
+ }
36
+ ]
37
+ }
package/src/index.ts ADDED
@@ -0,0 +1,337 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * MCP Server for MoonPay — fiat-to-crypto on/off-ramp.
5
+ *
6
+ * MoonPay spans 100+ crypto assets and many geographies, with both buy
7
+ * (fiat -> crypto) and sell (crypto -> fiat) flows. For LatAm, Pix is
8
+ * supported as a BR onramp rail. Complementary to UnblockPay (BRL/MXN
9
+ * <-> USDC) in the catalog: MoonPay is the broader-coverage, longer-tail
10
+ * option for agents paying out in crypto or end users buying crypto with
11
+ * local currency.
12
+ *
13
+ * Tools (10):
14
+ * get_buy_quote — preview a fiat -> crypto exchange before committing
15
+ * create_buy_transaction — create a buy transaction (fiat -> crypto)
16
+ * get_buy_transaction — retrieve a buy transaction by id
17
+ * list_buy_transactions — list buy transactions with filters
18
+ * get_sell_quote — preview a crypto -> fiat exchange
19
+ * create_sell_transaction — create a sell transaction (crypto -> fiat)
20
+ * get_sell_transaction — retrieve a sell transaction by id
21
+ * create_customer — create a KYC'd end user
22
+ * get_customer — retrieve a customer by id
23
+ * list_currencies — list supported fiat + crypto assets (dynamic discovery)
24
+ *
25
+ * Authentication
26
+ * Every request carries:
27
+ * Authorization: Api-Key <API_KEY>
28
+ * Sandbox vs production is selected by which key you pass; the base URL is the same.
29
+ *
30
+ * Environment
31
+ * MOONPAY_API_KEY — API key (required, secret)
32
+ * MOONPAY_BASE_URL — optional; defaults to https://api.moonpay.com
33
+ *
34
+ * Docs: https://dev.moonpay.com
35
+ */
36
+
37
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
38
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
39
+ import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
40
+ import { isInitializeRequest } from "@modelcontextprotocol/sdk/types.js";
41
+ import {
42
+ CallToolRequestSchema,
43
+ ListToolsRequestSchema,
44
+ } from "@modelcontextprotocol/sdk/types.js";
45
+
46
+ const API_KEY = process.env.MOONPAY_API_KEY || "";
47
+ const BASE_URL = process.env.MOONPAY_BASE_URL || "https://api.moonpay.com";
48
+
49
+ async function moonpayRequest(method: string, path: string, body?: unknown): Promise<unknown> {
50
+ const res = await fetch(`${BASE_URL}${path}`, {
51
+ method,
52
+ headers: {
53
+ "Content-Type": "application/json",
54
+ "Authorization": `Api-Key ${API_KEY}`,
55
+ },
56
+ body: body ? JSON.stringify(body) : undefined,
57
+ });
58
+ if (!res.ok) {
59
+ const err = await res.text();
60
+ throw new Error(`MoonPay API ${res.status}: ${err}`);
61
+ }
62
+ // Some endpoints (e.g. 204 No Content) may not return JSON.
63
+ const text = await res.text();
64
+ if (!text) return {};
65
+ try {
66
+ return JSON.parse(text);
67
+ } catch {
68
+ return { raw: text };
69
+ }
70
+ }
71
+
72
+ function qs(params: Record<string, unknown>): string {
73
+ const entries = Object.entries(params).filter(([, v]) => v !== undefined && v !== null && v !== "");
74
+ if (entries.length === 0) return "";
75
+ const search = new URLSearchParams();
76
+ for (const [k, v] of entries) search.set(k, String(v));
77
+ return `?${search.toString()}`;
78
+ }
79
+
80
+ const server = new Server(
81
+ { name: "mcp-moonpay", version: "0.1.0" },
82
+ { capabilities: { tools: {} } }
83
+ );
84
+
85
+ server.setRequestHandler(ListToolsRequestSchema, async () => ({
86
+ tools: [
87
+ {
88
+ name: "get_buy_quote",
89
+ description: "Preview a fiat -> crypto buy quote in real time. Use this before create_buy_transaction to show the end user the exact crypto amount, fees, and effective rate.",
90
+ inputSchema: {
91
+ type: "object",
92
+ properties: {
93
+ currencyCode: { type: "string", description: "Crypto currency code you want to buy (e.g. btc, eth, usdc, sol). Must be a code returned by list_currencies." },
94
+ baseCurrencyCode: { type: "string", description: "Fiat currency you are paying with (e.g. usd, eur, brl, mxn)" },
95
+ baseCurrencyAmount: { type: "number", description: "Amount in fiat to spend (major units). Either this or quoteCurrencyAmount must be supplied." },
96
+ quoteCurrencyAmount: { type: "number", description: "Amount of crypto to receive (major units). Either this or baseCurrencyAmount must be supplied." },
97
+ paymentMethod: { type: "string", description: "Optional payment method hint (e.g. credit_debit_card, sepa_bank_transfer, pix)" },
98
+ areFeesIncluded: { type: "boolean", description: "If true, baseCurrencyAmount includes MoonPay fees." },
99
+ },
100
+ required: ["currencyCode", "baseCurrencyCode"],
101
+ },
102
+ },
103
+ {
104
+ name: "create_buy_transaction",
105
+ description: "Create a buy transaction (fiat -> crypto). The returned object contains status plus — depending on method — redirect URL for hosted checkout, Pix QR data, or card auth next steps.",
106
+ inputSchema: {
107
+ type: "object",
108
+ properties: {
109
+ baseCurrencyAmount: { type: "number", description: "Fiat amount to charge (major units)" },
110
+ baseCurrencyCode: { type: "string", description: "Fiat currency code (e.g. brl, usd)" },
111
+ currencyCode: { type: "string", description: "Crypto currency code to receive (e.g. btc, usdc)" },
112
+ walletAddress: { type: "string", description: "Destination crypto wallet address" },
113
+ walletAddressTag: { type: "string", description: "Destination tag / memo (for chains that require it, e.g. XRP, XLM)" },
114
+ customerId: { type: "string", description: "Existing MoonPay customer id (use create_customer first)" },
115
+ externalCustomerId: { type: "string", description: "Your internal user id, propagated to MoonPay for reconciliation" },
116
+ externalTransactionId: { type: "string", description: "Your internal transaction reference" },
117
+ paymentMethod: { type: "string", description: "Payment method (e.g. credit_debit_card, sepa_bank_transfer, pix)" },
118
+ returnUrl: { type: "string", description: "Browser redirect after hosted flow completes" },
119
+ extraFields: { type: "object", description: "Additional provider-specific fields passed through as-is" },
120
+ },
121
+ required: ["baseCurrencyAmount", "baseCurrencyCode", "currencyCode", "walletAddress"],
122
+ },
123
+ },
124
+ {
125
+ name: "get_buy_transaction",
126
+ description: "Retrieve a buy transaction (fiat -> crypto) by its MoonPay id. Returns current status and settlement detail.",
127
+ inputSchema: {
128
+ type: "object",
129
+ properties: {
130
+ id: { type: "string", description: "MoonPay transaction id" },
131
+ },
132
+ required: ["id"],
133
+ },
134
+ },
135
+ {
136
+ name: "list_buy_transactions",
137
+ description: "List buy transactions with optional filters. Used for reconciliation and agent-driven monitoring.",
138
+ inputSchema: {
139
+ type: "object",
140
+ properties: {
141
+ customerId: { type: "string", description: "Filter to a single MoonPay customer id" },
142
+ externalCustomerId: { type: "string", description: "Filter to your internal user id" },
143
+ status: { type: "string", description: "Filter by transaction status (e.g. pending, completed, failed)" },
144
+ limit: { type: "number", description: "Max results to return" },
145
+ },
146
+ },
147
+ },
148
+ {
149
+ name: "get_sell_quote",
150
+ description: "Preview a crypto -> fiat sell quote in real time. Use this before create_sell_transaction to show the end user the exact fiat amount, fees, and effective rate.",
151
+ inputSchema: {
152
+ type: "object",
153
+ properties: {
154
+ currencyCode: { type: "string", description: "Crypto currency code you want to sell (e.g. btc, usdc)" },
155
+ quoteCurrencyCode: { type: "string", description: "Fiat currency to receive (e.g. usd, eur, brl)" },
156
+ baseCurrencyAmount: { type: "number", description: "Crypto amount to sell (major units). Either this or quoteCurrencyAmount must be supplied." },
157
+ quoteCurrencyAmount: { type: "number", description: "Fiat amount to receive (major units). Either this or baseCurrencyAmount must be supplied." },
158
+ payoutMethod: { type: "string", description: "Optional payout method hint (e.g. sepa_bank_transfer, credit_debit_card, pix)" },
159
+ },
160
+ required: ["currencyCode", "quoteCurrencyCode"],
161
+ },
162
+ },
163
+ {
164
+ name: "create_sell_transaction",
165
+ description: "Create a sell transaction (crypto -> fiat). Used for agents that need to pay out in local fiat after receiving crypto.",
166
+ inputSchema: {
167
+ type: "object",
168
+ properties: {
169
+ baseCurrencyAmount: { type: "number", description: "Crypto amount to sell (major units)" },
170
+ baseCurrencyCode: { type: "string", description: "Crypto currency code (e.g. btc, usdc)" },
171
+ quoteCurrencyCode: { type: "string", description: "Fiat currency to receive (e.g. usd, brl)" },
172
+ customerId: { type: "string", description: "MoonPay customer id receiving the fiat payout" },
173
+ externalCustomerId: { type: "string", description: "Your internal user id" },
174
+ externalTransactionId: { type: "string", description: "Your internal transaction reference" },
175
+ payoutMethod: { type: "string", description: "Fiat payout method (e.g. sepa_bank_transfer, pix)" },
176
+ bankAccount: { type: "object", description: "Destination bank account detail (country-specific fields)" },
177
+ returnUrl: { type: "string", description: "Browser redirect after hosted flow completes" },
178
+ extraFields: { type: "object", description: "Additional provider-specific fields passed through as-is" },
179
+ },
180
+ required: ["baseCurrencyAmount", "baseCurrencyCode", "quoteCurrencyCode"],
181
+ },
182
+ },
183
+ {
184
+ name: "get_sell_transaction",
185
+ description: "Retrieve a sell transaction (crypto -> fiat) by its MoonPay id.",
186
+ inputSchema: {
187
+ type: "object",
188
+ properties: {
189
+ id: { type: "string", description: "MoonPay sell transaction id" },
190
+ },
191
+ required: ["id"],
192
+ },
193
+ },
194
+ {
195
+ name: "create_customer",
196
+ description: "Create a MoonPay customer (KYC'd end user). Required before creating transactions that must be tied to an identified individual.",
197
+ inputSchema: {
198
+ type: "object",
199
+ properties: {
200
+ email: { type: "string", description: "Customer email (used for MoonPay communications + KYC)" },
201
+ firstName: { type: "string", description: "Legal first name" },
202
+ lastName: { type: "string", description: "Legal last name" },
203
+ dateOfBirth: { type: "string", description: "ISO date (YYYY-MM-DD)" },
204
+ externalCustomerId: { type: "string", description: "Your internal user id for correlation" },
205
+ address: {
206
+ type: "object",
207
+ description: "Residential address object",
208
+ properties: {
209
+ country: { type: "string", description: "ISO-3166 alpha-2 country code (e.g. BR, US, MX)" },
210
+ state: { type: "string", description: "State / region code" },
211
+ town: { type: "string", description: "City" },
212
+ postCode: { type: "string", description: "Postal / ZIP code" },
213
+ street: { type: "string", description: "Street address" },
214
+ subStreet: { type: "string", description: "Unit / apt / complement" },
215
+ },
216
+ },
217
+ },
218
+ required: ["email"],
219
+ },
220
+ },
221
+ {
222
+ name: "get_customer",
223
+ description: "Retrieve a MoonPay customer by id.",
224
+ inputSchema: {
225
+ type: "object",
226
+ properties: {
227
+ id: { type: "string", description: "MoonPay customer id" },
228
+ },
229
+ required: ["id"],
230
+ },
231
+ },
232
+ {
233
+ name: "list_currencies",
234
+ 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.",
235
+ inputSchema: {
236
+ type: "object",
237
+ properties: {
238
+ show: { type: "string", enum: ["enabled", "all"], description: "Filter to enabled currencies only, or return everything. Defaults to enabled on the API side." },
239
+ },
240
+ },
241
+ },
242
+ ],
243
+ }));
244
+
245
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
246
+ const { name, arguments: args } = request.params;
247
+ const a = (args ?? {}) as Record<string, unknown>;
248
+
249
+ try {
250
+ switch (name) {
251
+ case "get_buy_quote": {
252
+ const code = encodeURIComponent(String(a.currencyCode ?? ""));
253
+ const query = qs({
254
+ baseCurrencyCode: a.baseCurrencyCode,
255
+ baseCurrencyAmount: a.baseCurrencyAmount,
256
+ quoteCurrencyAmount: a.quoteCurrencyAmount,
257
+ paymentMethod: a.paymentMethod,
258
+ areFeesIncluded: a.areFeesIncluded,
259
+ });
260
+ return { content: [{ type: "text", text: JSON.stringify(await moonpayRequest("GET", `/v3/currencies/${code}/buy_quote${query}`), null, 2) }] };
261
+ }
262
+ case "create_buy_transaction":
263
+ return { content: [{ type: "text", text: JSON.stringify(await moonpayRequest("POST", "/v3/transactions", a), null, 2) }] };
264
+ case "get_buy_transaction":
265
+ return { content: [{ type: "text", text: JSON.stringify(await moonpayRequest("GET", `/v1/transactions/${encodeURIComponent(String(a.id ?? ""))}`), null, 2) }] };
266
+ case "list_buy_transactions": {
267
+ const query = qs({
268
+ customerId: a.customerId,
269
+ externalCustomerId: a.externalCustomerId,
270
+ status: a.status,
271
+ limit: a.limit,
272
+ });
273
+ return { content: [{ type: "text", text: JSON.stringify(await moonpayRequest("GET", `/v1/transactions${query}`), null, 2) }] };
274
+ }
275
+ case "get_sell_quote": {
276
+ const code = encodeURIComponent(String(a.currencyCode ?? ""));
277
+ const query = qs({
278
+ quoteCurrencyCode: a.quoteCurrencyCode,
279
+ baseCurrencyAmount: a.baseCurrencyAmount,
280
+ quoteCurrencyAmount: a.quoteCurrencyAmount,
281
+ payoutMethod: a.payoutMethod,
282
+ });
283
+ return { content: [{ type: "text", text: JSON.stringify(await moonpayRequest("GET", `/v3/currencies/${code}/sell_quote${query}`), null, 2) }] };
284
+ }
285
+ case "create_sell_transaction":
286
+ return { content: [{ type: "text", text: JSON.stringify(await moonpayRequest("POST", "/v3/sell_transactions", a), null, 2) }] };
287
+ case "get_sell_transaction":
288
+ return { content: [{ type: "text", text: JSON.stringify(await moonpayRequest("GET", `/v3/sell_transactions/${encodeURIComponent(String(a.id ?? ""))}`), null, 2) }] };
289
+ case "create_customer":
290
+ return { content: [{ type: "text", text: JSON.stringify(await moonpayRequest("POST", "/v1/customers", a), null, 2) }] };
291
+ case "get_customer":
292
+ return { content: [{ type: "text", text: JSON.stringify(await moonpayRequest("GET", `/v1/customers/${encodeURIComponent(String(a.id ?? ""))}`), null, 2) }] };
293
+ case "list_currencies": {
294
+ const query = qs({ show: a.show });
295
+ return { content: [{ type: "text", text: JSON.stringify(await moonpayRequest("GET", `/v3/currencies${query}`), null, 2) }] };
296
+ }
297
+ default:
298
+ return { content: [{ type: "text", text: `Unknown tool: ${name}` }], isError: true };
299
+ }
300
+ } catch (err) {
301
+ return { content: [{ type: "text", text: `Error: ${err instanceof Error ? err.message : String(err)}` }], isError: true };
302
+ }
303
+ });
304
+
305
+ async function main() {
306
+ if (process.argv.includes("--http") || process.env.MCP_HTTP === "true") {
307
+ const { default: express } = await import("express");
308
+ const { randomUUID } = await import("node:crypto");
309
+ const app = express();
310
+ app.use(express.json());
311
+ const transports = new Map<string, StreamableHTTPServerTransport>();
312
+ app.get("/health", (_req: unknown, res: { json: (body: unknown) => unknown }) => res.json({ status: "ok", sessions: transports.size }));
313
+ app.post("/mcp", async (req: { headers: Record<string, string | string[] | undefined>; body: unknown }, res: { status: (code: number) => { json: (body: unknown) => unknown } }) => {
314
+ const sid = req.headers["mcp-session-id"] as string | undefined;
315
+ if (sid && transports.has(sid)) { await transports.get(sid)!.handleRequest(req as never, res as never, req.body); return; }
316
+ if (!sid && isInitializeRequest(req.body)) {
317
+ const t = new StreamableHTTPServerTransport({ sessionIdGenerator: () => randomUUID(), onsessioninitialized: (id) => { transports.set(id, t); } });
318
+ t.onclose = () => { if (t.sessionId) transports.delete(t.sessionId); };
319
+ const s = new Server({ name: "mcp-moonpay", version: "0.1.0" }, { capabilities: { tools: {} } });
320
+ (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
+ (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
+ await s.connect(t);
323
+ await t.handleRequest(req as never, res as never, req.body); return;
324
+ }
325
+ res.status(400).json({ jsonrpc: "2.0", error: { code: -32000, message: "Bad Request" }, id: null });
326
+ });
327
+ 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"); });
328
+ 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"); });
329
+ const port = Number(process.env.MCP_PORT) || 3000;
330
+ app.listen(port, () => { console.error(`MCP HTTP server on http://localhost:${port}/mcp`); });
331
+ } else {
332
+ const transport = new StdioServerTransport();
333
+ await server.connect(transport);
334
+ }
335
+ }
336
+
337
+ 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
+ }