@ggboi360/mobile-dev-mcp 0.2.0 β 0.3.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/LICENSE +19 -18
- package/LICENSE-ELASTIC +10 -22
- package/README.md +51 -172
- package/dist/index.d.ts +12 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +714 -3394
- package/dist/index.js.map +1 -1
- package/dist/license.d.ts +11 -96
- package/dist/license.d.ts.map +1 -1
- package/dist/license.js +243 -570
- package/dist/license.js.map +1 -1
- package/dist/types.d.ts +52 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +50 -0
- package/dist/types.js.map +1 -0
- package/dist/utils.d.ts +72 -0
- package/dist/utils.d.ts.map +1 -0
- package/dist/utils.js +307 -0
- package/dist/utils.js.map +1 -0
- package/package.json +5 -3
- package/dist/license.test.d.ts +0 -2
- package/dist/license.test.d.ts.map +0 -1
- package/dist/license.test.js +0 -198
- package/dist/license.test.js.map +0 -1
- package/dist/tools.test.d.ts +0 -2
- package/dist/tools.test.d.ts.map +0 -1
- package/dist/tools.test.js +0 -337
- package/dist/tools.test.js.map +0 -1
package/dist/license.js
CHANGED
|
@@ -1,624 +1,297 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
*
|
|
4
|
-
* Tiers:
|
|
5
|
-
* - TRIAL: No license, 50 tool requests then blocked
|
|
6
|
-
* - BASIC ($6/mo): 17 core tools (Android + iOS basics), 50 line limits, 1 device
|
|
7
|
-
* - ADVANCED ($8/wk, $12/mo, $99/yr): All 56 tools, unlimited, multi-device
|
|
8
|
-
*
|
|
9
|
-
* Handles:
|
|
10
|
-
* - License key validation against API
|
|
11
|
-
* - Local caching of license status
|
|
12
|
-
* - Trial usage tracking
|
|
13
|
-
* - Graceful degradation when offline
|
|
14
|
-
* - Feature gating based on tier
|
|
15
|
-
*/
|
|
1
|
+
// Mobile Dev MCP - License Management
|
|
2
|
+
// Read-only debugging tool - simplified 2-tier system
|
|
16
3
|
import * as fs from "fs";
|
|
17
4
|
import * as path from "path";
|
|
18
5
|
import * as os from "os";
|
|
6
|
+
import * as https from "https";
|
|
19
7
|
import * as crypto from "crypto";
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
//
|
|
23
|
-
|
|
24
|
-
const
|
|
8
|
+
import { execSync } from "child_process";
|
|
9
|
+
import { TIER_LIMITS, FREE_TOOLS, ADVANCED_TOOLS } from "./types.js";
|
|
10
|
+
// Re-export for tests
|
|
11
|
+
export { TIER_LIMITS, FREE_TOOLS, ADVANCED_TOOLS };
|
|
12
|
+
const CONFIG_DIR = path.join(os.homedir(), ".mobile-dev-mcp");
|
|
25
13
|
const LICENSE_CACHE_FILE = path.join(CONFIG_DIR, "license.json");
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
const GRACE_PERIOD_MS = 7 * 24 * 60 * 60 * 1000; // 7 days
|
|
31
|
-
// Default API endpoint - UPDATE THIS after deploying your Cloudflare Worker!
|
|
32
|
-
const DEFAULT_API_ENDPOINT = "https://mobiledev-license-api.giladworkersdev.workers.dev";
|
|
33
|
-
// Trial settings
|
|
34
|
-
const TRIAL_LIMIT = 50; // Number of tool requests allowed in trial
|
|
35
|
-
// ============================================================================
|
|
36
|
-
// FEATURE TIERS
|
|
14
|
+
// License validation API (Cloudflare Worker)
|
|
15
|
+
const LICENSE_API_URL = "https://mobiledev-license-api.giladworkersdev.workers.dev/validate";
|
|
16
|
+
// Cache TTL: 1 hour (reduced from 24 hours for faster revocation)
|
|
17
|
+
const CACHE_TTL_MS = 60 * 60 * 1000;
|
|
37
18
|
// ============================================================================
|
|
38
|
-
//
|
|
39
|
-
export const BASIC_TOOLS = [
|
|
40
|
-
// Android tools
|
|
41
|
-
"get_metro_logs",
|
|
42
|
-
"get_adb_logs",
|
|
43
|
-
"screenshot_emulator",
|
|
44
|
-
"list_devices",
|
|
45
|
-
"check_metro_status",
|
|
46
|
-
"get_app_info",
|
|
47
|
-
"clear_app_data",
|
|
48
|
-
"restart_adb",
|
|
49
|
-
"get_device_info",
|
|
50
|
-
"start_metro_logging",
|
|
51
|
-
"stop_metro_logging",
|
|
52
|
-
// iOS Simulator tools
|
|
53
|
-
"list_ios_simulators",
|
|
54
|
-
"screenshot_ios_simulator",
|
|
55
|
-
"get_ios_simulator_logs",
|
|
56
|
-
"get_ios_simulator_info",
|
|
57
|
-
// License tools
|
|
58
|
-
"get_license_status",
|
|
59
|
-
"set_license_key",
|
|
60
|
-
];
|
|
61
|
-
// ADVANCED TIER ($8/wk, $12/mo, $99/yr) - Pro tools, only for Advanced subscribers
|
|
62
|
-
export const ADVANCED_TOOLS = [
|
|
63
|
-
// Android streaming & monitoring
|
|
64
|
-
"stream_adb_realtime",
|
|
65
|
-
"stop_adb_streaming",
|
|
66
|
-
"screenshot_history",
|
|
67
|
-
"watch_for_errors",
|
|
68
|
-
"multi_device_logs",
|
|
69
|
-
// Android interaction tools
|
|
70
|
-
"tap_screen",
|
|
71
|
-
"input_text",
|
|
72
|
-
"press_button",
|
|
73
|
-
"swipe_screen",
|
|
74
|
-
"launch_app",
|
|
75
|
-
"install_apk",
|
|
76
|
-
// iOS Simulator advanced tools
|
|
77
|
-
"boot_ios_simulator",
|
|
78
|
-
"shutdown_ios_simulator",
|
|
79
|
-
"install_ios_app",
|
|
80
|
-
"launch_ios_app",
|
|
81
|
-
"terminate_ios_app",
|
|
82
|
-
"ios_open_url",
|
|
83
|
-
"ios_push_notification",
|
|
84
|
-
"ios_set_location",
|
|
85
|
-
// React DevTools integration
|
|
86
|
-
"setup_react_devtools",
|
|
87
|
-
"check_devtools_connection",
|
|
88
|
-
"get_react_component_tree",
|
|
89
|
-
"inspect_react_component",
|
|
90
|
-
"search_react_components",
|
|
91
|
-
// Network inspection
|
|
92
|
-
"get_network_requests",
|
|
93
|
-
"start_network_monitoring",
|
|
94
|
-
"stop_network_monitoring",
|
|
95
|
-
"get_network_stats",
|
|
96
|
-
"analyze_request",
|
|
97
|
-
// Expo DevTools integration
|
|
98
|
-
"check_expo_status",
|
|
99
|
-
"get_expo_config",
|
|
100
|
-
"expo_dev_menu",
|
|
101
|
-
"expo_reload",
|
|
102
|
-
"get_eas_builds",
|
|
103
|
-
// Performance metrics
|
|
104
|
-
"get_cpu_usage",
|
|
105
|
-
"get_memory_usage",
|
|
106
|
-
"get_fps_stats",
|
|
107
|
-
"get_battery_stats",
|
|
108
|
-
"get_performance_snapshot",
|
|
109
|
-
];
|
|
110
|
-
// Limits for each tier
|
|
111
|
-
export const TIER_LIMITS = {
|
|
112
|
-
trial: {
|
|
113
|
-
maxLogLines: 50, // Same as Basic during trial
|
|
114
|
-
maxDevices: 1,
|
|
115
|
-
screenshotHistory: 0,
|
|
116
|
-
},
|
|
117
|
-
basic: {
|
|
118
|
-
maxLogLines: 50,
|
|
119
|
-
maxDevices: 1,
|
|
120
|
-
screenshotHistory: 0,
|
|
121
|
-
},
|
|
122
|
-
advanced: {
|
|
123
|
-
maxLogLines: Infinity,
|
|
124
|
-
maxDevices: 3,
|
|
125
|
-
screenshotHistory: 20,
|
|
126
|
-
},
|
|
127
|
-
};
|
|
19
|
+
// MACHINE ID
|
|
128
20
|
// ============================================================================
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
fs.mkdirSync(CONFIG_DIR, { recursive: true });
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
export function loadConfig() {
|
|
137
|
-
ensureConfigDir();
|
|
138
|
-
const defaultConfig = {
|
|
139
|
-
metroPort: 8081,
|
|
140
|
-
logBufferSize: 100,
|
|
141
|
-
apiEndpoint: DEFAULT_API_ENDPOINT,
|
|
142
|
-
};
|
|
143
|
-
if (!fs.existsSync(CONFIG_FILE)) {
|
|
144
|
-
saveConfig(defaultConfig);
|
|
145
|
-
return defaultConfig;
|
|
146
|
-
}
|
|
21
|
+
let cachedMachineId = null;
|
|
22
|
+
function getMachineId() {
|
|
23
|
+
if (cachedMachineId)
|
|
24
|
+
return cachedMachineId;
|
|
147
25
|
try {
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
26
|
+
if (process.platform === "win32") {
|
|
27
|
+
const output = execSync("wmic csproduct get uuid", { encoding: "utf-8" });
|
|
28
|
+
const lines = output.trim().split("\n");
|
|
29
|
+
if (lines.length > 1) {
|
|
30
|
+
cachedMachineId = lines[1].trim();
|
|
31
|
+
return cachedMachineId;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
else if (process.platform === "linux") {
|
|
35
|
+
if (fs.existsSync("/etc/machine-id")) {
|
|
36
|
+
cachedMachineId = fs.readFileSync("/etc/machine-id", "utf-8").trim();
|
|
37
|
+
return cachedMachineId;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
else if (process.platform === "darwin") {
|
|
41
|
+
const output = execSync("ioreg -rd1 -c IOPlatformExpertDevice | grep IOPlatformUUID", { encoding: "utf-8" });
|
|
42
|
+
const match = output.match(/"([A-F0-9-]+)"/);
|
|
43
|
+
if (match) {
|
|
44
|
+
cachedMachineId = match[1];
|
|
45
|
+
return cachedMachineId;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
151
48
|
}
|
|
152
49
|
catch {
|
|
153
|
-
|
|
50
|
+
// Ignore errors
|
|
154
51
|
}
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
ensureConfigDir();
|
|
158
|
-
fs.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2));
|
|
159
|
-
}
|
|
160
|
-
export function setLicenseKey(key) {
|
|
161
|
-
const config = loadConfig();
|
|
162
|
-
config.licenseKey = key;
|
|
163
|
-
saveConfig(config);
|
|
164
|
-
// Clear cached license to force revalidation
|
|
165
|
-
clearLicenseCache();
|
|
52
|
+
cachedMachineId = os.hostname();
|
|
53
|
+
return cachedMachineId;
|
|
166
54
|
}
|
|
167
55
|
// ============================================================================
|
|
168
|
-
//
|
|
56
|
+
// CACHE INTEGRITY (HMAC signing)
|
|
169
57
|
// ============================================================================
|
|
170
|
-
function
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
os.platform(),
|
|
174
|
-
os.arch(),
|
|
175
|
-
os.cpus()[0]?.model || "unknown",
|
|
176
|
-
].join("|");
|
|
177
|
-
return crypto.createHash("sha256").update(info).digest("hex").substring(0, 32);
|
|
58
|
+
function getCacheSecret() {
|
|
59
|
+
// Use machine ID as part of secret to prevent cache file copying between machines
|
|
60
|
+
return `mobiledev-${getMachineId()}-cache-v1`;
|
|
178
61
|
}
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
62
|
+
function signCacheData(data) {
|
|
63
|
+
const payload = JSON.stringify(data);
|
|
64
|
+
return crypto.createHmac("sha256", getCacheSecret()).update(payload).digest("hex");
|
|
65
|
+
}
|
|
66
|
+
function verifyCacheSignature(data, signature) {
|
|
67
|
+
const expected = signCacheData(data);
|
|
68
|
+
const sigBuffer = Buffer.from(signature);
|
|
69
|
+
const expBuffer = Buffer.from(expected);
|
|
70
|
+
// Length check before timingSafeEqual to prevent length-based timing leaks
|
|
71
|
+
// (timingSafeEqual throws if lengths differ, which is itself a timing leak)
|
|
72
|
+
if (sigBuffer.length !== expBuffer.length)
|
|
73
|
+
return false;
|
|
74
|
+
return crypto.timingSafeEqual(sigBuffer, expBuffer);
|
|
75
|
+
}
|
|
76
|
+
function loadCachedLicense() {
|
|
186
77
|
try {
|
|
187
|
-
|
|
188
|
-
|
|
78
|
+
if (fs.existsSync(LICENSE_CACHE_FILE)) {
|
|
79
|
+
const raw = JSON.parse(fs.readFileSync(LICENSE_CACHE_FILE, "utf-8"));
|
|
80
|
+
// Verify signature to prevent tampering
|
|
81
|
+
if (raw.data && raw.signature) {
|
|
82
|
+
if (verifyCacheSignature(raw.data, raw.signature)) {
|
|
83
|
+
return raw.data;
|
|
84
|
+
}
|
|
85
|
+
else {
|
|
86
|
+
// Signature mismatch - delete corrupted cache
|
|
87
|
+
console.error("License cache signature mismatch - clearing cache");
|
|
88
|
+
fs.unlinkSync(LICENSE_CACHE_FILE);
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
// Legacy cache without signature - delete it
|
|
93
|
+
fs.unlinkSync(LICENSE_CACHE_FILE);
|
|
94
|
+
}
|
|
189
95
|
}
|
|
190
96
|
catch {
|
|
191
|
-
|
|
97
|
+
// Ignore errors
|
|
192
98
|
}
|
|
99
|
+
return null;
|
|
193
100
|
}
|
|
194
|
-
function saveLicenseCache(
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
101
|
+
function saveLicenseCache(data) {
|
|
102
|
+
try {
|
|
103
|
+
if (!fs.existsSync(CONFIG_DIR)) {
|
|
104
|
+
fs.mkdirSync(CONFIG_DIR, { recursive: true, mode: 0o700 });
|
|
105
|
+
}
|
|
106
|
+
const signature = signCacheData(data);
|
|
107
|
+
const cache = { data, signature };
|
|
108
|
+
// Write with restricted permissions (owner read/write only)
|
|
109
|
+
fs.writeFileSync(LICENSE_CACHE_FILE, JSON.stringify(cache, null, 2), { mode: 0o600 });
|
|
110
|
+
}
|
|
111
|
+
catch {
|
|
112
|
+
// Ignore write errors
|
|
201
113
|
}
|
|
202
|
-
}
|
|
203
|
-
function isCacheFresh(license) {
|
|
204
|
-
const validatedAt = new Date(license.validatedAt).getTime();
|
|
205
|
-
const now = Date.now();
|
|
206
|
-
return now - validatedAt < CACHE_TTL_MS;
|
|
207
|
-
}
|
|
208
|
-
function isWithinGracePeriod(license) {
|
|
209
|
-
const validatedAt = new Date(license.validatedAt).getTime();
|
|
210
|
-
const now = Date.now();
|
|
211
|
-
return now - validatedAt < GRACE_PERIOD_MS;
|
|
212
114
|
}
|
|
213
115
|
// ============================================================================
|
|
214
|
-
//
|
|
116
|
+
// LICENSE VALIDATION
|
|
215
117
|
// ============================================================================
|
|
216
|
-
function
|
|
217
|
-
if (!fs.existsSync(TRIAL_FILE)) {
|
|
218
|
-
return null;
|
|
219
|
-
}
|
|
118
|
+
export async function checkLicense() {
|
|
220
119
|
try {
|
|
221
|
-
const
|
|
222
|
-
|
|
120
|
+
const cached = loadCachedLicense();
|
|
121
|
+
if (cached && cached.licenseKey) {
|
|
122
|
+
const now = Date.now();
|
|
123
|
+
const lastCheck = cached.lastValidated || 0;
|
|
124
|
+
// Check if cache is still valid (1 hour TTL)
|
|
125
|
+
if (now - lastCheck < CACHE_TTL_MS) {
|
|
126
|
+
return {
|
|
127
|
+
tier: cached.tier,
|
|
128
|
+
valid: true,
|
|
129
|
+
licenseKey: cached.licenseKey,
|
|
130
|
+
expiresAt: cached.expiresAt,
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
// Re-validate with API
|
|
134
|
+
const validated = await validateWithLemonSqueezy(cached.licenseKey);
|
|
135
|
+
if (validated) {
|
|
136
|
+
return validated;
|
|
137
|
+
}
|
|
138
|
+
// Validation failed - clear cache and fall back to free
|
|
139
|
+
try {
|
|
140
|
+
fs.unlinkSync(LICENSE_CACHE_FILE);
|
|
141
|
+
}
|
|
142
|
+
catch {
|
|
143
|
+
// Ignore
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
return { tier: "free", valid: true };
|
|
223
147
|
}
|
|
224
148
|
catch {
|
|
225
|
-
return
|
|
226
|
-
}
|
|
227
|
-
}
|
|
228
|
-
function saveTrialInfo(trial) {
|
|
229
|
-
ensureConfigDir();
|
|
230
|
-
fs.writeFileSync(TRIAL_FILE, JSON.stringify(trial, null, 2));
|
|
231
|
-
}
|
|
232
|
-
function getMachineIdForTrial() {
|
|
233
|
-
const info = [
|
|
234
|
-
os.hostname(),
|
|
235
|
-
os.platform(),
|
|
236
|
-
os.arch(),
|
|
237
|
-
os.cpus()[0]?.model || "unknown",
|
|
238
|
-
].join("|");
|
|
239
|
-
return crypto.createHash("sha256").update(info).digest("hex").substring(0, 32);
|
|
240
|
-
}
|
|
241
|
-
export function getTrialStatus() {
|
|
242
|
-
const trial = loadTrialInfo();
|
|
243
|
-
if (!trial) {
|
|
244
|
-
return { remaining: TRIAL_LIMIT, used: 0, expired: false };
|
|
245
|
-
}
|
|
246
|
-
// Verify machine ID matches (prevent copying trial.json to new machines)
|
|
247
|
-
const currentMachineId = getMachineIdForTrial();
|
|
248
|
-
if (trial.machineId !== currentMachineId) {
|
|
249
|
-
return { remaining: TRIAL_LIMIT, used: 0, expired: false };
|
|
250
|
-
}
|
|
251
|
-
const remaining = Math.max(0, TRIAL_LIMIT - trial.usageCount);
|
|
252
|
-
return {
|
|
253
|
-
remaining,
|
|
254
|
-
used: trial.usageCount,
|
|
255
|
-
expired: remaining === 0,
|
|
256
|
-
};
|
|
257
|
-
}
|
|
258
|
-
export function incrementTrialUsage() {
|
|
259
|
-
const machineId = getMachineIdForTrial();
|
|
260
|
-
let trial = loadTrialInfo();
|
|
261
|
-
// Initialize trial if needed
|
|
262
|
-
if (!trial || trial.machineId !== machineId) {
|
|
263
|
-
trial = {
|
|
264
|
-
usageCount: 0,
|
|
265
|
-
firstUsedAt: new Date().toISOString(),
|
|
266
|
-
lastUsedAt: new Date().toISOString(),
|
|
267
|
-
machineId,
|
|
268
|
-
};
|
|
269
|
-
}
|
|
270
|
-
// Check if trial expired
|
|
271
|
-
if (trial.usageCount >= TRIAL_LIMIT) {
|
|
272
|
-
return {
|
|
273
|
-
allowed: false,
|
|
274
|
-
remaining: 0,
|
|
275
|
-
message: `π Trial expired! You've used all ${TRIAL_LIMIT} trial requests.
|
|
276
|
-
|
|
277
|
-
To continue using Mobile Dev MCP, purchase a license:
|
|
278
|
-
|
|
279
|
-
βββββββββββββββββββββββββββββββββββββββββββ
|
|
280
|
-
β Basic Solo $6/month β
|
|
281
|
-
β β 17 core tools, 50 log lines β
|
|
282
|
-
βββββββββββββββββββββββββββββββββββββββββββ€
|
|
283
|
-
β Advanced Solo $12/month (or $99/year) β
|
|
284
|
-
β β All 56 tools, unlimited logs β
|
|
285
|
-
β β Real-time streaming, multi-device β
|
|
286
|
-
βββββββββββββββββββββββββββββββββββββββββββ
|
|
287
|
-
|
|
288
|
-
Purchase at: https://mobile-dev-mcp.com
|
|
289
|
-
Then use 'set_license_key' to activate.`,
|
|
290
|
-
};
|
|
149
|
+
return { tier: "free", valid: true };
|
|
291
150
|
}
|
|
292
|
-
// Increment usage
|
|
293
|
-
trial.usageCount++;
|
|
294
|
-
trial.lastUsedAt = new Date().toISOString();
|
|
295
|
-
saveTrialInfo(trial);
|
|
296
|
-
const remaining = TRIAL_LIMIT - trial.usageCount;
|
|
297
|
-
return {
|
|
298
|
-
allowed: true,
|
|
299
|
-
remaining,
|
|
300
|
-
};
|
|
301
151
|
}
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
152
|
+
async function validateWithLemonSqueezy(licenseKey) {
|
|
153
|
+
return new Promise((resolve) => {
|
|
154
|
+
const machineId = getMachineId();
|
|
155
|
+
const postData = JSON.stringify({
|
|
156
|
+
license_key: licenseKey,
|
|
157
|
+
instance_id: machineId,
|
|
158
|
+
});
|
|
159
|
+
const url = new URL(LICENSE_API_URL);
|
|
160
|
+
const req = https.request({
|
|
161
|
+
hostname: url.hostname,
|
|
162
|
+
path: url.pathname,
|
|
308
163
|
method: "POST",
|
|
309
164
|
headers: {
|
|
310
165
|
"Content-Type": "application/json",
|
|
166
|
+
"Content-Length": postData.length,
|
|
311
167
|
},
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
168
|
+
}, (res) => {
|
|
169
|
+
let data = "";
|
|
170
|
+
const MAX_RESPONSE_SIZE = 1024 * 1024; // 1MB limit to prevent DoS
|
|
171
|
+
res.on("data", (chunk) => {
|
|
172
|
+
data += chunk;
|
|
173
|
+
// Abort if response is too large (potential DoS)
|
|
174
|
+
if (data.length > MAX_RESPONSE_SIZE) {
|
|
175
|
+
req.destroy();
|
|
176
|
+
resolve(null);
|
|
177
|
+
}
|
|
178
|
+
});
|
|
179
|
+
res.on("end", () => {
|
|
180
|
+
try {
|
|
181
|
+
const response = JSON.parse(data);
|
|
182
|
+
if (response.valid) {
|
|
183
|
+
const tier = mapProductToTier(response.meta?.product_name || "");
|
|
184
|
+
const info = {
|
|
185
|
+
tier,
|
|
186
|
+
valid: true,
|
|
187
|
+
licenseKey,
|
|
188
|
+
expiresAt: response.license_key?.expires_at,
|
|
189
|
+
};
|
|
190
|
+
saveLicenseCache({
|
|
191
|
+
tier,
|
|
192
|
+
licenseKey,
|
|
193
|
+
expiresAt: response.license_key?.expires_at,
|
|
194
|
+
lastValidated: Date.now(),
|
|
195
|
+
});
|
|
196
|
+
resolve(info);
|
|
197
|
+
}
|
|
198
|
+
else {
|
|
199
|
+
resolve(null);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
catch {
|
|
203
|
+
resolve(null);
|
|
204
|
+
}
|
|
205
|
+
});
|
|
316
206
|
});
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
// Check variant name for tier hints
|
|
326
|
-
const variantName = data.meta?.variant_name?.toLowerCase() || "";
|
|
327
|
-
const productName = data.meta?.product_name?.toLowerCase() || "";
|
|
328
|
-
if (variantName.includes("advanced") ||
|
|
329
|
-
productName.includes("advanced")) {
|
|
330
|
-
tier = "advanced";
|
|
331
|
-
}
|
|
332
|
-
else if (variantName.includes("basic") ||
|
|
333
|
-
productName.includes("basic")) {
|
|
334
|
-
tier = "basic";
|
|
335
|
-
}
|
|
336
|
-
return {
|
|
337
|
-
key: licenseKey,
|
|
338
|
-
valid: true,
|
|
339
|
-
tier,
|
|
340
|
-
email: data.meta?.customer_email,
|
|
341
|
-
validatedAt: new Date().toISOString(),
|
|
342
|
-
expiresAt: data.license_key?.expires_at || undefined,
|
|
343
|
-
};
|
|
344
|
-
}
|
|
345
|
-
return {
|
|
346
|
-
key: licenseKey,
|
|
347
|
-
valid: false,
|
|
348
|
-
tier: "trial",
|
|
349
|
-
validatedAt: new Date().toISOString(),
|
|
350
|
-
};
|
|
351
|
-
}
|
|
352
|
-
catch (error) {
|
|
353
|
-
console.error("License validation API error:", error);
|
|
354
|
-
return null;
|
|
355
|
-
}
|
|
207
|
+
req.on("error", () => resolve(null));
|
|
208
|
+
req.setTimeout(5000, () => {
|
|
209
|
+
req.destroy();
|
|
210
|
+
resolve(null);
|
|
211
|
+
});
|
|
212
|
+
req.write(postData);
|
|
213
|
+
req.end();
|
|
214
|
+
});
|
|
356
215
|
}
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
//
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
// Return cached result if we've already checked this session
|
|
363
|
-
if (cachedLicenseResult) {
|
|
364
|
-
return cachedLicenseResult;
|
|
365
|
-
}
|
|
366
|
-
const config = loadConfig();
|
|
367
|
-
// No license key configured - trial mode
|
|
368
|
-
if (!config.licenseKey) {
|
|
369
|
-
cachedLicenseResult = {
|
|
370
|
-
key: "",
|
|
371
|
-
valid: false,
|
|
372
|
-
tier: "trial",
|
|
373
|
-
validatedAt: new Date().toISOString(),
|
|
374
|
-
};
|
|
375
|
-
return cachedLicenseResult;
|
|
376
|
-
}
|
|
377
|
-
// Check local cache first
|
|
378
|
-
const cached = loadLicenseCache();
|
|
379
|
-
if (cached && cached.key === config.licenseKey) {
|
|
380
|
-
if (cached.valid && isCacheFresh(cached)) {
|
|
381
|
-
cachedLicenseResult = cached;
|
|
382
|
-
return cached;
|
|
383
|
-
}
|
|
384
|
-
}
|
|
385
|
-
// Try to validate with API
|
|
386
|
-
const apiResult = await validateWithApi(config.licenseKey, config.apiEndpoint);
|
|
387
|
-
if (apiResult) {
|
|
388
|
-
saveLicenseCache(apiResult);
|
|
389
|
-
cachedLicenseResult = apiResult;
|
|
390
|
-
return apiResult;
|
|
216
|
+
function mapProductToTier(productName) {
|
|
217
|
+
const name = productName.toLowerCase();
|
|
218
|
+
// Any paid product maps to advanced tier
|
|
219
|
+
if (name.includes("advanced") || name.includes("pro") || name.includes("basic")) {
|
|
220
|
+
return "advanced";
|
|
391
221
|
}
|
|
392
|
-
|
|
393
|
-
if (cached && cached.valid && isWithinGracePeriod(cached)) {
|
|
394
|
-
console.error("License API unreachable, using cached license (grace period)");
|
|
395
|
-
cachedLicenseResult = cached;
|
|
396
|
-
return cached;
|
|
397
|
-
}
|
|
398
|
-
// No valid cache, API down - fall back to trial
|
|
399
|
-
cachedLicenseResult = {
|
|
400
|
-
key: config.licenseKey,
|
|
401
|
-
valid: false,
|
|
402
|
-
tier: "trial",
|
|
403
|
-
validatedAt: new Date().toISOString(),
|
|
404
|
-
};
|
|
405
|
-
return cachedLicenseResult;
|
|
222
|
+
return "free";
|
|
406
223
|
}
|
|
407
224
|
// ============================================================================
|
|
408
|
-
//
|
|
225
|
+
// TOOL ACCESS CONTROL
|
|
409
226
|
// ============================================================================
|
|
410
|
-
export function
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
if (isAdvancedTool(toolName)) {
|
|
420
|
-
return license.valid && license.tier === "advanced";
|
|
421
|
-
}
|
|
422
|
-
// Basic tools require Basic or Advanced tier
|
|
423
|
-
if (isBasicTool(toolName)) {
|
|
424
|
-
return license.valid && (license.tier === "basic" || license.tier === "advanced");
|
|
425
|
-
}
|
|
426
|
-
// Unknown tool - allow (might be a new tool)
|
|
427
|
-
return true;
|
|
428
|
-
}
|
|
429
|
-
export async function requireAdvanced(toolName) {
|
|
430
|
-
const license = await checkLicense();
|
|
431
|
-
// Advanced license holders always allowed
|
|
432
|
-
if (license.valid && license.tier === "advanced") {
|
|
433
|
-
return { allowed: true };
|
|
434
|
-
}
|
|
435
|
-
// Trial users can try Advanced tools (uses trial quota)
|
|
436
|
-
if (!license.valid || license.tier === "trial") {
|
|
437
|
-
const trialResult = incrementTrialUsage();
|
|
438
|
-
if (!trialResult.allowed) {
|
|
439
|
-
return {
|
|
440
|
-
allowed: false,
|
|
441
|
-
message: trialResult.message,
|
|
442
|
-
};
|
|
443
|
-
}
|
|
444
|
-
// Trial still has requests - allow with reminder
|
|
445
|
-
if (trialResult.remaining <= 10) {
|
|
446
|
-
return {
|
|
447
|
-
allowed: true,
|
|
448
|
-
message: `β οΈ Trial: ${trialResult.remaining} requests remaining. This is an Advanced feature - upgrade to keep using it!`,
|
|
449
|
-
};
|
|
227
|
+
export function canAccessTool(toolName, tier) {
|
|
228
|
+
// Free tools are always accessible
|
|
229
|
+
if (FREE_TOOLS.includes(toolName)) {
|
|
230
|
+
return true;
|
|
231
|
+
}
|
|
232
|
+
// Advanced tools require advanced tier
|
|
233
|
+
if (tier === "advanced") {
|
|
234
|
+
if (ADVANCED_TOOLS.includes(toolName)) {
|
|
235
|
+
return true;
|
|
450
236
|
}
|
|
451
|
-
return { allowed: true };
|
|
452
237
|
}
|
|
453
|
-
|
|
454
|
-
return {
|
|
455
|
-
allowed: false,
|
|
456
|
-
message: `π "${toolName}" requires Advanced tier.
|
|
457
|
-
|
|
458
|
-
Your current tier: BASIC
|
|
459
|
-
|
|
460
|
-
Upgrade to Advanced for:
|
|
461
|
-
- Real-time log streaming
|
|
462
|
-
- Screenshot history
|
|
463
|
-
- Multi-device support (3 devices)
|
|
464
|
-
- Error watching
|
|
465
|
-
- Unlimited log lines
|
|
466
|
-
- Device interaction (tap, type, swipe)
|
|
467
|
-
|
|
468
|
-
Pricing: $8/week, $12/month, or $99/year
|
|
469
|
-
|
|
470
|
-
Upgrade at: https://mobile-dev-mcp.com`,
|
|
471
|
-
};
|
|
238
|
+
return false;
|
|
472
239
|
}
|
|
473
|
-
export
|
|
474
|
-
|
|
475
|
-
// Licensed users can always use basic tools
|
|
476
|
-
if (license.valid) {
|
|
477
|
-
return { allowed: true };
|
|
478
|
-
}
|
|
479
|
-
// Trial users - check trial status
|
|
480
|
-
const trialResult = incrementTrialUsage();
|
|
481
|
-
if (!trialResult.allowed) {
|
|
482
|
-
return {
|
|
483
|
-
allowed: false,
|
|
484
|
-
message: trialResult.message,
|
|
485
|
-
};
|
|
486
|
-
}
|
|
487
|
-
// Trial still has requests - allow with reminder
|
|
488
|
-
if (trialResult.remaining <= 10) {
|
|
489
|
-
return {
|
|
490
|
-
allowed: true,
|
|
491
|
-
message: `β οΈ Trial: ${trialResult.remaining} requests remaining. Purchase a license to continue uninterrupted.`,
|
|
492
|
-
};
|
|
493
|
-
}
|
|
494
|
-
return { allowed: true };
|
|
240
|
+
export function isFreeTool(toolName) {
|
|
241
|
+
return FREE_TOOLS.includes(toolName);
|
|
495
242
|
}
|
|
496
|
-
export function
|
|
497
|
-
return
|
|
243
|
+
export function isAdvancedOnlyTool(toolName) {
|
|
244
|
+
return ADVANCED_TOOLS.includes(toolName) && !FREE_TOOLS.includes(toolName);
|
|
498
245
|
}
|
|
499
|
-
export
|
|
500
|
-
|
|
501
|
-
|
|
246
|
+
export function getMaxLogLines(tier) {
|
|
247
|
+
return TIER_LIMITS[tier]?.maxLogLines || 50;
|
|
248
|
+
}
|
|
249
|
+
export function getMaxDevices(tier) {
|
|
250
|
+
return TIER_LIMITS[tier]?.maxDevices || 1;
|
|
502
251
|
}
|
|
503
252
|
// ============================================================================
|
|
504
|
-
// LICENSE
|
|
253
|
+
// LICENSE TOOLS (exposed via MCP)
|
|
505
254
|
// ============================================================================
|
|
506
255
|
export async function getLicenseStatus() {
|
|
507
256
|
const license = await checkLicense();
|
|
508
|
-
const config = loadConfig();
|
|
509
|
-
if (!config.licenseKey) {
|
|
510
|
-
const trialStatus = getTrialStatus();
|
|
511
|
-
if (trialStatus.expired) {
|
|
512
|
-
return `π License Status: TRIAL EXPIRED
|
|
513
|
-
|
|
514
|
-
You've used all ${TRIAL_LIMIT} trial requests.
|
|
515
|
-
|
|
516
|
-
βββββββββββββββββββββββββββββββββββββββββββ
|
|
517
|
-
β Basic Solo $6/mo β Core tools β
|
|
518
|
-
β Advanced Solo $12/mo β All featuresβ
|
|
519
|
-
β $8/wk or $99/yr β
|
|
520
|
-
βββββββββββββββββββββββββββββββββββββββββββ
|
|
521
|
-
|
|
522
|
-
Purchase at: https://mobile-dev-mcp.com
|
|
523
|
-
Then use 'set_license_key' to activate.`;
|
|
524
|
-
}
|
|
525
|
-
return `π License Status: TRIAL
|
|
526
|
-
|
|
527
|
-
Trial requests remaining: ${trialStatus.remaining}/${TRIAL_LIMIT}
|
|
528
|
-
Trial requests used: ${trialStatus.used}
|
|
529
|
-
|
|
530
|
-
βββββββββββββββββββββββββββββββββββββββββββ
|
|
531
|
-
β Basic Solo $6/mo β Core tools β
|
|
532
|
-
β Advanced Solo $12/mo β All featuresβ
|
|
533
|
-
β $8/wk or $99/yr β
|
|
534
|
-
βββββββββββββββββββββββββββββββββββββββββββ
|
|
535
|
-
|
|
536
|
-
Purchase at: https://mobile-dev-mcp.com
|
|
537
|
-
Then use 'set_license_key' to activate.`;
|
|
538
|
-
}
|
|
539
|
-
if (!license.valid) {
|
|
540
|
-
return `π License Status: INVALID
|
|
541
|
-
|
|
542
|
-
License key: ${maskLicenseKey(config.licenseKey)}
|
|
543
|
-
Status: Invalid or expired
|
|
544
|
-
|
|
545
|
-
Please check your license key or purchase a new one.
|
|
546
|
-
https://mobile-dev-mcp.com`;
|
|
547
|
-
}
|
|
548
|
-
const tierEmoji = license.tier === "advanced" ? "β" : "β";
|
|
549
257
|
const limits = TIER_LIMITS[license.tier];
|
|
550
|
-
return
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
- Max log lines: ${limits.maxLogLines === Infinity ? "Unlimited" : limits.maxLogLines}
|
|
559
|
-
- Max devices: ${limits.maxDevices}
|
|
560
|
-
- Screenshot history: ${limits.screenshotHistory || "Not available"}
|
|
561
|
-
|
|
562
|
-
${license.tier === "basic" ? "\nπ‘ Upgrade to Advanced for real-time streaming & multi-device support!" : "All features unlocked! π"}`;
|
|
563
|
-
}
|
|
564
|
-
function maskLicenseKey(key) {
|
|
565
|
-
if (key.length < 8)
|
|
566
|
-
return "****";
|
|
567
|
-
return key.substring(0, 4) + "****" + key.substring(key.length - 4);
|
|
568
|
-
}
|
|
569
|
-
export const licenseTools = [
|
|
570
|
-
{
|
|
571
|
-
name: "get_license_status",
|
|
572
|
-
description: "Check your current license status and tier (free/basic/advanced)",
|
|
573
|
-
inputSchema: {
|
|
574
|
-
type: "object",
|
|
575
|
-
properties: {},
|
|
576
|
-
},
|
|
577
|
-
},
|
|
578
|
-
{
|
|
579
|
-
name: "set_license_key",
|
|
580
|
-
description: "Set or update your license key to unlock paid features",
|
|
581
|
-
inputSchema: {
|
|
582
|
-
type: "object",
|
|
583
|
-
properties: {
|
|
584
|
-
licenseKey: {
|
|
585
|
-
type: "string",
|
|
586
|
-
description: "Your license key from mobile-dev-mcp.com",
|
|
587
|
-
},
|
|
588
|
-
},
|
|
589
|
-
required: ["licenseKey"],
|
|
258
|
+
return JSON.stringify({
|
|
259
|
+
tier: license.tier.toUpperCase(),
|
|
260
|
+
valid: license.valid,
|
|
261
|
+
expiresAt: license.expiresAt || "N/A",
|
|
262
|
+
features: {
|
|
263
|
+
maxLogLines: limits?.maxLogLines || 50,
|
|
264
|
+
maxDevices: limits?.maxDevices || 1,
|
|
265
|
+
tools: license.tier === "advanced" ? 21 : 8,
|
|
590
266
|
},
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
}
|
|
614
|
-
|
|
615
|
-
return `β License key is invalid or expired.
|
|
616
|
-
|
|
617
|
-
Please check your key or contact support.
|
|
618
|
-
https://mobile-dev-mcp.com`;
|
|
619
|
-
}
|
|
620
|
-
default:
|
|
621
|
-
return `Unknown license tool: ${name}`;
|
|
267
|
+
upgrade: license.tier === "free" ? {
|
|
268
|
+
url: "https://mobiledevmcp.dev/pricing",
|
|
269
|
+
price: "$18/month",
|
|
270
|
+
features: ["UI inspection", "Screen analysis", "Multi-device support"],
|
|
271
|
+
} : null,
|
|
272
|
+
}, null, 2);
|
|
273
|
+
}
|
|
274
|
+
export async function setLicenseKey(licenseKey) {
|
|
275
|
+
if (!licenseKey || licenseKey.trim() === "") {
|
|
276
|
+
return JSON.stringify({ success: false, error: "License key is required" });
|
|
277
|
+
}
|
|
278
|
+
// Basic format validation
|
|
279
|
+
const key = licenseKey.trim();
|
|
280
|
+
if (key.length < 10 || key.length > 100) {
|
|
281
|
+
return JSON.stringify({ success: false, error: "Invalid license key format" });
|
|
282
|
+
}
|
|
283
|
+
const validated = await validateWithLemonSqueezy(key);
|
|
284
|
+
if (validated) {
|
|
285
|
+
return JSON.stringify({
|
|
286
|
+
success: true,
|
|
287
|
+
tier: validated.tier.toUpperCase(),
|
|
288
|
+
expiresAt: validated.expiresAt || "N/A",
|
|
289
|
+
message: `License activated! You now have ${validated.tier.toUpperCase()} tier access with all 21 tools.`,
|
|
290
|
+
});
|
|
622
291
|
}
|
|
292
|
+
return JSON.stringify({
|
|
293
|
+
success: false,
|
|
294
|
+
error: "Invalid license key. Please check your key and try again.",
|
|
295
|
+
});
|
|
623
296
|
}
|
|
624
297
|
//# sourceMappingURL=license.js.map
|