reloadpi-mcp 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +102 -0
- package/bin/cli.js +2 -0
- package/index.js +362 -0
- package/package.json +59 -0
package/README.md
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
# reloadpi-mcp
|
|
2
|
+
|
|
3
|
+
MCP server for the [Reloadpi](https://reloadpi.com) digital goods catalog.
|
|
4
|
+
|
|
5
|
+
Exposes **13 tools** across eSIMs, mobile top-ups, and gift vouchers — all paid automatically
|
|
6
|
+
in USDC on Base via the [x402 protocol](https://x402.org). No account needed. Agents bring their own funded wallet.
|
|
7
|
+
|
|
8
|
+
**Live endpoint:** `https://mcp.reloadpi.com/mcp`
|
|
9
|
+
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
## Tools
|
|
13
|
+
|
|
14
|
+
| Category | Tools |
|
|
15
|
+
|----------|-------|
|
|
16
|
+
| Vouchers | `browse_voucher_offers` `get_voucher_offer` `get_voucher_filters` `purchase_voucher` |
|
|
17
|
+
| Top-ups | `browse_topup_offers` `get_topup_offer` `purchase_topup` |
|
|
18
|
+
| eSIMs | `browse_esim_offers` `get_esim_offer` `purchase_esim` |
|
|
19
|
+
| Orders | `get_order` `recover_order_by_txhash` `claim_refund` |
|
|
20
|
+
|
|
21
|
+
Browse and order-polling tools are free. Purchase tools trigger an x402 USDC payment automatically.
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
## Connecting
|
|
26
|
+
|
|
27
|
+
### Browse only (no purchases)
|
|
28
|
+
|
|
29
|
+
Just add the hosted URL — no setup needed:
|
|
30
|
+
|
|
31
|
+
```json
|
|
32
|
+
{
|
|
33
|
+
"mcpServers": {
|
|
34
|
+
"reloadpi": {
|
|
35
|
+
"url": "https://mcp.reloadpi.com/mcp"
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
### With purchases (recommended)
|
|
42
|
+
|
|
43
|
+
Run a local instance with your own wallet. **Claude or Cursor can set this up for you** — just paste this prompt:
|
|
44
|
+
|
|
45
|
+
> "Clone https://github.com/cross555/reloadpi-mcp, create a .env with my EVM_PRIVATE_KEY, run npm install && npm start, then add it to my MCP config pointing to http://localhost:3100/mcp"
|
|
46
|
+
|
|
47
|
+
Or manually:
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
git clone https://github.com/cross555/reloadpi-mcp
|
|
51
|
+
cd reloadpi-mcp
|
|
52
|
+
npm install
|
|
53
|
+
cp .env.example .env # add your EVM_PRIVATE_KEY
|
|
54
|
+
npm start
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Then update your MCP config:
|
|
58
|
+
|
|
59
|
+
```json
|
|
60
|
+
{
|
|
61
|
+
"mcpServers": {
|
|
62
|
+
"reloadpi": {
|
|
63
|
+
"url": "http://localhost:3100/mcp"
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
### Environment variables
|
|
70
|
+
|
|
71
|
+
```env
|
|
72
|
+
EVM_PRIVATE_KEY=0x... # funded Base wallet — agent pays USDC for purchases
|
|
73
|
+
RELOADPI_API_BASE=https://api.reloadpi.com/api/catalog # optional override
|
|
74
|
+
PORT=3100 # optional
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
You need USDC on Base mainnet. Get it at [Coinbase](https://coinbase.com) or bridge from another chain.
|
|
78
|
+
|
|
79
|
+
> **No account required.** Your wallet is your identity. Payments settle on-chain directly — Reloadpi never holds your funds.
|
|
80
|
+
|
|
81
|
+
---
|
|
82
|
+
|
|
83
|
+
## Payment model
|
|
84
|
+
|
|
85
|
+
- **Browse / polling tools** — free, no payment
|
|
86
|
+
- **Purchase tools** — x402 micropayment in USDC deducted automatically per call
|
|
87
|
+
- Refunds: eSIM orders refunded on `FULFILLMENT_FAILED`; vouchers and topups non-refundable once delivered
|
|
88
|
+
|
|
89
|
+
---
|
|
90
|
+
|
|
91
|
+
## Transport
|
|
92
|
+
|
|
93
|
+
Streamable HTTP (`POST /mcp`) — compatible with Claude Desktop 2025/2026 and any MCP client
|
|
94
|
+
supporting the streamable HTTP transport spec.
|
|
95
|
+
|
|
96
|
+
---
|
|
97
|
+
|
|
98
|
+
## Built with
|
|
99
|
+
|
|
100
|
+
- [`@modelcontextprotocol/sdk`](https://github.com/modelcontextprotocol/typescript-sdk)
|
|
101
|
+
- [`@x402/axios`](https://github.com/x402-org/x402) — x402 payment middleware
|
|
102
|
+
- [viem](https://viem.sh) — EVM wallet signing
|
package/bin/cli.js
ADDED
package/index.js
ADDED
|
@@ -0,0 +1,362 @@
|
|
|
1
|
+
// mcp.reloadpi.com — Reloadpi MCP Server
|
|
2
|
+
// Exposes Reloadpi catalog as MCP tools with automatic x402 payment.
|
|
3
|
+
// Agents bring their own EVM wallet via EVM_PRIVATE_KEY env var.
|
|
4
|
+
//
|
|
5
|
+
// Transport: Streamable HTTP — compatible with Claude Desktop 2025/2026
|
|
6
|
+
// Deploy as a separate service on Render (same account as api.reloadpi.com)
|
|
7
|
+
|
|
8
|
+
import "dotenv/config";
|
|
9
|
+
import axios from "axios";
|
|
10
|
+
import express from "express";
|
|
11
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
12
|
+
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
13
|
+
import { isInitializeRequest } from "@modelcontextprotocol/sdk/types.js";
|
|
14
|
+
import { wrapAxiosWithPaymentFromConfig } from "@x402/axios";
|
|
15
|
+
import { ExactEvmScheme } from "@x402/evm";
|
|
16
|
+
import { privateKeyToAccount } from "viem/accounts";
|
|
17
|
+
import { z } from "zod";
|
|
18
|
+
import { randomUUID } from "crypto";
|
|
19
|
+
|
|
20
|
+
// ── Config ────────────────────────────────────────────────────────────────────
|
|
21
|
+
|
|
22
|
+
const API_BASE = process.env.RELOADPI_API_BASE ?? "https://api.reloadpi.com/api/catalog";
|
|
23
|
+
const PORT = process.env.PORT ?? 3100;
|
|
24
|
+
process.on('unhandledRejection', (reason) => {
|
|
25
|
+
console.error('[unhandledRejection]', reason);
|
|
26
|
+
});
|
|
27
|
+
if (!process.env.EVM_PRIVATE_KEY) {
|
|
28
|
+
console.warn("⚠️ EVM_PRIVATE_KEY not set — browse tools work, purchase tools will fail.");
|
|
29
|
+
console.warn(" Set it in your MCP config env or in a .env file to enable purchases.");
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// ── x402 axios client ─────────────────────────────────────────────────────────
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
function buildHttpClient() {
|
|
36
|
+
if (!process.env.EVM_PRIVATE_KEY) {
|
|
37
|
+
throw new Error("EVM_PRIVATE_KEY is required for purchase tools. Add it to your MCP config env block.");
|
|
38
|
+
}
|
|
39
|
+
const account = privateKeyToAccount(process.env.EVM_PRIVATE_KEY);
|
|
40
|
+
return wrapAxiosWithPaymentFromConfig(axios.create({ baseURL: API_BASE }), {
|
|
41
|
+
schemes: [
|
|
42
|
+
{
|
|
43
|
+
network: "eip155:8453",
|
|
44
|
+
client: new ExactEvmScheme(account),
|
|
45
|
+
},
|
|
46
|
+
],
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// ── MCP server factory (one per session) ─────────────────────────────────────
|
|
51
|
+
|
|
52
|
+
function createMcpServer() {
|
|
53
|
+
const server = new McpServer({
|
|
54
|
+
name: "reloadpi",
|
|
55
|
+
version: "1.0.0",
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
59
|
+
// VOUCHER TOOLS
|
|
60
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
61
|
+
|
|
62
|
+
server.tool(
|
|
63
|
+
"browse_voucher_offers",
|
|
64
|
+
"Search gift cards and vouchers from 5000+ brands across 150+ countries (Amazon, Google Play, Netflix, Steam, Visa and more). Filter by brand, country or category. Call this before purchasing to discover available offer IDs and prices.",
|
|
65
|
+
{
|
|
66
|
+
brand: z.string().optional().describe("Brand name filter e.g. Amazon"),
|
|
67
|
+
country: z.string().optional().describe("ISO country code e.g. US"),
|
|
68
|
+
category: z.string().optional().describe("Category slug e.g. shopping, gaming, entertainment"),
|
|
69
|
+
limit: z.number().optional().default(10).describe("Results per page"),
|
|
70
|
+
offset: z.number().optional().default(0).describe("Pagination offset"),
|
|
71
|
+
},
|
|
72
|
+
async ({ brand, country, category, limit, offset }) => {
|
|
73
|
+
const api = buildHttpClient();
|
|
74
|
+
const res = await api.get("/vouchers/offers", {
|
|
75
|
+
params: { brand, country, category, limit, offset },
|
|
76
|
+
});
|
|
77
|
+
return { content: [{ type: "text", text: JSON.stringify(res.data) }] };
|
|
78
|
+
}
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
server.tool(
|
|
82
|
+
"get_voucher_offer",
|
|
83
|
+
"Get full details for a specific voucher or gift card offer by ID — including exact price, denomination, currency, priceType (FIXED or RANGE), and requiredFields for the recipient. Call this before purchasing to confirm the exact payment amount.",
|
|
84
|
+
{
|
|
85
|
+
offerId: z.string().describe("Voucher offer ID e.g. 1-800-BASKETS_US_002_EGIFT"),
|
|
86
|
+
},
|
|
87
|
+
async ({ offerId }) => {
|
|
88
|
+
const api = buildHttpClient();
|
|
89
|
+
const res = await api.get(`/vouchers/offers/${offerId}`);
|
|
90
|
+
return { content: [{ type: "text", text: JSON.stringify(res.data) }] };
|
|
91
|
+
}
|
|
92
|
+
);
|
|
93
|
+
|
|
94
|
+
server.tool(
|
|
95
|
+
"get_voucher_filters",
|
|
96
|
+
"Get all valid filter values for the voucher catalog — available brand names, ISO country codes, and category slugs. Use before calling browse_voucher_offers to know what filter values are accepted.",
|
|
97
|
+
{},
|
|
98
|
+
async () => {
|
|
99
|
+
const api = buildHttpClient();
|
|
100
|
+
const res = await api.get("/vouchers/filters");
|
|
101
|
+
return { content: [{ type: "text", text: JSON.stringify(res.data) }] };
|
|
102
|
+
}
|
|
103
|
+
);
|
|
104
|
+
|
|
105
|
+
server.tool(
|
|
106
|
+
"purchase_voucher",
|
|
107
|
+
"Purchase a gift card or voucher. The x402 payment (USDC on Base) is handled automatically from the agent's wallet. Provide offerId from browse_voucher_offers. For RANGE priceType (open-value cards like Amazon), also supply value in USD. Returns orderId, txHash, and delivery (pinCode / redeemUrl) when available.",
|
|
108
|
+
{
|
|
109
|
+
offerId: z.string().describe("Voucher offer ID from browse_voucher_offers"),
|
|
110
|
+
firstName: z.string().describe("Recipient first name"),
|
|
111
|
+
lastName: z.string().describe("Recipient last name"),
|
|
112
|
+
email: z.string().optional().describe("Recipient email — required for some brands, check requiredFields on the offer"),
|
|
113
|
+
country: z.string().optional().describe("Recipient ISO country code — required for some brands"),
|
|
114
|
+
value: z.number().optional().describe("USD amount — RANGE priceType only (open-value cards). Omit for FIXED."),
|
|
115
|
+
},
|
|
116
|
+
async ({ offerId, firstName, lastName, email, country, value }) => {
|
|
117
|
+
const api = buildHttpClient();
|
|
118
|
+
const body = {
|
|
119
|
+
offerId,
|
|
120
|
+
recipient: { firstName, lastName, ...(email && { email }), ...(country && { country }) },
|
|
121
|
+
...(value !== undefined && { value }),
|
|
122
|
+
};
|
|
123
|
+
try {
|
|
124
|
+
const res = await api.post("/vouchers/purchase", body);
|
|
125
|
+
return { content: [{ type: "text", text: JSON.stringify(res.data) }] };
|
|
126
|
+
} catch (err) {
|
|
127
|
+
console.error('[purchase_voucher error]', err?.message);
|
|
128
|
+
console.error('[purchase_voucher status]', err?.response?.status);
|
|
129
|
+
console.error('[purchase_voucher data]', JSON.stringify(err?.response?.data));
|
|
130
|
+
console.error('[purchase_voucher stack]', err?.stack);
|
|
131
|
+
throw err;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
);
|
|
135
|
+
|
|
136
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
137
|
+
// TOPUP TOOLS
|
|
138
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
139
|
+
|
|
140
|
+
server.tool(
|
|
141
|
+
"browse_topup_offers",
|
|
142
|
+
"Search prepaid mobile airtime and data top-up offers across 500+ operators in 150+ countries — including MTN, Airtel, Orange, Movistar, Digicel and more. Filter by country or operator. Call this before purchasing to discover available offer IDs and prices.",
|
|
143
|
+
{
|
|
144
|
+
country: z.string().optional().describe("ISO country code e.g. GH, ES, NG"),
|
|
145
|
+
operator: z.string().optional().describe("Operator name filter e.g. MTN, Airtel"),
|
|
146
|
+
limit: z.number().optional().default(10),
|
|
147
|
+
offset: z.number().optional().default(0),
|
|
148
|
+
},
|
|
149
|
+
async ({ country, operator, limit, offset }) => {
|
|
150
|
+
const api = buildHttpClient();
|
|
151
|
+
const res = await api.get("/topups/offers", {
|
|
152
|
+
params: { country, operator, limit },
|
|
153
|
+
});
|
|
154
|
+
return { content: [{ type: "text", text: JSON.stringify(res.data) }] };
|
|
155
|
+
}
|
|
156
|
+
);
|
|
157
|
+
|
|
158
|
+
server.tool(
|
|
159
|
+
"get_topup_offer",
|
|
160
|
+
"Get full details for a specific mobile top-up offer by ID — including price, operator, country, and required recipient fields.",
|
|
161
|
+
{
|
|
162
|
+
offerId: z.string().describe("Topup offer ID e.g. AIRTELTIGO_GH_025"),
|
|
163
|
+
},
|
|
164
|
+
async ({ offerId }) => {
|
|
165
|
+
const api = buildHttpClient();
|
|
166
|
+
const res = await api.get(`/topups/offers/${offerId}`);
|
|
167
|
+
return { content: [{ type: "text", text: JSON.stringify(res.data) }] };
|
|
168
|
+
}
|
|
169
|
+
);
|
|
170
|
+
|
|
171
|
+
server.tool(
|
|
172
|
+
"purchase_topup",
|
|
173
|
+
"Purchase a prepaid mobile airtime or data top-up. The x402 payment (USDC on Base) is handled automatically from the agent's wallet. Provide offerId from browse_topup_offers and the recipient phone number in E.164 format. Top-up is delivered directly to the recipient's SIM. Returns orderId and txHash — poll get_order for delivery confirmation.",
|
|
174
|
+
{
|
|
175
|
+
offerId: z.string().describe("Topup offer ID from browse_topup_offers"),
|
|
176
|
+
msisdn: z.string().describe("Recipient phone number in E.164 format e.g. +233201234567"),
|
|
177
|
+
},
|
|
178
|
+
async ({ offerId, msisdn }) => {
|
|
179
|
+
const api = buildHttpClient();
|
|
180
|
+
const res = await api.post("/topups/purchase", {
|
|
181
|
+
offerId,
|
|
182
|
+
recipient: { msisdn },
|
|
183
|
+
});
|
|
184
|
+
return { content: [{ type: "text", text: JSON.stringify(res.data) }] };
|
|
185
|
+
}
|
|
186
|
+
);
|
|
187
|
+
|
|
188
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
189
|
+
// ESIM TOOLS
|
|
190
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
191
|
+
|
|
192
|
+
server.tool(
|
|
193
|
+
"browse_esim_offers",
|
|
194
|
+
"Browse eSIM data plans across 190+ countries and regions — including unlimited data plans, regional multi-country plans, and global roaming. Filter by country, region, duration or data amount. Call this before purchasing to discover available offer IDs and prices.",
|
|
195
|
+
{
|
|
196
|
+
country: z.string().optional().describe("ISO country code e.g. ES, US, JP"),
|
|
197
|
+
region: z.string().optional().describe("Region slug e.g. europe, latam, global"),
|
|
198
|
+
duration: z.number().optional().describe("Plan duration in days e.g. 7, 30"),
|
|
199
|
+
limit: z.number().optional().default(10),
|
|
200
|
+
offset: z.number().optional().default(0),
|
|
201
|
+
},
|
|
202
|
+
async ({ country, region, duration, limit, offset }) => {
|
|
203
|
+
const api = buildHttpClient();
|
|
204
|
+
const res = await api.get("/esims/offers", {
|
|
205
|
+
params: { country, region, duration, limit, offset },
|
|
206
|
+
});
|
|
207
|
+
return { content: [{ type: "text", text: JSON.stringify(res.data) }] };
|
|
208
|
+
}
|
|
209
|
+
);
|
|
210
|
+
|
|
211
|
+
server.tool(
|
|
212
|
+
"get_esim_offer",
|
|
213
|
+
"Get full details for a specific eSIM plan by ID — including exact price, data allowance, duration, coverage countries, and whether data is unlimited.",
|
|
214
|
+
{
|
|
215
|
+
offerId: z.string().describe("eSIM offer ID e.g. ESIM-ES-7D-10GB-NOROAM"),
|
|
216
|
+
},
|
|
217
|
+
async ({ offerId }) => {
|
|
218
|
+
const api = buildHttpClient();
|
|
219
|
+
const res = await api.get(`/esims/offers/${offerId}`);
|
|
220
|
+
return { content: [{ type: "text", text: JSON.stringify(res.data) }] };
|
|
221
|
+
}
|
|
222
|
+
);
|
|
223
|
+
|
|
224
|
+
server.tool(
|
|
225
|
+
"purchase_esim",
|
|
226
|
+
"Purchase an eSIM data plan. The x402 payment (USDC on Base) is handled automatically from the agent's wallet. Provide offerId from browse_esim_offers. Returns orderId, txHash, ICCID and QR code (base64 PNG) when ready. If QR is not immediately available, poll get_order with the returned orderId.",
|
|
227
|
+
{
|
|
228
|
+
offerId: z.string().describe("eSIM offer ID from browse_esim_offers"),
|
|
229
|
+
iccid: z.string().optional().describe("Existing ICCID — only for top-up/recharge of an installed eSIM"),
|
|
230
|
+
},
|
|
231
|
+
async ({ offerId, iccid }) => {
|
|
232
|
+
const api = buildHttpClient();
|
|
233
|
+
const res = await api.post("/esims/purchase", {
|
|
234
|
+
offerId,
|
|
235
|
+
...(iccid && { iccid }),
|
|
236
|
+
});
|
|
237
|
+
return { content: [{ type: "text", text: JSON.stringify(res.data) }] };
|
|
238
|
+
}
|
|
239
|
+
);
|
|
240
|
+
|
|
241
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
242
|
+
// ORDER POLLING TOOLS (free — no payment)
|
|
243
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
244
|
+
|
|
245
|
+
server.tool(
|
|
246
|
+
"get_order",
|
|
247
|
+
"Poll the fulfillment state of any purchase. Returns status (PROCESSING / FULFILLED / FULFILLMENT_FAILED / EXPIRED), delivery payload (ICCID, QR code, pin code, redeem URL), refund eligibility, and whether to keep polling. Use the orderId returned from any purchase tool. Free — no payment required.",
|
|
248
|
+
{
|
|
249
|
+
orderId: z.string().describe("Order UUID returned from any purchase tool"),
|
|
250
|
+
},
|
|
251
|
+
async ({ orderId }) => {
|
|
252
|
+
const res = await axios.get(`${API_BASE}/orders/${orderId}`);
|
|
253
|
+
return { content: [{ type: "text", text: JSON.stringify(res.data) }] };
|
|
254
|
+
}
|
|
255
|
+
);
|
|
256
|
+
|
|
257
|
+
server.tool(
|
|
258
|
+
"recover_order_by_txhash",
|
|
259
|
+
"Recover a lost order using the on-chain transaction hash. Use this if the purchase response was lost (network drop etc.) but you have the txHash from the blockchain. Free — no payment required.",
|
|
260
|
+
{
|
|
261
|
+
txHash: z.string().describe("On-chain transaction hash from the purchase settlement e.g. 0xdeaed4..."),
|
|
262
|
+
},
|
|
263
|
+
async ({ txHash }) => {
|
|
264
|
+
const res = await axios.get(`${API_BASE}/orders`, { params: { txHash } });
|
|
265
|
+
return { content: [{ type: "text", text: JSON.stringify(res.data) }] };
|
|
266
|
+
}
|
|
267
|
+
);
|
|
268
|
+
|
|
269
|
+
server.tool(
|
|
270
|
+
"claim_refund",
|
|
271
|
+
"Claim a refund for a failed or expired eSIM order. Only eligible when status is FULFILLMENT_FAILED or EXPIRED and refund_policy is auto_on_fail. Requires the txHash from the original purchase response as proof of payment. Topup and voucher orders are non-refundable. Free — no payment required.",
|
|
272
|
+
{
|
|
273
|
+
orderId: z.string().describe("Order UUID from the purchase response"),
|
|
274
|
+
txHash: z.string().describe("On-chain txHash from the purchase response — proves you are the original payer"),
|
|
275
|
+
idempotencyKey: z.string().describe("Unique UUID for this refund request — prevents double submission"),
|
|
276
|
+
},
|
|
277
|
+
async ({ orderId, txHash, idempotencyKey }) => {
|
|
278
|
+
const res = await axios.post(
|
|
279
|
+
`${API_BASE}/orders/${orderId}/refund`,
|
|
280
|
+
{ txHash },
|
|
281
|
+
{ headers: { "Idempotency-Key": idempotencyKey } }
|
|
282
|
+
);
|
|
283
|
+
return { content: [{ type: "text", text: JSON.stringify(res.data) }] };
|
|
284
|
+
}
|
|
285
|
+
);
|
|
286
|
+
|
|
287
|
+
return server;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
291
|
+
// Streamable HTTP transport
|
|
292
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
293
|
+
|
|
294
|
+
const app = express();
|
|
295
|
+
app.use(express.json());
|
|
296
|
+
|
|
297
|
+
const sessions = new Map(); // sessionId → { server, transport }
|
|
298
|
+
|
|
299
|
+
app.post("/mcp", async (req, res) => {
|
|
300
|
+
try {
|
|
301
|
+
const sessionId = req.headers["mcp-session-id"];
|
|
302
|
+
|
|
303
|
+
// Existing session
|
|
304
|
+
if (sessionId && sessions.has(sessionId)) {
|
|
305
|
+
const { transport } = sessions.get(sessionId);
|
|
306
|
+
await transport.handleRequest(req, res, req.body);
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// New session — must be an initialize request
|
|
311
|
+
if (!isInitializeRequest(req.body)) {
|
|
312
|
+
return res.status(400).json({ error: "Expected initialize request for new session" });
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
const newSessionId = randomUUID();
|
|
316
|
+
const server = createMcpServer();
|
|
317
|
+
const transport = new StreamableHTTPServerTransport({
|
|
318
|
+
sessionIdGenerator: () => newSessionId,
|
|
319
|
+
onsessioninitialized: (id) => {
|
|
320
|
+
sessions.set(id, { server, transport });
|
|
321
|
+
},
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
// Clean up on close
|
|
325
|
+
transport.onclose = () => sessions.delete(newSessionId);
|
|
326
|
+
|
|
327
|
+
await server.connect(transport);
|
|
328
|
+
await transport.handleRequest(req, res, req.body);
|
|
329
|
+
} catch (err) {
|
|
330
|
+
console.error("MCP error:", err);
|
|
331
|
+
if (!res.headersSent) res.status(500).json({ error: "Internal server error" });
|
|
332
|
+
}
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
// SSE notifications (GET /mcp)
|
|
336
|
+
app.get("/mcp", async (req, res) => {
|
|
337
|
+
const sessionId = req.headers["mcp-session-id"];
|
|
338
|
+
if (!sessionId || !sessions.has(sessionId)) {
|
|
339
|
+
return res.status(400).json({ error: "Invalid or missing session ID" });
|
|
340
|
+
}
|
|
341
|
+
const { transport } = sessions.get(sessionId);
|
|
342
|
+
await transport.handleRequest(req, res);
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
// Session termination (DELETE /mcp)
|
|
346
|
+
app.delete("/mcp", async (req, res) => {
|
|
347
|
+
const sessionId = req.headers["mcp-session-id"];
|
|
348
|
+
if (!sessionId || !sessions.has(sessionId)) {
|
|
349
|
+
return res.status(404).json({ error: "Session not found" });
|
|
350
|
+
}
|
|
351
|
+
const { transport } = sessions.get(sessionId);
|
|
352
|
+
await transport.handleRequest(req, res);
|
|
353
|
+
sessions.delete(sessionId);
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
app.get("/health", (_req, res) => res.json({ status: "ok", name: "reloadpi-mcp" }));
|
|
357
|
+
|
|
358
|
+
app.listen(PORT, () => {
|
|
359
|
+
console.log(`🚀 Reloadpi MCP server listening on port ${PORT}`);
|
|
360
|
+
console.log(` MCP endpoint: http://localhost:${PORT}/mcp`);
|
|
361
|
+
console.log(` Production: https://mcp.reloadpi.com/mcp`);
|
|
362
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "reloadpi-mcp",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Reloadpi MCP server — browse and purchase eSIMs, vouchers, and topups via x402 (USDC on Base)",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "index.js",
|
|
7
|
+
"bin": {
|
|
8
|
+
"reloadpi-mcp": "./bin/cli.js"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"bin/",
|
|
12
|
+
"index.js",
|
|
13
|
+
"README.md"
|
|
14
|
+
],
|
|
15
|
+
"scripts": {
|
|
16
|
+
"start": "node index.js",
|
|
17
|
+
"dev": "node --watch index.js"
|
|
18
|
+
},
|
|
19
|
+
"engines": {
|
|
20
|
+
"node": ">=18.0.0"
|
|
21
|
+
},
|
|
22
|
+
"keywords": [
|
|
23
|
+
"mcp",
|
|
24
|
+
"esim",
|
|
25
|
+
"topup",
|
|
26
|
+
"voucher",
|
|
27
|
+
"gift-card",
|
|
28
|
+
"x402",
|
|
29
|
+
"usdc",
|
|
30
|
+
"base",
|
|
31
|
+
"reloadpi",
|
|
32
|
+
"agentic",
|
|
33
|
+
"claude"
|
|
34
|
+
],
|
|
35
|
+
"author": "Reloadpi",
|
|
36
|
+
"license": "MIT",
|
|
37
|
+
"repository": {
|
|
38
|
+
"type": "git",
|
|
39
|
+
"url": "https://github.com/cross555/reloadpi-mcp"
|
|
40
|
+
},
|
|
41
|
+
"homepage": "https://reloadpi.com",
|
|
42
|
+
"dependencies": {
|
|
43
|
+
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
44
|
+
"@x402/axios": "^2.13.0",
|
|
45
|
+
"@x402/core": "^2.13.0",
|
|
46
|
+
"@x402/evm": "^2.13.0",
|
|
47
|
+
"@x402/express": "2.12.0",
|
|
48
|
+
"@x402/extensions": "2.12.0",
|
|
49
|
+
"@x402/fetch": "2.12.0",
|
|
50
|
+
"axios": "1.16.1",
|
|
51
|
+
"dotenv": "^16.0.0",
|
|
52
|
+
"express": "^4.18.0",
|
|
53
|
+
"viem": "^2.39.0",
|
|
54
|
+
"zod": "^3.22.0"
|
|
55
|
+
},
|
|
56
|
+
"overrides": {
|
|
57
|
+
"axios": "1.16.1"
|
|
58
|
+
}
|
|
59
|
+
}
|