@voidmob/mcp 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 +138 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +22 -0
- package/dist/mock-data/esim.d.ts +17 -0
- package/dist/mock-data/esim.js +30 -0
- package/dist/mock-data/proxy.d.ts +17 -0
- package/dist/mock-data/proxy.js +29 -0
- package/dist/mock-data/sms.d.ts +13 -0
- package/dist/mock-data/sms.js +24 -0
- package/dist/sandbox/state.d.ts +75 -0
- package/dist/sandbox/state.js +42 -0
- package/dist/tools/esim.d.ts +2 -0
- package/dist/tools/esim.js +218 -0
- package/dist/tools/orders.d.ts +2 -0
- package/dist/tools/orders.js +81 -0
- package/dist/tools/proxy.d.ts +2 -0
- package/dist/tools/proxy.js +232 -0
- package/dist/tools/sms.d.ts +2 -0
- package/dist/tools/sms.js +158 -0
- package/dist/tools/wallet.d.ts +2 -0
- package/dist/tools/wallet.js +66 -0
- package/dist/utils/format.d.ts +12 -0
- package/dist/utils/format.js +55 -0
- package/dist/utils/validation.d.ts +6 -0
- package/dist/utils/validation.js +32 -0
- package/package.json +46 -0
package/README.md
ADDED
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
# VoidMob MCP
|
|
2
|
+
|
|
3
|
+
Mobile proxies, non-VoIP SMS verifications, and global eSIMs for AI agents and MCP clients.
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
npx -y @voidmob/mcp
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
## Quick Start
|
|
10
|
+
|
|
11
|
+
Add VoidMob to your MCP client. No auth, no config, no API key.
|
|
12
|
+
|
|
13
|
+
### Claude Code
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
claude mcp add voidmob -- npx -y @voidmob/mcp
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
### Cursor
|
|
20
|
+
|
|
21
|
+
Add to `~/.cursor/mcp.json`:
|
|
22
|
+
|
|
23
|
+
```json
|
|
24
|
+
{
|
|
25
|
+
"mcpServers": {
|
|
26
|
+
"voidmob": {
|
|
27
|
+
"command": "npx",
|
|
28
|
+
"args": ["-y", "@voidmob/mcp"]
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
### Claude Desktop
|
|
35
|
+
|
|
36
|
+
Add to your config file:
|
|
37
|
+
|
|
38
|
+
- macOS: `~/Library/Application Support/Claude/claude_desktop_config.json`
|
|
39
|
+
- Linux: `~/.config/Claude/claude_desktop_config.json`
|
|
40
|
+
- Windows: `%APPDATA%\Claude\claude_desktop_config.json`
|
|
41
|
+
|
|
42
|
+
```json
|
|
43
|
+
{
|
|
44
|
+
"mcpServers": {
|
|
45
|
+
"voidmob": {
|
|
46
|
+
"command": "npx",
|
|
47
|
+
"args": ["-y", "@voidmob/mcp"]
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Restart your client. You now have 18 tools for mobile proxies, SMS verification, eSIM, and wallet operations.
|
|
54
|
+
|
|
55
|
+
## Available Tools
|
|
56
|
+
|
|
57
|
+
### Mobile Proxies
|
|
58
|
+
|
|
59
|
+
| Tool | Description |
|
|
60
|
+
|------|-------------|
|
|
61
|
+
| `search_proxies` | Search mobile proxy options by country or type |
|
|
62
|
+
| `get_proxy_pricing` | Get detailed pricing for a proxy type + country |
|
|
63
|
+
| `purchase_proxy` | Purchase a mobile proxy (pay-per-GB or dedicated monthly) |
|
|
64
|
+
| `get_proxy_status` | Check bandwidth usage and connection details |
|
|
65
|
+
| `rotate_proxy` | Rotate to a new IP address |
|
|
66
|
+
|
|
67
|
+
### SMS (US non-VoIP)
|
|
68
|
+
|
|
69
|
+
| Tool | Description |
|
|
70
|
+
|------|-------------|
|
|
71
|
+
| `search_sms_services` | Search US non-VoIP SMS verification services by name or category |
|
|
72
|
+
| `get_sms_price` | Get pricing for a US non-VoIP SMS verification service |
|
|
73
|
+
| `rent_number` | Rent a US non-VoIP phone number for verification (5 min expiry) |
|
|
74
|
+
| `get_messages` | Check for incoming SMS messages on a rented number |
|
|
75
|
+
| `cancel_rental` | Cancel a rental (full refund if no messages received) |
|
|
76
|
+
|
|
77
|
+
### eSIM
|
|
78
|
+
|
|
79
|
+
| Tool | Description |
|
|
80
|
+
|------|-------------|
|
|
81
|
+
| `search_esim_plans` | Search eSIM data plans by country, duration, or data amount |
|
|
82
|
+
| `get_esim_plan_details` | Get full plan details including APN and top-up info |
|
|
83
|
+
| `purchase_esim` | Purchase an eSIM plan and get a QR code for installation |
|
|
84
|
+
| `get_esim_usage` | Check data usage, remaining balance, and time left |
|
|
85
|
+
| `topup_esim` | Add more data to an active eSIM order |
|
|
86
|
+
|
|
87
|
+
### Wallet
|
|
88
|
+
|
|
89
|
+
| Tool | Description |
|
|
90
|
+
|------|-------------|
|
|
91
|
+
| `get_balance` | Get wallet balance and recent transactions |
|
|
92
|
+
| `deposit` | Create a crypto deposit (BTC, ETH, SOL) |
|
|
93
|
+
|
|
94
|
+
### General
|
|
95
|
+
|
|
96
|
+
| Tool | Description |
|
|
97
|
+
|------|-------------|
|
|
98
|
+
| `list_orders` | List all orders across SMS, eSIM, and proxy services |
|
|
99
|
+
|
|
100
|
+
## Example Conversations
|
|
101
|
+
|
|
102
|
+
These are things you can ask Claude, Cursor, or any MCP client after adding VoidMob:
|
|
103
|
+
|
|
104
|
+
> Set up a dedicated US mobile proxy and give me the credentials
|
|
105
|
+
|
|
106
|
+
> Rotate my US dedicated proxy IP
|
|
107
|
+
|
|
108
|
+
> How much does a 5GB eSIM plan in the UK cost?
|
|
109
|
+
|
|
110
|
+
> Rent me a US phone number for WhatsApp verification
|
|
111
|
+
|
|
112
|
+
> Check if my verification code arrived
|
|
113
|
+
|
|
114
|
+
> Find the cheapest eSIM plan for 2 weeks in Japan with at least 5GB
|
|
115
|
+
|
|
116
|
+
> Show me all my active orders
|
|
117
|
+
|
|
118
|
+
## Sandbox Mode
|
|
119
|
+
|
|
120
|
+
VoidMob MCP currently runs in **sandbox mode**. Everything works, but the data is mock.
|
|
121
|
+
|
|
122
|
+
- **$50 starting balance** - enough to try every tool
|
|
123
|
+
- **Stateful** - renting a number deducts balance, messages appear after a few seconds, deposits auto-confirm
|
|
124
|
+
- **Deposits auto-confirm** in ~5 seconds
|
|
125
|
+
- **State resets** on server restart
|
|
126
|
+
- **No auth required** - zero config, just `npx -y @voidmob/mcp`
|
|
127
|
+
|
|
128
|
+
The sandbox is designed to let you explore the full flow: deposit funds, rent a number, receive a verification code, check your balance. Everything behaves like the real API, just with mock data underneath.
|
|
129
|
+
|
|
130
|
+
## API Access
|
|
131
|
+
|
|
132
|
+
Currently in sandbox mode. Join the waitlist at [voidmob.com](https://voidmob.com) for early API access.
|
|
133
|
+
|
|
134
|
+
---
|
|
135
|
+
|
|
136
|
+
<p align="center">
|
|
137
|
+
<a href="https://voidmob.com">Website</a> · <a href="https://github.com/voidmobcom/voidmob-mcp">GitHub</a> · <a href="https://x.com/voidmob_com">X (Twitter)</a> · MIT License
|
|
138
|
+
</p>
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
4
|
+
import { registerWalletTools } from "./tools/wallet.js";
|
|
5
|
+
import { registerSmsTools } from "./tools/sms.js";
|
|
6
|
+
import { registerEsimTools } from "./tools/esim.js";
|
|
7
|
+
import { registerProxyTools } from "./tools/proxy.js";
|
|
8
|
+
import { registerOrdersTools } from "./tools/orders.js";
|
|
9
|
+
const server = new McpServer({
|
|
10
|
+
name: "@voidmob/mcp",
|
|
11
|
+
version: "0.1.0",
|
|
12
|
+
});
|
|
13
|
+
registerWalletTools(server);
|
|
14
|
+
registerSmsTools(server);
|
|
15
|
+
registerEsimTools(server);
|
|
16
|
+
registerProxyTools(server);
|
|
17
|
+
registerOrdersTools(server);
|
|
18
|
+
async function main() {
|
|
19
|
+
const transport = new StdioServerTransport();
|
|
20
|
+
await server.connect(transport);
|
|
21
|
+
}
|
|
22
|
+
main().catch(console.error);
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export interface EsimPlan {
|
|
2
|
+
planId: string;
|
|
3
|
+
name: string;
|
|
4
|
+
country: string;
|
|
5
|
+
region: string;
|
|
6
|
+
data: number;
|
|
7
|
+
duration: number;
|
|
8
|
+
price: number;
|
|
9
|
+
carrier: string;
|
|
10
|
+
apn: string;
|
|
11
|
+
routing: string;
|
|
12
|
+
topupAvailable: boolean;
|
|
13
|
+
topupPrice: number;
|
|
14
|
+
}
|
|
15
|
+
export declare const esimPlans: EsimPlan[];
|
|
16
|
+
export declare function searchPlans(country?: string, duration?: number, dataAmount?: number): EsimPlan[];
|
|
17
|
+
export declare function getPlan(planId: string): EsimPlan | undefined;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
export const esimPlans = [
|
|
2
|
+
{ planId: "esim_jp_3g_7d", name: "Japan 3GB / 7 Days", country: "JP", region: "Asia", data: 3, duration: 7, price: 4.50, carrier: "IIJmio", apn: "iijmio.jp", routing: "Tokyo, Japan", topupAvailable: true, topupPrice: 1.80 },
|
|
3
|
+
{ planId: "esim_jp_5g_14d", name: "Japan 5GB / 14 Days", country: "JP", region: "Asia", data: 5, duration: 14, price: 7.50, carrier: "IIJmio", apn: "iijmio.jp", routing: "Tokyo, Japan", topupAvailable: true, topupPrice: 1.80 },
|
|
4
|
+
{ planId: "esim_jp_10g_30d", name: "Japan 10GB / 30 Days", country: "JP", region: "Asia", data: 10, duration: 30, price: 12.00, carrier: "IIJmio", apn: "iijmio.jp", routing: "Tokyo, Japan", topupAvailable: true, topupPrice: 1.50 },
|
|
5
|
+
{ planId: "esim_jp_unl_30d", name: "Japan Unlimited / 30 Days", country: "JP", region: "Asia", data: 999, duration: 30, price: 22.00, carrier: "SoftBank", apn: "plus.4g", routing: "Tokyo, Japan", topupAvailable: false, topupPrice: 0 },
|
|
6
|
+
{ planId: "esim_us_5g_7d", name: "USA 5GB / 7 Days", country: "US", region: "North America", data: 5, duration: 7, price: 6.00, carrier: "T-Mobile", apn: "fast.t-mobile.com", routing: "Los Angeles, US", topupAvailable: true, topupPrice: 1.50 },
|
|
7
|
+
{ planId: "esim_us_10g_30d", name: "USA 10GB / 30 Days", country: "US", region: "North America", data: 10, duration: 30, price: 11.00, carrier: "T-Mobile", apn: "fast.t-mobile.com", routing: "Los Angeles, US", topupAvailable: true, topupPrice: 1.30 },
|
|
8
|
+
{ planId: "esim_us_20g_30d", name: "USA 20GB / 30 Days", country: "US", region: "North America", data: 20, duration: 30, price: 18.00, carrier: "T-Mobile", apn: "fast.t-mobile.com", routing: "Los Angeles, US", topupAvailable: true, topupPrice: 1.10 },
|
|
9
|
+
{ planId: "esim_gb_5g_7d", name: "UK 5GB / 7 Days", country: "GB", region: "Europe", data: 5, duration: 7, price: 5.50, carrier: "Three", apn: "three.co.uk", routing: "London, UK", topupAvailable: true, topupPrice: 1.40 },
|
|
10
|
+
{ planId: "esim_gb_10g_30d", name: "UK 10GB / 30 Days", country: "GB", region: "Europe", data: 10, duration: 30, price: 10.00, carrier: "Three", apn: "three.co.uk", routing: "London, UK", topupAvailable: true, topupPrice: 1.20 },
|
|
11
|
+
{ planId: "esim_de_5g_7d", name: "Germany 5GB / 7 Days", country: "DE", region: "Europe", data: 5, duration: 7, price: 5.00, carrier: "O2", apn: "internet", routing: "Frankfurt, DE", topupAvailable: true, topupPrice: 1.30 },
|
|
12
|
+
{ planId: "esim_de_10g_30d", name: "Germany 10GB / 30 Days", country: "DE", region: "Europe", data: 10, duration: 30, price: 9.00, carrier: "O2", apn: "internet", routing: "Frankfurt, DE", topupAvailable: true, topupPrice: 1.10 },
|
|
13
|
+
{ planId: "esim_th_5g_7d", name: "Thailand 5GB / 7 Days", country: "TH", region: "Asia", data: 5, duration: 7, price: 3.50, carrier: "AIS", apn: "internet", routing: "Bangkok, TH", topupAvailable: true, topupPrice: 0.90 },
|
|
14
|
+
{ planId: "esim_th_15g_30d", name: "Thailand 15GB / 30 Days", country: "TH", region: "Asia", data: 15, duration: 30, price: 8.00, carrier: "AIS", apn: "internet", routing: "Bangkok, TH", topupAvailable: true, topupPrice: 0.70 },
|
|
15
|
+
{ planId: "esim_tr_5g_7d", name: "Turkey 5GB / 7 Days", country: "TR", region: "Europe", data: 5, duration: 7, price: 4.00, carrier: "Turkcell", apn: "internet", routing: "Istanbul, TR", topupAvailable: true, topupPrice: 1.00 },
|
|
16
|
+
{ planId: "esim_br_5g_7d", name: "Brazil 5GB / 7 Days", country: "BR", region: "South America", data: 5, duration: 7, price: 5.50, carrier: "Claro", apn: "claro.com.br", routing: "Sao Paulo, BR", topupAvailable: true, topupPrice: 1.30 },
|
|
17
|
+
];
|
|
18
|
+
export function searchPlans(country, duration, dataAmount) {
|
|
19
|
+
let results = esimPlans;
|
|
20
|
+
if (country)
|
|
21
|
+
results = results.filter((p) => p.country === country);
|
|
22
|
+
if (duration)
|
|
23
|
+
results = results.filter((p) => p.duration >= duration);
|
|
24
|
+
if (dataAmount)
|
|
25
|
+
results = results.filter((p) => p.data >= dataAmount);
|
|
26
|
+
return results.sort((a, b) => a.price - b.price);
|
|
27
|
+
}
|
|
28
|
+
export function getPlan(planId) {
|
|
29
|
+
return esimPlans.find((p) => p.planId === planId);
|
|
30
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export type ProxyOption = {
|
|
2
|
+
country: string;
|
|
3
|
+
carrier: string;
|
|
4
|
+
type: "gb";
|
|
5
|
+
pricePerGb: number;
|
|
6
|
+
network: string;
|
|
7
|
+
} | {
|
|
8
|
+
country: string;
|
|
9
|
+
carrier: string;
|
|
10
|
+
type: "dedicated";
|
|
11
|
+
pricePerMonth: number;
|
|
12
|
+
bandwidth: number;
|
|
13
|
+
network: string;
|
|
14
|
+
};
|
|
15
|
+
export declare const proxyOptions: ProxyOption[];
|
|
16
|
+
export declare function searchProxyOptions(country?: string, type?: string): ProxyOption[];
|
|
17
|
+
export declare function getProxyPricing(type: string, country: string): ProxyOption[];
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
export const proxyOptions = [
|
|
2
|
+
{ country: "US", carrier: "Verizon", type: "gb", pricePerGb: 2.99, network: "5G" },
|
|
3
|
+
{ country: "US", carrier: "T-Mobile", type: "gb", pricePerGb: 2.49, network: "5G" },
|
|
4
|
+
{ country: "US", carrier: "AT&T", type: "gb", pricePerGb: 2.79, network: "5G" },
|
|
5
|
+
{ country: "US", carrier: "Verizon", type: "dedicated", pricePerMonth: 80, bandwidth: 30, network: "5G" },
|
|
6
|
+
{ country: "US", carrier: "T-Mobile", type: "dedicated", pricePerMonth: 70, bandwidth: 30, network: "5G" },
|
|
7
|
+
{ country: "GB", carrier: "Vodafone", type: "gb", pricePerGb: 2.99, network: "5G" },
|
|
8
|
+
{ country: "GB", carrier: "EE", type: "gb", pricePerGb: 2.79, network: "5G" },
|
|
9
|
+
{ country: "GB", carrier: "Vodafone", type: "dedicated", pricePerMonth: 85, bandwidth: 30, network: "5G" },
|
|
10
|
+
{ country: "DE", carrier: "Telekom", type: "gb", pricePerGb: 3.29, network: "5G" },
|
|
11
|
+
{ country: "DE", carrier: "Telekom", type: "dedicated", pricePerMonth: 90, bandwidth: 25, network: "5G" },
|
|
12
|
+
{ country: "NL", carrier: "KPN", type: "gb", pricePerGb: 2.79, network: "4G LTE" },
|
|
13
|
+
{ country: "NL", carrier: "KPN", type: "dedicated", pricePerMonth: 75, bandwidth: 25, network: "4G LTE" },
|
|
14
|
+
{ country: "BR", carrier: "Claro", type: "gb", pricePerGb: 1.99, network: "4G LTE" },
|
|
15
|
+
{ country: "IN", carrier: "Jio", type: "gb", pricePerGb: 0.99, network: "5G" },
|
|
16
|
+
{ country: "JP", carrier: "NTT Docomo", type: "gb", pricePerGb: 3.49, network: "5G" },
|
|
17
|
+
{ country: "JP", carrier: "NTT Docomo", type: "dedicated", pricePerMonth: 100, bandwidth: 25, network: "5G" },
|
|
18
|
+
];
|
|
19
|
+
export function searchProxyOptions(country, type) {
|
|
20
|
+
let results = proxyOptions;
|
|
21
|
+
if (country)
|
|
22
|
+
results = results.filter((p) => p.country === country);
|
|
23
|
+
if (type)
|
|
24
|
+
results = results.filter((p) => p.type === type);
|
|
25
|
+
return results;
|
|
26
|
+
}
|
|
27
|
+
export function getProxyPricing(type, country) {
|
|
28
|
+
return proxyOptions.filter((p) => p.type === type && p.country === country);
|
|
29
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export interface SmsService {
|
|
2
|
+
id: string;
|
|
3
|
+
name: string;
|
|
4
|
+
category: string;
|
|
5
|
+
price: number;
|
|
6
|
+
estimatedDelivery: string;
|
|
7
|
+
}
|
|
8
|
+
export declare const smsServices: SmsService[];
|
|
9
|
+
export declare function searchServices(query?: string): SmsService[];
|
|
10
|
+
export declare function getServicePrice(serviceId: string): {
|
|
11
|
+
service: SmsService;
|
|
12
|
+
price: number;
|
|
13
|
+
} | null;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export const smsServices = [
|
|
2
|
+
{ id: "whatsapp", name: "WhatsApp", category: "messaging", price: 2.50, estimatedDelivery: "1-3 minutes" },
|
|
3
|
+
{ id: "telegram", name: "Telegram", category: "messaging", price: 1.50, estimatedDelivery: "1-2 minutes" },
|
|
4
|
+
{ id: "google", name: "Google / Gmail", category: "email", price: 1.80, estimatedDelivery: "1-3 minutes" },
|
|
5
|
+
{ id: "twitter", name: "Twitter / X", category: "social", price: 2.00, estimatedDelivery: "1-5 minutes" },
|
|
6
|
+
{ id: "instagram", name: "Instagram", category: "social", price: 2.20, estimatedDelivery: "1-3 minutes" },
|
|
7
|
+
{ id: "discord", name: "Discord", category: "messaging", price: 1.20, estimatedDelivery: "1-2 minutes" },
|
|
8
|
+
{ id: "tiktok", name: "TikTok", category: "social", price: 2.50, estimatedDelivery: "1-5 minutes" },
|
|
9
|
+
{ id: "facebook", name: "Facebook", category: "social", price: 1.80, estimatedDelivery: "1-3 minutes" },
|
|
10
|
+
{ id: "uber", name: "Uber", category: "ride-hailing", price: 2.00, estimatedDelivery: "1-3 minutes" },
|
|
11
|
+
{ id: "openai", name: "OpenAI / ChatGPT", category: "ai", price: 3.00, estimatedDelivery: "1-3 minutes" },
|
|
12
|
+
];
|
|
13
|
+
export function searchServices(query) {
|
|
14
|
+
if (!query)
|
|
15
|
+
return smsServices;
|
|
16
|
+
const q = query.toLowerCase();
|
|
17
|
+
return smsServices.filter((s) => s.name.toLowerCase().includes(q) || s.category.toLowerCase().includes(q) || s.id.toLowerCase().includes(q));
|
|
18
|
+
}
|
|
19
|
+
export function getServicePrice(serviceId) {
|
|
20
|
+
const service = smsServices.find((s) => s.id === serviceId);
|
|
21
|
+
if (!service)
|
|
22
|
+
return null;
|
|
23
|
+
return { service, price: service.price };
|
|
24
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
export interface SmsMessage {
|
|
2
|
+
from: string;
|
|
3
|
+
text: string;
|
|
4
|
+
receivedAt: number;
|
|
5
|
+
}
|
|
6
|
+
export interface SmsRental {
|
|
7
|
+
rentalId: string;
|
|
8
|
+
number: string;
|
|
9
|
+
service: string;
|
|
10
|
+
country: string;
|
|
11
|
+
status: "active" | "completed" | "cancelled" | "expired";
|
|
12
|
+
messages: SmsMessage[];
|
|
13
|
+
expiry: number;
|
|
14
|
+
createdAt: number;
|
|
15
|
+
price: number;
|
|
16
|
+
}
|
|
17
|
+
export interface EsimOrder {
|
|
18
|
+
orderId: string;
|
|
19
|
+
planId: string;
|
|
20
|
+
planName: string;
|
|
21
|
+
country: string;
|
|
22
|
+
dataTotal: number;
|
|
23
|
+
dataUsed: number;
|
|
24
|
+
status: "active" | "completed" | "expired";
|
|
25
|
+
qrUrl: string;
|
|
26
|
+
apn: string;
|
|
27
|
+
expiry: number;
|
|
28
|
+
createdAt: number;
|
|
29
|
+
price: number;
|
|
30
|
+
}
|
|
31
|
+
export interface ProxyEntry {
|
|
32
|
+
proxyId: string;
|
|
33
|
+
type: "gb" | "dedicated";
|
|
34
|
+
country: string;
|
|
35
|
+
carrier: string;
|
|
36
|
+
credentials: {
|
|
37
|
+
host: string;
|
|
38
|
+
port: number;
|
|
39
|
+
username: string;
|
|
40
|
+
password: string;
|
|
41
|
+
};
|
|
42
|
+
bandwidthUsed: number;
|
|
43
|
+
bandwidthTotal: number;
|
|
44
|
+
status: "active" | "expired";
|
|
45
|
+
ip: string;
|
|
46
|
+
createdAt: number;
|
|
47
|
+
price: number;
|
|
48
|
+
}
|
|
49
|
+
export interface Deposit {
|
|
50
|
+
invoiceId: string;
|
|
51
|
+
amount: number;
|
|
52
|
+
currency: string;
|
|
53
|
+
status: "pending" | "completed";
|
|
54
|
+
createdAt: number;
|
|
55
|
+
}
|
|
56
|
+
export interface Transaction {
|
|
57
|
+
id: string;
|
|
58
|
+
type: "deposit" | "sms_rental" | "esim_purchase" | "proxy_purchase" | "refund" | "topup";
|
|
59
|
+
amount: number;
|
|
60
|
+
description: string;
|
|
61
|
+
createdAt: number;
|
|
62
|
+
}
|
|
63
|
+
declare class SandboxState {
|
|
64
|
+
balance: number;
|
|
65
|
+
transactions: Transaction[];
|
|
66
|
+
smsRentals: Map<string, SmsRental>;
|
|
67
|
+
esimOrders: Map<string, EsimOrder>;
|
|
68
|
+
proxies: Map<string, ProxyEntry>;
|
|
69
|
+
deposits: Map<string, Deposit>;
|
|
70
|
+
deductBalance(amount: number, type: Transaction["type"], description: string): boolean;
|
|
71
|
+
addBalance(amount: number, type: Transaction["type"], description: string): void;
|
|
72
|
+
resolvePendingDeposits(): void;
|
|
73
|
+
}
|
|
74
|
+
export declare const state: SandboxState;
|
|
75
|
+
export {};
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { generateId } from "../utils/validation.js";
|
|
2
|
+
class SandboxState {
|
|
3
|
+
balance = 50.0;
|
|
4
|
+
transactions = [];
|
|
5
|
+
smsRentals = new Map();
|
|
6
|
+
esimOrders = new Map();
|
|
7
|
+
proxies = new Map();
|
|
8
|
+
deposits = new Map();
|
|
9
|
+
deductBalance(amount, type, description) {
|
|
10
|
+
if (this.balance < amount)
|
|
11
|
+
return false;
|
|
12
|
+
this.balance = Math.round((this.balance - amount) * 100) / 100;
|
|
13
|
+
this.transactions.push({
|
|
14
|
+
id: generateId("tx"),
|
|
15
|
+
type,
|
|
16
|
+
amount: -amount,
|
|
17
|
+
description,
|
|
18
|
+
createdAt: Date.now(),
|
|
19
|
+
});
|
|
20
|
+
return true;
|
|
21
|
+
}
|
|
22
|
+
addBalance(amount, type, description) {
|
|
23
|
+
this.balance = Math.round((this.balance + amount) * 100) / 100;
|
|
24
|
+
this.transactions.push({
|
|
25
|
+
id: generateId("tx"),
|
|
26
|
+
type,
|
|
27
|
+
amount,
|
|
28
|
+
description,
|
|
29
|
+
createdAt: Date.now(),
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
resolvePendingDeposits() {
|
|
33
|
+
const now = Date.now();
|
|
34
|
+
for (const deposit of this.deposits.values()) {
|
|
35
|
+
if (deposit.status === "pending" && now - deposit.createdAt > 5000) {
|
|
36
|
+
deposit.status = "completed";
|
|
37
|
+
this.addBalance(deposit.amount, "deposit", `Crypto deposit (${deposit.currency})`);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
export const state = new SandboxState();
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { state } from "../sandbox/state.js";
|
|
3
|
+
import { validateCountry, ToolError, generateId } from "../utils/validation.js";
|
|
4
|
+
import { formatUsd, formatGb, formatTimeRemaining } from "../utils/format.js";
|
|
5
|
+
import { searchPlans, getPlan } from "../mock-data/esim.js";
|
|
6
|
+
function errorResponse(message) {
|
|
7
|
+
return {
|
|
8
|
+
content: [{ type: "text", text: message }],
|
|
9
|
+
isError: true,
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
function formatData(gb) {
|
|
13
|
+
return gb >= 999 ? "Unlimited" : formatGb(gb);
|
|
14
|
+
}
|
|
15
|
+
export function registerEsimTools(server) {
|
|
16
|
+
server.tool("search_esim_plans", "Search available eSIM data plans. Filter by country, minimum duration, or minimum data amount.", {
|
|
17
|
+
country: z.string().describe("ISO 3166-1 alpha-2 country code (e.g., JP, US, GB)"),
|
|
18
|
+
duration: z
|
|
19
|
+
.number()
|
|
20
|
+
.min(1)
|
|
21
|
+
.optional()
|
|
22
|
+
.describe("Minimum plan duration in days"),
|
|
23
|
+
dataAmount: z
|
|
24
|
+
.number()
|
|
25
|
+
.min(1)
|
|
26
|
+
.optional()
|
|
27
|
+
.describe("Minimum data amount in GB"),
|
|
28
|
+
}, async ({ country, duration, dataAmount }) => {
|
|
29
|
+
try {
|
|
30
|
+
const validatedCountry = validateCountry(country);
|
|
31
|
+
const results = searchPlans(validatedCountry, duration, dataAmount);
|
|
32
|
+
if (results.length === 0) {
|
|
33
|
+
return errorResponse("No eSIM plans found matching your criteria. Try a different country or adjust filters.");
|
|
34
|
+
}
|
|
35
|
+
let text = `Found ${results.length} eSIM plan(s) for ${validatedCountry}:\n\n`;
|
|
36
|
+
for (const plan of results) {
|
|
37
|
+
text += ` ${plan.name} (${plan.planId})\n`;
|
|
38
|
+
text += ` Data: ${formatData(plan.data)}\n`;
|
|
39
|
+
text += ` Duration: ${plan.duration} days\n`;
|
|
40
|
+
text += ` Price: ${formatUsd(plan.price)}\n`;
|
|
41
|
+
text += ` Carrier: ${plan.carrier}\n`;
|
|
42
|
+
text += ` Routing: ${plan.routing}\n`;
|
|
43
|
+
text += ` Top-up: ${plan.topupAvailable ? `Yes (${formatUsd(plan.topupPrice)}/GB)` : "No"}\n\n`;
|
|
44
|
+
}
|
|
45
|
+
return { content: [{ type: "text", text }] };
|
|
46
|
+
}
|
|
47
|
+
catch (e) {
|
|
48
|
+
if (e instanceof ToolError)
|
|
49
|
+
return errorResponse(e.message);
|
|
50
|
+
throw e;
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
server.tool("get_esim_plan_details", "Get full details for a specific eSIM plan including APN settings and top-up availability.", {
|
|
54
|
+
planId: z.string().describe("Plan ID (e.g., 'esim_jp_3g_7d')"),
|
|
55
|
+
}, async ({ planId }) => {
|
|
56
|
+
try {
|
|
57
|
+
const plan = getPlan(planId);
|
|
58
|
+
if (!plan) {
|
|
59
|
+
return errorResponse(`Plan not found: ${planId}. Use search_esim_plans to browse available plans.`);
|
|
60
|
+
}
|
|
61
|
+
const text = [
|
|
62
|
+
`${plan.name}`,
|
|
63
|
+
``,
|
|
64
|
+
` Plan ID: ${plan.planId}`,
|
|
65
|
+
` Country: ${plan.country}`,
|
|
66
|
+
` Region: ${plan.region}`,
|
|
67
|
+
` Data: ${formatData(plan.data)}`,
|
|
68
|
+
` Duration: ${plan.duration} days`,
|
|
69
|
+
` Price: ${formatUsd(plan.price)}`,
|
|
70
|
+
` Carrier: ${plan.carrier}`,
|
|
71
|
+
` APN: ${plan.apn}`,
|
|
72
|
+
` Routing: ${plan.routing}`,
|
|
73
|
+
` Top-up: ${plan.topupAvailable ? `Available at ${formatUsd(plan.topupPrice)}/GB` : "Not available"}`,
|
|
74
|
+
].join("\n");
|
|
75
|
+
return { content: [{ type: "text", text }] };
|
|
76
|
+
}
|
|
77
|
+
catch (e) {
|
|
78
|
+
if (e instanceof ToolError)
|
|
79
|
+
return errorResponse(e.message);
|
|
80
|
+
throw e;
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
server.tool("purchase_esim", "Purchase an eSIM plan. Deducts cost from wallet and provides QR code for installation.", {
|
|
84
|
+
planId: z.string().describe("Plan ID to purchase (e.g., 'esim_jp_3g_7d')"),
|
|
85
|
+
}, async ({ planId }) => {
|
|
86
|
+
try {
|
|
87
|
+
const plan = getPlan(planId);
|
|
88
|
+
if (!plan) {
|
|
89
|
+
return errorResponse(`Plan not found: ${planId}. Use search_esim_plans to browse available plans.`);
|
|
90
|
+
}
|
|
91
|
+
if (!state.deductBalance(plan.price, "esim_purchase", `eSIM: ${plan.name}`)) {
|
|
92
|
+
return errorResponse(`Insufficient balance. Need ${formatUsd(plan.price)} but have ${formatUsd(state.balance)}. Use deposit to add funds.`);
|
|
93
|
+
}
|
|
94
|
+
const orderId = generateId("ord");
|
|
95
|
+
const now = Date.now();
|
|
96
|
+
state.esimOrders.set(orderId, {
|
|
97
|
+
orderId,
|
|
98
|
+
planId: plan.planId,
|
|
99
|
+
planName: plan.name,
|
|
100
|
+
country: plan.country,
|
|
101
|
+
dataTotal: plan.data,
|
|
102
|
+
dataUsed: 0,
|
|
103
|
+
status: "active",
|
|
104
|
+
qrUrl: `https://sandbox.voidmob.com/esim/qr/${orderId}`,
|
|
105
|
+
apn: plan.apn,
|
|
106
|
+
expiry: now + plan.duration * 24 * 60 * 60 * 1000,
|
|
107
|
+
createdAt: now,
|
|
108
|
+
price: plan.price,
|
|
109
|
+
});
|
|
110
|
+
const text = [
|
|
111
|
+
`eSIM purchased!`,
|
|
112
|
+
``,
|
|
113
|
+
` Order ID: ${orderId}`,
|
|
114
|
+
` Plan: ${plan.name}`,
|
|
115
|
+
` Data: ${formatData(plan.data)}`,
|
|
116
|
+
` Duration: ${plan.duration} days`,
|
|
117
|
+
` Cost: ${formatUsd(plan.price)}`,
|
|
118
|
+
` Carrier: ${plan.carrier}`,
|
|
119
|
+
` Routing: ${plan.routing}`,
|
|
120
|
+
``,
|
|
121
|
+
` QR Code: https://sandbox.voidmob.com/esim/qr/${orderId}`,
|
|
122
|
+
` APN: ${plan.apn}`,
|
|
123
|
+
``,
|
|
124
|
+
`Setup steps:`,
|
|
125
|
+
` 1. Scan the QR code with your device camera`,
|
|
126
|
+
` 2. Set APN to "${plan.apn}"`,
|
|
127
|
+
` 3. Enable the eSIM line in Settings`,
|
|
128
|
+
``,
|
|
129
|
+
`Use get_esim_usage with the order ID to check data consumption.`,
|
|
130
|
+
].join("\n");
|
|
131
|
+
return { content: [{ type: "text", text }] };
|
|
132
|
+
}
|
|
133
|
+
catch (e) {
|
|
134
|
+
if (e instanceof ToolError)
|
|
135
|
+
return errorResponse(e.message);
|
|
136
|
+
throw e;
|
|
137
|
+
}
|
|
138
|
+
});
|
|
139
|
+
server.tool("get_esim_usage", "Check data usage and status for an eSIM order. Shows data consumed, remaining, and time left.", {
|
|
140
|
+
orderId: z.string().describe("Order ID returned from purchase_esim"),
|
|
141
|
+
}, async ({ orderId }) => {
|
|
142
|
+
try {
|
|
143
|
+
const order = state.esimOrders.get(orderId);
|
|
144
|
+
if (!order) {
|
|
145
|
+
return errorResponse(`Order not found: ${orderId}`);
|
|
146
|
+
}
|
|
147
|
+
const now = Date.now();
|
|
148
|
+
if (order.status === "active" && now >= order.expiry) {
|
|
149
|
+
order.status = "expired";
|
|
150
|
+
}
|
|
151
|
+
// Simulate usage growth: ~0.1 GB/hour, capped at 95% of total
|
|
152
|
+
const hoursElapsed = (now - order.createdAt) / 3600000;
|
|
153
|
+
const simulatedUsage = hoursElapsed * 0.1;
|
|
154
|
+
const maxUsage = order.dataTotal * 0.95;
|
|
155
|
+
order.dataUsed = Math.round(Math.min(simulatedUsage, maxUsage) * 100) / 100;
|
|
156
|
+
const daysLeft = Math.max(0, Math.ceil((order.expiry - now) / 86400000));
|
|
157
|
+
const isUnlimited = order.dataTotal >= 999;
|
|
158
|
+
const dataRemaining = isUnlimited
|
|
159
|
+
? "Unlimited"
|
|
160
|
+
: formatGb(Math.max(0, order.dataTotal - order.dataUsed));
|
|
161
|
+
const text = [
|
|
162
|
+
`eSIM Usage — ${order.planName}`,
|
|
163
|
+
``,
|
|
164
|
+
` Order ID: ${order.orderId}`,
|
|
165
|
+
` Status: ${order.status}`,
|
|
166
|
+
` Data used: ${formatGb(order.dataUsed)}`,
|
|
167
|
+
` Data remaining: ${dataRemaining}`,
|
|
168
|
+
` Data total: ${formatData(order.dataTotal)}`,
|
|
169
|
+
` Days left: ${order.status === "expired" ? "Expired" : `${daysLeft} day(s)`}`,
|
|
170
|
+
` Expires: ${order.status === "expired" ? "Expired" : formatTimeRemaining(order.expiry)}`,
|
|
171
|
+
].join("\n");
|
|
172
|
+
return { content: [{ type: "text", text }] };
|
|
173
|
+
}
|
|
174
|
+
catch (e) {
|
|
175
|
+
if (e instanceof ToolError)
|
|
176
|
+
return errorResponse(e.message);
|
|
177
|
+
throw e;
|
|
178
|
+
}
|
|
179
|
+
});
|
|
180
|
+
server.tool("topup_esim", "Add more data to an active eSIM order. Only available for plans that support top-ups.", {
|
|
181
|
+
orderId: z.string().describe("Order ID to top up"),
|
|
182
|
+
dataAmount: z.number().positive().describe("Amount of data to add in GB"),
|
|
183
|
+
}, async ({ orderId, dataAmount }) => {
|
|
184
|
+
try {
|
|
185
|
+
const order = state.esimOrders.get(orderId);
|
|
186
|
+
if (!order) {
|
|
187
|
+
return errorResponse(`Order not found: ${orderId}`);
|
|
188
|
+
}
|
|
189
|
+
if (order.status !== "active") {
|
|
190
|
+
return errorResponse(`Order ${orderId} is ${order.status}. Only active orders can be topped up.`);
|
|
191
|
+
}
|
|
192
|
+
const plan = getPlan(order.planId);
|
|
193
|
+
if (!plan || !plan.topupAvailable) {
|
|
194
|
+
return errorResponse(`Top-up is not available for this plan (${order.planName}).`);
|
|
195
|
+
}
|
|
196
|
+
const cost = Math.round(dataAmount * plan.topupPrice * 100) / 100;
|
|
197
|
+
if (!state.deductBalance(cost, "topup", `eSIM top-up: +${formatGb(dataAmount)} for ${order.planName}`)) {
|
|
198
|
+
return errorResponse(`Insufficient balance. Need ${formatUsd(cost)} but have ${formatUsd(state.balance)}. Use deposit to add funds.`);
|
|
199
|
+
}
|
|
200
|
+
order.dataTotal = Math.round((order.dataTotal + dataAmount) * 100) / 100;
|
|
201
|
+
const text = [
|
|
202
|
+
`Top-up successful!`,
|
|
203
|
+
``,
|
|
204
|
+
` Order: ${order.orderId}`,
|
|
205
|
+
` Added: ${formatGb(dataAmount)}`,
|
|
206
|
+
` Cost: ${formatUsd(cost)}`,
|
|
207
|
+
` New total: ${formatData(order.dataTotal)}`,
|
|
208
|
+
` Balance: ${formatUsd(state.balance)}`,
|
|
209
|
+
].join("\n");
|
|
210
|
+
return { content: [{ type: "text", text }] };
|
|
211
|
+
}
|
|
212
|
+
catch (e) {
|
|
213
|
+
if (e instanceof ToolError)
|
|
214
|
+
return errorResponse(e.message);
|
|
215
|
+
throw e;
|
|
216
|
+
}
|
|
217
|
+
});
|
|
218
|
+
}
|