@ggboi360/mobile-dev-mcp 0.2.0 β†’ 0.3.1

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/dist/license.js CHANGED
@@ -1,624 +1,297 @@
1
- /**
2
- * License validation module for Mobile Dev MCP
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
- // CONSTANTS
22
- // ============================================================================
23
- const CONFIG_DIR = path.join(os.homedir(), ".mobiledev-mcp");
24
- const CONFIG_FILE = path.join(CONFIG_DIR, "config.json");
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
- const TRIAL_FILE = path.join(CONFIG_DIR, "trial.json");
27
- // How long to trust cached license without revalidation
28
- const CACHE_TTL_MS = 24 * 60 * 60 * 1000; // 24 hours
29
- // Grace period if API is down but we have a cached valid license
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
- // BASIC TIER ($6/mo) - Core tools, available to all paid users
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
- // CONFIG MANAGEMENT
130
- // ============================================================================
131
- function ensureConfigDir() {
132
- if (!fs.existsSync(CONFIG_DIR)) {
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
- const raw = fs.readFileSync(CONFIG_FILE, "utf-8");
149
- const loaded = JSON.parse(raw);
150
- return { ...defaultConfig, ...loaded };
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
- return defaultConfig;
50
+ // Ignore errors
154
51
  }
155
- }
156
- export function saveConfig(config) {
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
- // MACHINE ID (for license binding)
56
+ // CACHE INTEGRITY (HMAC signing)
169
57
  // ============================================================================
170
- function getMachineId() {
171
- const info = [
172
- os.hostname(),
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
- // LICENSE CACHE
181
- // ============================================================================
182
- function loadLicenseCache() {
183
- if (!fs.existsSync(LICENSE_CACHE_FILE)) {
184
- return null;
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
- const raw = fs.readFileSync(LICENSE_CACHE_FILE, "utf-8");
188
- return JSON.parse(raw);
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
- return null;
97
+ // Ignore errors
192
98
  }
99
+ return null;
193
100
  }
194
- function saveLicenseCache(license) {
195
- ensureConfigDir();
196
- fs.writeFileSync(LICENSE_CACHE_FILE, JSON.stringify(license, null, 2));
197
- }
198
- function clearLicenseCache() {
199
- if (fs.existsSync(LICENSE_CACHE_FILE)) {
200
- fs.unlinkSync(LICENSE_CACHE_FILE);
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
- // TRIAL TRACKING
116
+ // LICENSE VALIDATION
215
117
  // ============================================================================
216
- function loadTrialInfo() {
217
- if (!fs.existsSync(TRIAL_FILE)) {
218
- return null;
219
- }
118
+ export async function checkLicense() {
220
119
  try {
221
- const raw = fs.readFileSync(TRIAL_FILE, "utf-8");
222
- return JSON.parse(raw);
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 null;
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
- // API VALIDATION
304
- // ============================================================================
305
- async function validateWithApi(licenseKey, apiEndpoint) {
306
- try {
307
- const response = await fetch(`${apiEndpoint}/validate`, {
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
- body: JSON.stringify({
313
- license_key: licenseKey,
314
- instance_id: getMachineId(),
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
- if (!response.ok) {
318
- console.error("License API returned status:", response.status);
319
- return null;
320
- }
321
- const data = (await response.json());
322
- if (data.valid) {
323
- // Determine tier from product/variant name
324
- let tier = data.tier || "basic";
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
- // MAIN LICENSE CHECK
359
- // ============================================================================
360
- let cachedLicenseResult = null;
361
- export async function checkLicense() {
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
- // API is down - check if we have a valid cached license within grace period
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
- // FEATURE GATING
225
+ // TOOL ACCESS CONTROL
409
226
  // ============================================================================
410
- export function isAdvancedTool(toolName) {
411
- return ADVANCED_TOOLS.includes(toolName);
412
- }
413
- export function isBasicTool(toolName) {
414
- return BASIC_TOOLS.includes(toolName);
415
- }
416
- export async function canUseTool(toolName) {
417
- const license = await checkLicense();
418
- // Advanced tools require Advanced tier
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
- // Basic tier users cannot use Advanced tools
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 async function requireBasic(toolName) {
474
- const license = await checkLicense();
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 getTierLimits(tier) {
497
- return TIER_LIMITS[tier];
243
+ export function isAdvancedOnlyTool(toolName) {
244
+ return ADVANCED_TOOLS.includes(toolName) && !FREE_TOOLS.includes(toolName);
498
245
  }
499
- export async function getMaxLogLines() {
500
- const license = await checkLicense();
501
- return TIER_LIMITS[license.tier].maxLogLines;
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 INFO TOOL
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 `πŸ“‹ License Status: ${license.tier.toUpperCase()} ${tierEmoji}
551
-
552
- License key: ${maskLicenseKey(license.key)}
553
- Email: ${license.email || "N/A"}
554
- Valid until: ${license.expiresAt || "Active subscription"}
555
- Last validated: ${license.validatedAt}
556
-
557
- Your limits:
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
- export async function handleLicenseTool(name, args) {
594
- switch (name) {
595
- case "get_license_status":
596
- return getLicenseStatus();
597
- case "set_license_key":
598
- const key = args.licenseKey;
599
- if (!key || key.length < 10) {
600
- return "Invalid license key format. Keys should look like: lk_XXXXX...";
601
- }
602
- setLicenseKey(key);
603
- // Force revalidation
604
- cachedLicenseResult = null;
605
- const status = await checkLicense();
606
- if (status.valid) {
607
- return `βœ… License activated successfully!
608
-
609
- Tier: ${status.tier.toUpperCase()}
610
- ${status.tier === "advanced" ? "All features unlocked!" : "Basic features unlocked!"}
611
-
612
- Use 'get_license_status' to see your limits.`;
613
- }
614
- else {
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://codecontrol.ai/mcp",
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