reloadpi-mcp 1.1.3 → 1.1.4
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 +19 -8
- package/index.js +240 -161
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -2,10 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
MCP server for the [Reloadpi](https://reloadpi.com) digital goods catalog.
|
|
4
4
|
|
|
5
|
-
Exposes
|
|
6
|
-
in USDC on Base via the [x402 protocol](https://x402.org)
|
|
5
|
+
Exposes tools across eSIMs, mobile top-ups, and gift vouchers. **Browsing and order-polling are
|
|
6
|
+
free.** Purchasing settles in USDC on Base via the [x402 protocol](https://x402.org) from **your
|
|
7
|
+
own wallet** — so purchase tools are only available when you self-host with a key (see below).
|
|
8
|
+
No account needed.
|
|
7
9
|
|
|
8
|
-
**Live endpoint:** `https://mcp.reloadpi.com/mcp`
|
|
10
|
+
**Live endpoint (browse-only):** `https://mcp.reloadpi.com/mcp`
|
|
9
11
|
|
|
10
12
|
---
|
|
11
13
|
|
|
@@ -18,7 +20,13 @@ in USDC on Base via the [x402 protocol](https://x402.org). No account needed. Ag
|
|
|
18
20
|
| eSIMs | `browse_esim_offers` `get_esim_offer` `purchase_esim` |
|
|
19
21
|
| Orders | `get_order` `recover_order_by_txhash` `claim_refund` |
|
|
20
22
|
|
|
21
|
-
Browse and order-polling tools are free
|
|
23
|
+
Browse, filter and order-polling tools are **free** and always available (they use the free `/ai`
|
|
24
|
+
API — no wallet). `get_*_offer` (paid detail) and `purchase_*` appear **only when you self-host
|
|
25
|
+
with `EVM_PRIVATE_KEY`** — they settle x402 payments from your own wallet.
|
|
26
|
+
|
|
27
|
+
> ⚠️ **Never set `EVM_PRIVATE_KEY` on a public deployment.** The server signs from whatever key it
|
|
28
|
+
> holds, for anyone who connects. Public hosting (like `mcp.reloadpi.com`) must run **without** a
|
|
29
|
+
> key — browse-only. Keys belong only on a machine you control.
|
|
22
30
|
|
|
23
31
|
---
|
|
24
32
|
|
|
@@ -47,7 +55,7 @@ No cloning needed. Add to your MCP config and set your wallet key:
|
|
|
47
55
|
"mcpServers": {
|
|
48
56
|
"reloadpi": {
|
|
49
57
|
"command": "npx",
|
|
50
|
-
"args": ["-y", "reloadpi-mcp"],
|
|
58
|
+
"args": ["-y", "reloadpi-mcp", "--stdio"],
|
|
51
59
|
"env": {
|
|
52
60
|
"EVM_PRIVATE_KEY": "0x..."
|
|
53
61
|
}
|
|
@@ -83,9 +91,12 @@ Then update your MCP config:
|
|
|
83
91
|
### Environment variables
|
|
84
92
|
|
|
85
93
|
```env
|
|
86
|
-
EVM_PRIVATE_KEY=0x... # funded Base wallet
|
|
87
|
-
|
|
88
|
-
|
|
94
|
+
EVM_PRIVATE_KEY=0x... # SELF-HOST ONLY — funded Base wallet; enables purchase tools.
|
|
95
|
+
# NEVER set on a public host. Unset = browse-only.
|
|
96
|
+
RELOADPI_API_BASE=https://api.reloadpi.com/api/catalog # optional — paid routes
|
|
97
|
+
RELOADPI_AI_BASE=https://api.reloadpi.com/ai # optional — free browse API
|
|
98
|
+
PORT=3100 # optional
|
|
99
|
+
MCP_AUTH_TOKEN= # optional — Bearer gate on /mcp
|
|
89
100
|
```
|
|
90
101
|
|
|
91
102
|
You need USDC on Base mainnet. Get it at [Coinbase](https://coinbase.com) or bridge from another chain.
|
package/index.js
CHANGED
|
@@ -1,9 +1,17 @@
|
|
|
1
1
|
// mcp.reloadpi.com — Reloadpi MCP Server
|
|
2
|
-
// Exposes Reloadpi catalog as MCP tools
|
|
3
|
-
// Agents bring their own EVM wallet via EVM_PRIVATE_KEY env var.
|
|
2
|
+
// Exposes Reloadpi catalog as MCP tools.
|
|
4
3
|
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
4
|
+
// Two modes, decided by whether EVM_PRIVATE_KEY is present:
|
|
5
|
+
//
|
|
6
|
+
// • Browse-only mode (NO EVM_PRIVATE_KEY) — safe for public hosting.
|
|
7
|
+
// Browse / filter / order tools only. They hit the FREE /ai API, so the
|
|
8
|
+
// server never holds or spends a wallet. This is what mcp.reloadpi.com runs.
|
|
9
|
+
//
|
|
10
|
+
// • Purchase mode (EVM_PRIVATE_KEY set) — for self-hosters (npx / local).
|
|
11
|
+
// Adds get_*_offer (paid detail) and purchase_* tools that settle x402
|
|
12
|
+
// payments from the operator's OWN wallet. Never set this on the public host.
|
|
13
|
+
//
|
|
14
|
+
// Transport: Streamable HTTP — compatible with Claude Desktop 2025/2026.
|
|
7
15
|
|
|
8
16
|
import "dotenv/config";
|
|
9
17
|
import axios from "axios";
|
|
@@ -19,21 +27,40 @@ import { randomUUID } from "crypto";
|
|
|
19
27
|
|
|
20
28
|
// ── Config ────────────────────────────────────────────────────────────────────
|
|
21
29
|
|
|
30
|
+
// Paid x402 routes (offer detail + purchase). Only used in purchase mode.
|
|
22
31
|
const API_BASE = process.env.RELOADPI_API_BASE ?? "https://api.reloadpi.com/api/catalog";
|
|
32
|
+
// Free, walletless browse API. Used for all browse/filter tools in both modes.
|
|
33
|
+
const AI_BASE = process.env.RELOADPI_AI_BASE ?? "https://api.reloadpi.com/ai";
|
|
23
34
|
const PORT = process.env.PORT ?? 3100;
|
|
24
|
-
|
|
25
|
-
|
|
35
|
+
|
|
36
|
+
// Optional shared-secret gate for /mcp. OFF by default so the public browse
|
|
37
|
+
// server stays open to anonymous MCP clients. Set MCP_AUTH_TOKEN to require
|
|
38
|
+
// `Authorization: Bearer <token>` (useful for a private self-hosted instance).
|
|
39
|
+
const MCP_AUTH_TOKEN = process.env.MCP_AUTH_TOKEN?.trim() || null;
|
|
40
|
+
|
|
41
|
+
const HAS_WALLET = Boolean(process.env.EVM_PRIVATE_KEY);
|
|
42
|
+
|
|
43
|
+
// Transport: default HTTP (for the hosted server). Pass --stdio (or MCP_STDIO=1)
|
|
44
|
+
// when an MCP client spawns this process locally via `command`/`args` — Claude
|
|
45
|
+
// Desktop and Cursor talk to spawned servers over stdio, not HTTP.
|
|
46
|
+
const STDIO = process.argv.includes("--stdio") || process.env.MCP_STDIO === "1";
|
|
47
|
+
|
|
48
|
+
process.on("unhandledRejection", (reason) => {
|
|
49
|
+
console.error("[unhandledRejection]", reason);
|
|
26
50
|
});
|
|
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
51
|
|
|
32
|
-
// ──
|
|
52
|
+
// ── HTTP clients ────────────────────────────────────────────────────────────
|
|
53
|
+
|
|
54
|
+
// Free browse client — no wallet, never pays.
|
|
55
|
+
const freeApi = axios.create({ baseURL: AI_BASE });
|
|
33
56
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
57
|
+
// Paid x402 client — built lazily, only when a wallet is configured.
|
|
58
|
+
function buildPaidClient() {
|
|
59
|
+
if (!HAS_WALLET) {
|
|
60
|
+
throw new Error(
|
|
61
|
+
"This tool requires a funded wallet. Self-host reloadpi-mcp with EVM_PRIVATE_KEY " +
|
|
62
|
+
"in your MCP config env to enable offer-detail and purchase tools."
|
|
63
|
+
);
|
|
37
64
|
}
|
|
38
65
|
const account = privateKeyToAccount(process.env.EVM_PRIVATE_KEY);
|
|
39
66
|
return wrapAxiosWithPaymentFromConfig(axios.create({ baseURL: API_BASE }), {
|
|
@@ -46,99 +73,55 @@ function buildHttpClient() {
|
|
|
46
73
|
});
|
|
47
74
|
}
|
|
48
75
|
|
|
76
|
+
const asText = (data) => ({ content: [{ type: "text", text: JSON.stringify(data) }] });
|
|
77
|
+
|
|
49
78
|
// ── MCP server factory (one per session) ─────────────────────────────────────
|
|
50
79
|
|
|
51
80
|
function createMcpServer() {
|
|
52
81
|
const server = new McpServer({
|
|
53
82
|
name: "reloadpi",
|
|
54
|
-
version: "1.
|
|
83
|
+
version: "1.1.4",
|
|
55
84
|
});
|
|
56
85
|
|
|
57
|
-
//
|
|
58
|
-
//
|
|
59
|
-
//
|
|
86
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
87
|
+
// FREE TOOLS — always available, no wallet. Backed by the /ai browse API.
|
|
88
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
89
|
+
|
|
90
|
+
// ── Vouchers (browse) ──────────────────────────────────────────────────────
|
|
60
91
|
|
|
61
92
|
server.tool(
|
|
62
93
|
"browse_voucher_offers",
|
|
63
|
-
"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.
|
|
94
|
+
"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. Free — no payment. Returns offer IDs and prices; use them with purchase_voucher (requires a self-hosted wallet).",
|
|
64
95
|
{
|
|
65
96
|
brand: z.string().optional().describe("Brand name filter e.g. Amazon"),
|
|
66
97
|
country: z.string().optional().describe("ISO country code e.g. US"),
|
|
67
98
|
category: z.string().optional().describe("Category slug e.g. shopping, gaming, entertainment"),
|
|
68
|
-
limit: z.number().optional().default(10).describe("Results per page"),
|
|
99
|
+
limit: z.number().optional().default(10).describe("Results per page (max 50)"),
|
|
69
100
|
offset: z.number().optional().default(0).describe("Pagination offset"),
|
|
70
101
|
},
|
|
71
102
|
async ({ brand, country, category, limit, offset }) => {
|
|
72
|
-
const
|
|
73
|
-
|
|
74
|
-
params: { brand, country, category, limit, offset },
|
|
103
|
+
const res = await freeApi.get("/vouchers", {
|
|
104
|
+
params: { brand, country, subType: category, limit, offset },
|
|
75
105
|
});
|
|
76
|
-
return
|
|
77
|
-
}
|
|
78
|
-
);
|
|
79
|
-
|
|
80
|
-
server.tool(
|
|
81
|
-
"get_voucher_offer",
|
|
82
|
-
"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.",
|
|
83
|
-
{
|
|
84
|
-
offerId: z.string().describe("Voucher offer ID e.g. 1-800-BASKETS_US_002_EGIFT"),
|
|
85
|
-
},
|
|
86
|
-
async ({ offerId }) => {
|
|
87
|
-
const api = buildHttpClient();
|
|
88
|
-
const res = await api.get(`/vouchers/offers/${offerId}`);
|
|
89
|
-
return { content: [{ type: "text", text: JSON.stringify(res.data) }] };
|
|
106
|
+
return asText(res.data);
|
|
90
107
|
}
|
|
91
108
|
);
|
|
92
109
|
|
|
93
110
|
server.tool(
|
|
94
111
|
"get_voucher_filters",
|
|
95
|
-
"Get all valid filter values for the voucher catalog — available brand names, ISO country codes,
|
|
112
|
+
"Get all valid filter values for the voucher catalog — available brand names, ISO country codes, category slugs and regions. Free — no payment. Use before browse_voucher_offers to know what filter values are accepted.",
|
|
96
113
|
{},
|
|
97
114
|
async () => {
|
|
98
|
-
const
|
|
99
|
-
|
|
100
|
-
return { content: [{ type: "text", text: JSON.stringify(res.data) }] };
|
|
115
|
+
const res = await freeApi.get("/capabilities");
|
|
116
|
+
return asText(res.data?.vouchers ?? res.data);
|
|
101
117
|
}
|
|
102
118
|
);
|
|
103
119
|
|
|
104
|
-
|
|
105
|
-
"purchase_voucher",
|
|
106
|
-
"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.",
|
|
107
|
-
{
|
|
108
|
-
offerId: z.string().describe("Voucher offer ID from browse_voucher_offers"),
|
|
109
|
-
firstName: z.string().describe("Recipient first name"),
|
|
110
|
-
lastName: z.string().describe("Recipient last name"),
|
|
111
|
-
email: z.string().optional().describe("Recipient email — required for some brands, check requiredFields on the offer"),
|
|
112
|
-
country: z.string().optional().describe("Recipient ISO country code — required for some brands"),
|
|
113
|
-
value: z.number().optional().describe("USD amount — RANGE priceType only (open-value cards). Omit for FIXED."),
|
|
114
|
-
},
|
|
115
|
-
async ({ offerId, firstName, lastName, email, country, value }) => {
|
|
116
|
-
const api = buildHttpClient();
|
|
117
|
-
const body = {
|
|
118
|
-
offerId,
|
|
119
|
-
recipient: { firstName, lastName, ...(email && { email }), ...(country && { country }) },
|
|
120
|
-
...(value !== undefined && { value }),
|
|
121
|
-
};
|
|
122
|
-
try {
|
|
123
|
-
const res = await api.post("/vouchers/purchase", body);
|
|
124
|
-
return { content: [{ type: "text", text: JSON.stringify(res.data) }] };
|
|
125
|
-
} catch (err) {
|
|
126
|
-
console.error('[purchase_voucher error]', err?.message);
|
|
127
|
-
console.error('[purchase_voucher status]', err?.response?.status);
|
|
128
|
-
console.error('[purchase_voucher data]', JSON.stringify(err?.response?.data));
|
|
129
|
-
console.error('[purchase_voucher stack]', err?.stack);
|
|
130
|
-
throw err;
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
);
|
|
134
|
-
|
|
135
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
136
|
-
// TOPUP TOOLS
|
|
137
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
120
|
+
// ── Topups (browse) ────────────────────────────────────────────────────────
|
|
138
121
|
|
|
139
122
|
server.tool(
|
|
140
123
|
"browse_topup_offers",
|
|
141
|
-
"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.
|
|
124
|
+
"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. Free — no payment. Returns offer IDs and prices; use them with purchase_topup (requires a self-hosted wallet).",
|
|
142
125
|
{
|
|
143
126
|
country: z.string().optional().describe("ISO country code e.g. GH, ES, NG"),
|
|
144
127
|
operator: z.string().optional().describe("Operator name filter e.g. MTN, Airtel"),
|
|
@@ -146,51 +129,18 @@ function createMcpServer() {
|
|
|
146
129
|
offset: z.number().optional().default(0),
|
|
147
130
|
},
|
|
148
131
|
async ({ country, operator, limit, offset }) => {
|
|
149
|
-
const
|
|
150
|
-
|
|
151
|
-
params: { country, operator, limit },
|
|
132
|
+
const res = await freeApi.get("/topups", {
|
|
133
|
+
params: { country, brand: operator, limit, offset },
|
|
152
134
|
});
|
|
153
|
-
return
|
|
135
|
+
return asText(res.data);
|
|
154
136
|
}
|
|
155
137
|
);
|
|
156
138
|
|
|
157
|
-
|
|
158
|
-
"get_topup_offer",
|
|
159
|
-
"Get full details for a specific mobile top-up offer by ID — including price, operator, country, and required recipient fields.",
|
|
160
|
-
{
|
|
161
|
-
offerId: z.string().describe("Topup offer ID e.g. AIRTELTIGO_GH_025"),
|
|
162
|
-
},
|
|
163
|
-
async ({ offerId }) => {
|
|
164
|
-
const api = buildHttpClient();
|
|
165
|
-
const res = await api.get(`/topups/offers/${offerId}`);
|
|
166
|
-
return { content: [{ type: "text", text: JSON.stringify(res.data) }] };
|
|
167
|
-
}
|
|
168
|
-
);
|
|
169
|
-
|
|
170
|
-
server.tool(
|
|
171
|
-
"purchase_topup",
|
|
172
|
-
"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.",
|
|
173
|
-
{
|
|
174
|
-
offerId: z.string().describe("Topup offer ID from browse_topup_offers"),
|
|
175
|
-
msisdn: z.string().describe("Recipient phone number in E.164 format e.g. +233201234567"),
|
|
176
|
-
},
|
|
177
|
-
async ({ offerId, msisdn }) => {
|
|
178
|
-
const api = buildHttpClient();
|
|
179
|
-
const res = await api.post("/topups/purchase", {
|
|
180
|
-
offerId,
|
|
181
|
-
recipient: { msisdn },
|
|
182
|
-
});
|
|
183
|
-
return { content: [{ type: "text", text: JSON.stringify(res.data) }] };
|
|
184
|
-
}
|
|
185
|
-
);
|
|
186
|
-
|
|
187
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
188
|
-
// ESIM TOOLS
|
|
189
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
139
|
+
// ── eSIMs (browse) ─────────────────────────────────────────────────────────
|
|
190
140
|
|
|
191
141
|
server.tool(
|
|
192
142
|
"browse_esim_offers",
|
|
193
|
-
"Browse eSIM data plans across 190+ countries and regions —
|
|
143
|
+
"Browse eSIM data plans across 190+ countries and regions — unlimited plans, regional multi-country bundles, and global roaming. Filter by single country (e.g. ES) or by a multi-country region. Set regional:true to list ONLY multi-country bundles (e.g. ESIM-N-AMERICA-10D-UNLIMITED covering US+CA+MX); these have no single country code. Free — no payment. Returns offer IDs and prices; use them with purchase_esim (requires a self-hosted wallet).",
|
|
194
144
|
{
|
|
195
145
|
country: z.string().optional().describe("ISO country code for single-country plans, e.g. ES, US, JP. Omit when using regional/regions."),
|
|
196
146
|
regions: z.enum([
|
|
@@ -200,51 +150,19 @@ function createMcpServer() {
|
|
|
200
150
|
"Middle East and North Africa",
|
|
201
151
|
]).optional().describe("Multi-country region to filter by (exact Zendit enum value)."),
|
|
202
152
|
regional: z.boolean().optional().describe("true → return ONLY multi-country regional bundles (country is empty). Combine with `regions` to scope to one region."),
|
|
153
|
+
q: z.string().optional().describe("Free-text filter e.g. \"10GB\", \"unlimited\""),
|
|
203
154
|
limit: z.number().optional().default(10),
|
|
204
155
|
offset: z.number().optional().default(0),
|
|
205
156
|
},
|
|
206
|
-
async ({ country, regions, regional, limit, offset }) => {
|
|
207
|
-
const
|
|
208
|
-
|
|
209
|
-
params: { country, regions, regional: regional ? "true" : undefined, limit, offset },
|
|
210
|
-
});
|
|
211
|
-
return { content: [{ type: "text", text: JSON.stringify(res.data) }] };
|
|
212
|
-
}
|
|
213
|
-
);
|
|
214
|
-
|
|
215
|
-
server.tool(
|
|
216
|
-
"get_esim_offer",
|
|
217
|
-
"Get full details for a specific eSIM plan by ID — including exact price, data allowance, duration, coverage countries, and whether data is unlimited.",
|
|
218
|
-
{
|
|
219
|
-
offerId: z.string().describe("eSIM offer ID e.g. ESIM-ES-7D-10GB-NOROAM"),
|
|
220
|
-
},
|
|
221
|
-
async ({ offerId }) => {
|
|
222
|
-
const api = buildHttpClient();
|
|
223
|
-
const res = await api.get(`/esims/offers/${offerId}`);
|
|
224
|
-
return { content: [{ type: "text", text: JSON.stringify(res.data) }] };
|
|
225
|
-
}
|
|
226
|
-
);
|
|
227
|
-
|
|
228
|
-
server.tool(
|
|
229
|
-
"purchase_esim",
|
|
230
|
-
"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.",
|
|
231
|
-
{
|
|
232
|
-
offerId: z.string().describe("eSIM offer ID from browse_esim_offers"),
|
|
233
|
-
iccid: z.string().optional().describe("Existing ICCID — only for top-up/recharge of an installed eSIM"),
|
|
234
|
-
},
|
|
235
|
-
async ({ offerId, iccid }) => {
|
|
236
|
-
const api = buildHttpClient();
|
|
237
|
-
const res = await api.post("/esims/purchase", {
|
|
238
|
-
offerId,
|
|
239
|
-
...(iccid && { iccid }),
|
|
157
|
+
async ({ country, regions, regional, q, limit, offset }) => {
|
|
158
|
+
const res = await freeApi.get("/esims", {
|
|
159
|
+
params: { country, regions, regional: regional ? "true" : undefined, q, limit, offset },
|
|
240
160
|
});
|
|
241
|
-
return
|
|
161
|
+
return asText(res.data);
|
|
242
162
|
}
|
|
243
163
|
);
|
|
244
164
|
|
|
245
|
-
//
|
|
246
|
-
// ORDER POLLING TOOLS (free — no payment)
|
|
247
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
165
|
+
// ── Order polling & refunds (free — no payment) ────────────────────────────
|
|
248
166
|
|
|
249
167
|
server.tool(
|
|
250
168
|
"get_order",
|
|
@@ -254,7 +172,7 @@ function createMcpServer() {
|
|
|
254
172
|
},
|
|
255
173
|
async ({ orderId }) => {
|
|
256
174
|
const res = await axios.get(`${API_BASE}/orders/${orderId}`);
|
|
257
|
-
return
|
|
175
|
+
return asText(res.data);
|
|
258
176
|
}
|
|
259
177
|
);
|
|
260
178
|
|
|
@@ -266,7 +184,7 @@ function createMcpServer() {
|
|
|
266
184
|
},
|
|
267
185
|
async ({ txHash }) => {
|
|
268
186
|
const res = await axios.get(`${API_BASE}/orders`, { params: { txHash } });
|
|
269
|
-
return
|
|
187
|
+
return asText(res.data);
|
|
270
188
|
}
|
|
271
189
|
);
|
|
272
190
|
|
|
@@ -284,7 +202,124 @@ function createMcpServer() {
|
|
|
284
202
|
{ txHash },
|
|
285
203
|
{ headers: { "Idempotency-Key": idempotencyKey } }
|
|
286
204
|
);
|
|
287
|
-
return
|
|
205
|
+
return asText(res.data);
|
|
206
|
+
}
|
|
207
|
+
);
|
|
208
|
+
|
|
209
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
210
|
+
// PAID TOOLS — only registered when EVM_PRIVATE_KEY is set (self-host).
|
|
211
|
+
// These settle x402 payments from the operator's OWN wallet. They are NOT
|
|
212
|
+
// present on the public hosted server, so it can never spend.
|
|
213
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
214
|
+
|
|
215
|
+
if (!HAS_WALLET) {
|
|
216
|
+
return server;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// ── Offer detail (paid $0.001 x402) ────────────────────────────────────────
|
|
220
|
+
|
|
221
|
+
server.tool(
|
|
222
|
+
"get_voucher_offer",
|
|
223
|
+
"Get full details for a specific voucher or gift card offer by ID — exact price, denomination, currency, priceType (FIXED or RANGE), and requiredFields for the recipient. Costs a small x402 fee from your wallet. Call before purchasing to confirm the exact payment amount.",
|
|
224
|
+
{
|
|
225
|
+
offerId: z.string().describe("Voucher offer ID e.g. 1-800-BASKETS_US_002_EGIFT"),
|
|
226
|
+
},
|
|
227
|
+
async ({ offerId }) => {
|
|
228
|
+
const api = buildPaidClient();
|
|
229
|
+
const res = await api.get(`/vouchers/offers/${offerId}`);
|
|
230
|
+
return asText(res.data);
|
|
231
|
+
}
|
|
232
|
+
);
|
|
233
|
+
|
|
234
|
+
server.tool(
|
|
235
|
+
"get_topup_offer",
|
|
236
|
+
"Get full details for a specific mobile top-up offer by ID — price, operator, country, and required recipient fields. Costs a small x402 fee from your wallet.",
|
|
237
|
+
{
|
|
238
|
+
offerId: z.string().describe("Topup offer ID e.g. AIRTELTIGO_GH_025"),
|
|
239
|
+
},
|
|
240
|
+
async ({ offerId }) => {
|
|
241
|
+
const api = buildPaidClient();
|
|
242
|
+
const res = await api.get(`/topups/offers/${offerId}`);
|
|
243
|
+
return asText(res.data);
|
|
244
|
+
}
|
|
245
|
+
);
|
|
246
|
+
|
|
247
|
+
server.tool(
|
|
248
|
+
"get_esim_offer",
|
|
249
|
+
"Get full details for a specific eSIM plan by ID — exact price, data allowance, duration, coverage countries, and whether data is unlimited. Costs a small x402 fee from your wallet.",
|
|
250
|
+
{
|
|
251
|
+
offerId: z.string().describe("eSIM offer ID e.g. ESIM-ES-7D-10GB-NOROAM"),
|
|
252
|
+
},
|
|
253
|
+
async ({ offerId }) => {
|
|
254
|
+
const api = buildPaidClient();
|
|
255
|
+
const res = await api.get(`/esims/offers/${offerId}`);
|
|
256
|
+
return asText(res.data);
|
|
257
|
+
}
|
|
258
|
+
);
|
|
259
|
+
|
|
260
|
+
// ── Purchase (paid — product price + markup, x402) ─────────────────────────
|
|
261
|
+
|
|
262
|
+
server.tool(
|
|
263
|
+
"purchase_voucher",
|
|
264
|
+
"Purchase a gift card or voucher. The x402 payment (USDC on Base) settles automatically from YOUR 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.",
|
|
265
|
+
{
|
|
266
|
+
offerId: z.string().describe("Voucher offer ID from browse_voucher_offers"),
|
|
267
|
+
firstName: z.string().describe("Recipient first name"),
|
|
268
|
+
lastName: z.string().describe("Recipient last name"),
|
|
269
|
+
email: z.string().optional().describe("Recipient email — required for some brands, check requiredFields on the offer"),
|
|
270
|
+
country: z.string().optional().describe("Recipient ISO country code — required for some brands"),
|
|
271
|
+
value: z.number().optional().describe("USD amount — RANGE priceType only (open-value cards). Omit for FIXED."),
|
|
272
|
+
},
|
|
273
|
+
async ({ offerId, firstName, lastName, email, country, value }) => {
|
|
274
|
+
const api = buildPaidClient();
|
|
275
|
+
const body = {
|
|
276
|
+
offerId,
|
|
277
|
+
recipient: { firstName, lastName, ...(email && { email }), ...(country && { country }) },
|
|
278
|
+
...(value !== undefined && { value }),
|
|
279
|
+
};
|
|
280
|
+
try {
|
|
281
|
+
const res = await api.post("/vouchers/purchase", body);
|
|
282
|
+
return asText(res.data);
|
|
283
|
+
} catch (err) {
|
|
284
|
+
console.error("[purchase_voucher error]", err?.message);
|
|
285
|
+
console.error("[purchase_voucher status]", err?.response?.status);
|
|
286
|
+
console.error("[purchase_voucher data]", JSON.stringify(err?.response?.data));
|
|
287
|
+
throw err;
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
);
|
|
291
|
+
|
|
292
|
+
server.tool(
|
|
293
|
+
"purchase_topup",
|
|
294
|
+
"Purchase a prepaid mobile airtime or data top-up. The x402 payment (USDC on Base) settles automatically from YOUR wallet. Provide offerId from browse_topup_offers and the recipient phone number in E.164 format. Delivered directly to the recipient's SIM. Returns orderId and txHash — poll get_order for delivery confirmation.",
|
|
295
|
+
{
|
|
296
|
+
offerId: z.string().describe("Topup offer ID from browse_topup_offers"),
|
|
297
|
+
msisdn: z.string().describe("Recipient phone number in E.164 format e.g. +233201234567"),
|
|
298
|
+
},
|
|
299
|
+
async ({ offerId, msisdn }) => {
|
|
300
|
+
const api = buildPaidClient();
|
|
301
|
+
const res = await api.post("/topups/purchase", {
|
|
302
|
+
offerId,
|
|
303
|
+
recipient: { msisdn },
|
|
304
|
+
});
|
|
305
|
+
return asText(res.data);
|
|
306
|
+
}
|
|
307
|
+
);
|
|
308
|
+
|
|
309
|
+
server.tool(
|
|
310
|
+
"purchase_esim",
|
|
311
|
+
"Purchase an eSIM data plan. The x402 payment (USDC on Base) settles automatically from YOUR 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.",
|
|
312
|
+
{
|
|
313
|
+
offerId: z.string().describe("eSIM offer ID from browse_esim_offers"),
|
|
314
|
+
iccid: z.string().optional().describe("Existing ICCID — only for top-up/recharge of an installed eSIM"),
|
|
315
|
+
},
|
|
316
|
+
async ({ offerId, iccid }) => {
|
|
317
|
+
const api = buildPaidClient();
|
|
318
|
+
const res = await api.post("/esims/purchase", {
|
|
319
|
+
offerId,
|
|
320
|
+
...(iccid && { iccid }),
|
|
321
|
+
});
|
|
322
|
+
return asText(res.data);
|
|
288
323
|
}
|
|
289
324
|
);
|
|
290
325
|
|
|
@@ -298,9 +333,19 @@ function createMcpServer() {
|
|
|
298
333
|
const app = express();
|
|
299
334
|
app.use(express.json());
|
|
300
335
|
|
|
336
|
+
// Optional bearer-token gate. No-op unless MCP_AUTH_TOKEN is set.
|
|
337
|
+
function authorized(req, res) {
|
|
338
|
+
if (!MCP_AUTH_TOKEN) return true;
|
|
339
|
+
const header = req.headers["authorization"] ?? "";
|
|
340
|
+
if (header === `Bearer ${MCP_AUTH_TOKEN}`) return true;
|
|
341
|
+
res.status(401).json({ error: "Unauthorized" });
|
|
342
|
+
return false;
|
|
343
|
+
}
|
|
344
|
+
|
|
301
345
|
const sessions = new Map(); // sessionId → { server, transport }
|
|
302
346
|
|
|
303
347
|
app.post("/mcp", async (req, res) => {
|
|
348
|
+
if (!authorized(req, res)) return;
|
|
304
349
|
try {
|
|
305
350
|
const sessionId = req.headers["mcp-session-id"];
|
|
306
351
|
|
|
@@ -338,6 +383,7 @@ app.post("/mcp", async (req, res) => {
|
|
|
338
383
|
|
|
339
384
|
// SSE notifications (GET /mcp)
|
|
340
385
|
app.get("/mcp", async (req, res) => {
|
|
386
|
+
if (!authorized(req, res)) return;
|
|
341
387
|
const sessionId = req.headers["mcp-session-id"];
|
|
342
388
|
if (!sessionId || !sessions.has(sessionId)) {
|
|
343
389
|
return res.status(400).json({ error: "Invalid or missing session ID" });
|
|
@@ -348,6 +394,7 @@ app.get("/mcp", async (req, res) => {
|
|
|
348
394
|
|
|
349
395
|
// Session termination (DELETE /mcp)
|
|
350
396
|
app.delete("/mcp", async (req, res) => {
|
|
397
|
+
if (!authorized(req, res)) return;
|
|
351
398
|
const sessionId = req.headers["mcp-session-id"];
|
|
352
399
|
if (!sessionId || !sessions.has(sessionId)) {
|
|
353
400
|
return res.status(404).json({ error: "Session not found" });
|
|
@@ -357,10 +404,42 @@ app.delete("/mcp", async (req, res) => {
|
|
|
357
404
|
sessions.delete(sessionId);
|
|
358
405
|
});
|
|
359
406
|
|
|
360
|
-
app.get("/health", (_req, res) =>
|
|
407
|
+
app.get("/health", (_req, res) =>
|
|
408
|
+
res.json({ status: "ok", name: "reloadpi-mcp", mode: HAS_WALLET ? "purchase" : "browse-only" })
|
|
409
|
+
);
|
|
410
|
+
|
|
411
|
+
// In stdio mode we must NOT write to stdout (it's the MCP channel) — log to stderr.
|
|
412
|
+
function logModeToStderr(prefix) {
|
|
413
|
+
if (HAS_WALLET) {
|
|
414
|
+
try {
|
|
415
|
+
const addr = privateKeyToAccount(process.env.EVM_PRIVATE_KEY).address;
|
|
416
|
+
console.error(`${prefix}💳 Purchase mode — signing from ${addr}`);
|
|
417
|
+
} catch {
|
|
418
|
+
console.error(`${prefix}💳 Purchase mode — EVM_PRIVATE_KEY set`);
|
|
419
|
+
}
|
|
420
|
+
} else {
|
|
421
|
+
console.error(`${prefix}🔎 Browse-only mode — no wallet; purchase tools disabled`);
|
|
422
|
+
}
|
|
423
|
+
}
|
|
361
424
|
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
425
|
+
if (STDIO) {
|
|
426
|
+
// Local, client-spawned server (npx via Claude/Cursor `command`/`args`).
|
|
427
|
+
const { StdioServerTransport } = await import("@modelcontextprotocol/sdk/server/stdio.js");
|
|
428
|
+
const server = createMcpServer();
|
|
429
|
+
await server.connect(new StdioServerTransport());
|
|
430
|
+
logModeToStderr("reloadpi-mcp (stdio) ready — ");
|
|
431
|
+
} else {
|
|
432
|
+
// Hosted / HTTP server.
|
|
433
|
+
app.listen(PORT, () => {
|
|
434
|
+
console.log(`🚀 Reloadpi MCP server listening on port ${PORT}`);
|
|
435
|
+
console.log(` MCP endpoint: http://localhost:${PORT}/mcp`);
|
|
436
|
+
if (HAS_WALLET) {
|
|
437
|
+
logModeToStderr(" ");
|
|
438
|
+
console.error(` ⚠️ Do NOT run this on a public host; anyone who connects could spend`);
|
|
439
|
+
console.error(` this wallet. Public hosting must omit EVM_PRIVATE_KEY.`);
|
|
440
|
+
} else {
|
|
441
|
+
console.log(` 🔎 Browse-only mode — no wallet; purchase tools disabled (safe for public hosting)`);
|
|
442
|
+
}
|
|
443
|
+
if (MCP_AUTH_TOKEN) console.log(` 🔒 /mcp requires Authorization: Bearer <MCP_AUTH_TOKEN>`);
|
|
444
|
+
});
|
|
445
|
+
}
|