devlens-mcp 0.3.0 → 0.3.2

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.
Files changed (39) hide show
  1. package/README.md +10 -21
  2. package/dist/src/server.js +1 -1
  3. package/docs/setup-guide.md +39 -34
  4. package/package.json +1 -1
  5. package/.claude/settings.json +0 -12
  6. package/.claude/settings.local.json +0 -17
  7. package/bin/cli.ts +0 -22
  8. package/bin/register.ts +0 -96
  9. package/src/config/devlens-config.ts +0 -76
  10. package/src/index.ts +0 -5
  11. package/src/metro/cdp-client.ts +0 -160
  12. package/src/metro/log-collector.ts +0 -137
  13. package/src/metro/metro-bridge.ts +0 -307
  14. package/src/metro/network-inspector.ts +0 -134
  15. package/src/platform/android/adb.ts +0 -200
  16. package/src/platform/android/android-device.ts +0 -116
  17. package/src/platform/android/ui-automator.ts +0 -141
  18. package/src/platform/device-manager.ts +0 -229
  19. package/src/platform/device.ts +0 -110
  20. package/src/platform/ios/accessibility.ts +0 -189
  21. package/src/platform/ios/ios-device.ts +0 -116
  22. package/src/platform/ios/simctl.ts +0 -244
  23. package/src/server.ts +0 -228
  24. package/src/snapshot/formatter.ts +0 -102
  25. package/src/snapshot/ref-registry.ts +0 -230
  26. package/src/snapshot/snapshot-differ.ts +0 -220
  27. package/src/tools/app-tools.ts +0 -111
  28. package/src/tools/device-tools.ts +0 -96
  29. package/src/tools/ds-tools.ts +0 -395
  30. package/src/tools/interaction-tools.ts +0 -467
  31. package/src/tools/metro-tools.ts +0 -320
  32. package/src/tools/navigation-tools.ts +0 -71
  33. package/src/tools/screenshot-tools.ts +0 -698
  34. package/src/tools/snapshot-tools.ts +0 -585
  35. package/src/utils/image-preprocess.ts +0 -430
  36. package/src/utils/retry.ts +0 -51
  37. package/src/visual/comparator.ts +0 -191
  38. package/src/visual/layout-analyzer.ts +0 -283
  39. package/src/visual/screenshot.ts +0 -49
package/README.md CHANGED
@@ -14,26 +14,19 @@ Figma Design ──► AI Agent ──► Code Changes ──► Hot Reloa
14
14
 
15
15
  ## Quick Start
16
16
 
17
- ### Step 1 — Clone and build
18
-
17
+ ### Claude Code
19
18
  ```bash
20
- git clone https://JioOmni@dev.azure.com/JioOmni/OmniAI/_git/mcp-devlens
21
- cd mcp-devlens
22
- npm install
23
- npm run build
19
+ claude mcp add devlens -- npx devlens-mcp@latest
24
20
  ```
25
21
 
26
- Note the **absolute path** to the cloned folder — you'll need it in the next step (e.g. `/Users/yourname/mcp-devlens`).
27
-
28
- ### Step 2 — Add to your AI client
29
-
30
- **Cursor IDE** — edit `~/.cursor/mcp.json`:
22
+ ### Cursor IDE
23
+ Add to `~/.cursor/mcp.json`:
31
24
  ```json
32
25
  {
33
26
  "mcpServers": {
34
27
  "devlens": {
35
- "command": "node",
36
- "args": ["/absolute/path/to/mcp-devlens/dist/bin/cli.js"],
28
+ "command": "npx",
29
+ "args": ["devlens-mcp@latest"],
37
30
  "env": {
38
31
  "METRO_PORT": "8081",
39
32
  "FIGMA_TOKEN": "figd_xxxxx"
@@ -43,18 +36,14 @@ Note the **absolute path** to the cloned folder — you'll need it in the next s
43
36
  }
44
37
  ```
45
38
 
46
- **Claude Code**:
47
- ```bash
48
- claude mcp add devlens -- node /absolute/path/to/mcp-devlens/dist/bin/cli.js
49
- ```
50
-
51
- **Claude Desktop** — edit `~/Library/Application Support/Claude/claude_desktop_config.json`:
39
+ ### Claude Desktop
40
+ Add to `~/Library/Application Support/Claude/claude_desktop_config.json`:
52
41
  ```json
53
42
  {
54
43
  "mcpServers": {
55
44
  "devlens": {
56
- "command": "node",
57
- "args": ["/absolute/path/to/mcp-devlens/dist/bin/cli.js"],
45
+ "command": "npx",
46
+ "args": ["devlens-mcp@latest"],
58
47
  "env": {
59
48
  "METRO_PORT": "8081",
60
49
  "FIGMA_TOKEN": "figd_xxxxx"
@@ -18,7 +18,7 @@ const navigation_tools_js_1 = require("./tools/navigation-tools.js");
18
18
  async function startServer() {
19
19
  const server = new index_js_1.Server({
20
20
  name: "devlens-mcp",
21
- version: "0.3.0",
21
+ version: "0.3.1",
22
22
  }, {
23
23
  capabilities: {
24
24
  tools: {},
@@ -42,27 +42,14 @@ npx expo start
42
42
 
43
43
  ## Installation
44
44
 
45
- ### Step 1 Clone and build
46
-
47
- ```bash
48
- git clone https://JioOmni@dev.azure.com/JioOmni/OmniAI/_git/mcp-devlens
49
- cd mcp-devlens
50
- npm install
51
- npm run build
52
- ```
53
-
54
- This produces `dist/bin/cli.js`. Note the **absolute path** to this file — you'll need it below.
55
-
56
- ### Step 2 — Register with your AI client
57
-
58
- #### Cursor IDE
59
- Edit `~/.cursor/mcp.json`:
45
+ ### Option 1: Cursor IDE
46
+ Add to `~/.cursor/mcp.json`:
60
47
  ```json
61
48
  {
62
49
  "mcpServers": {
63
50
  "devlens": {
64
- "command": "node",
65
- "args": ["/absolute/path/to/mcp-devlens/dist/bin/cli.js"],
51
+ "command": "npx",
52
+ "args": ["devlens-mcp@latest"],
66
53
  "env": {
67
54
  "METRO_PORT": "8081",
68
55
  "FIGMA_TOKEN": "figd_xxxxx",
@@ -75,26 +62,19 @@ Edit `~/.cursor/mcp.json`:
75
62
 
76
63
  Restart Cursor after saving. DevLens tools will appear in the MCP tools panel.
77
64
 
78
- #### Claude Code
65
+ ### Option 2: Claude Code
79
66
  ```bash
80
- claude mcp add devlens -- node /absolute/path/to/mcp-devlens/dist/bin/cli.js
67
+ claude mcp add devlens -- npx devlens-mcp@latest
81
68
  ```
82
69
 
83
- Or run the auto-registration helper from the cloned repo:
84
- ```bash
85
- npm run register
86
- ```
87
-
88
- This runs `claude mcp add devlens -- node <absolute-path>/dist/bin/cli.js` for you. If DevLens is already registered, it will detect it and skip.
89
-
90
- #### Claude Desktop
91
- Edit `~/Library/Application Support/Claude/claude_desktop_config.json`:
70
+ ### Option 3: Claude Desktop
71
+ Add to `~/Library/Application Support/Claude/claude_desktop_config.json`:
92
72
  ```json
93
73
  {
94
74
  "mcpServers": {
95
75
  "devlens": {
96
- "command": "node",
97
- "args": ["/absolute/path/to/mcp-devlens/dist/bin/cli.js"],
76
+ "command": "npx",
77
+ "args": ["devlens-mcp@latest"],
98
78
  "env": {
99
79
  "METRO_PORT": "8081",
100
80
  "FIGMA_TOKEN": "figd_xxxxx",
@@ -105,7 +85,7 @@ Edit `~/Library/Application Support/Claude/claude_desktop_config.json`:
105
85
  }
106
86
  ```
107
87
 
108
- > **Tip:** The `postbuild` script automatically prints the correct `args` path after every build, so you always know the right value to paste.
88
+ > **Note:** `npx` downloads and runs DevLens automatically on first use. No clone or build step needed.
109
89
 
110
90
  ---
111
91
 
@@ -335,10 +315,35 @@ If steps 1–5 succeed, DevLens is fully operational. Steps 6–7 require the op
335
315
  - Verify the `tokensFile` path is correct relative to `projectRoot`
336
316
  - The tool logs warnings to stderr — check your MCP server logs
337
317
 
318
+ ### "env: node: No such file or directory" when using npx
319
+ This happens on macOS when Node.js is installed via `nvm`, `fnm`, or a version manager. Cursor (and other GUI apps) don't load shell profiles, so `node` isn't in their PATH even though `npx` is found.
320
+
321
+ **Fix:** Use the full absolute path to `npx` instead of just `npx`.
322
+
323
+ 1. Open Terminal and run:
324
+ ```bash
325
+ which npx
326
+ # e.g. /Users/yourname/.nvm/versions/node/v23.10.0/bin/npx
327
+ ```
328
+ 2. Replace `"command": "npx"` in `~/.cursor/mcp.json` with that full path:
329
+ ```json
330
+ {
331
+ "mcpServers": {
332
+ "devlens": {
333
+ "command": "/Users/yourname/.nvm/versions/node/v23.10.0/bin/npx",
334
+ "args": ["devlens-mcp@latest"],
335
+ "env": { "METRO_PORT": "8081", "FIGMA_TOKEN": "figd_xxxxx" }
336
+ }
337
+ }
338
+ }
339
+ ```
340
+ 3. Restart Cursor.
341
+
338
342
  ### DevLens tools not available in AI assistant
339
- - **npx users:** Verify the MCP config JSON is correct and restart your AI assistant
340
- - **Local dev:** Run `npm run register` to register with Claude Code
341
- - Check `~/.claude/mcp.json` or your IDE's MCP config for a `devlens` entry
343
+ - Verify `"command": "npx"` (not `"command": "node"`) in your MCP config
344
+ - If you see `env: node: No such file or directory`, see the section above
345
+ - Restart your AI client after any config change
346
+ - Check `~/.cursor/mcp.json` or your IDE's MCP config for a `devlens` entry
342
347
 
343
348
  ---
344
349
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "devlens-mcp",
3
- "version": "0.3.0",
3
+ "version": "0.3.2",
4
4
  "description": "DevLens — Playwright-style MCP server for mobile development. Take screenshots, compare with Figma designs, interact with iOS Simulators & Android Emulators, and access Metro bundler logs.",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -1,12 +0,0 @@
1
- {
2
- "permissions": {
3
- "allow": [
4
- "Bash(git remote set-url origin \"https://JioOmni@dev.azure.com/JioOmni/OmniAI/_git/mcp-devlens\")",
5
- "Bash(git add README.md docs/setup-guide.md)",
6
- "Bash(PAT=\"9Myzk2VeYmr8RfL9i7uJbfQvMWnqy7M5NVkB8cIDEB2u9njYt19yJQQJ99CBACAAAAAUmMaZAAASAZDO1XcC\")",
7
- "Bash(GIT_TERMINAL_PROMPT=0 git -c credential.helper= pull --rebase origin main)",
8
- "Bash(GIT_TERMINAL_PROMPT=0 git -c credential.helper= push -u origin main)",
9
- "Bash(npm whoami)"
10
- ]
11
- }
12
- }
@@ -1,17 +0,0 @@
1
- {
2
- "permissions": {
3
- "allow": [
4
- "Bash(wc:*)",
5
- "Bash(npx tsc:*)",
6
- "Bash(sips:*)",
7
- "Bash(npm run build)",
8
- "Bash(npm run register)",
9
- "Bash(claude mcp list)",
10
- "WebFetch(domain:github.com)",
11
- "WebFetch(domain:developers.figma.com)",
12
- "WebFetch(domain:sharp.pixelplumbing.com)",
13
- "WebFetch(domain:forum.figma.com)",
14
- "Bash(ls /Users/siddhant1.singh/StudioProjects/devlens/*.md)"
15
- ]
16
- }
17
- }
package/bin/cli.ts DELETED
@@ -1,22 +0,0 @@
1
- #!/usr/bin/env node
2
-
3
- import { createServer } from "../src/index.js";
4
-
5
- async function main() {
6
- const server = await createServer();
7
-
8
- process.on("SIGINT", async () => {
9
- await server.close();
10
- process.exit(0);
11
- });
12
-
13
- process.on("SIGTERM", async () => {
14
- await server.close();
15
- process.exit(0);
16
- });
17
- }
18
-
19
- main().catch((error) => {
20
- console.error("DevLens failed to start:", error);
21
- process.exit(1);
22
- });
package/bin/register.ts DELETED
@@ -1,96 +0,0 @@
1
- #!/usr/bin/env node
2
-
3
- /**
4
- * DevLens MCP Registration Script
5
- *
6
- * Usage:
7
- * node dist/bin/register.js — Check status + print instructions
8
- * node dist/bin/register.js --register — Actually register with Claude Code
9
- */
10
-
11
- import { execFile } from "child_process";
12
- import { promisify } from "util";
13
- import { resolve } from "path";
14
- import { existsSync, readFileSync } from "fs";
15
-
16
- const execFileAsync = promisify(execFile);
17
-
18
- async function register(): Promise<void> {
19
- const cliPath = resolve(__dirname, "cli.js");
20
-
21
- if (!existsSync(cliPath)) {
22
- console.error("Error: dist/bin/cli.js not found. Run 'npm run build' first.");
23
- process.exit(1);
24
- }
25
-
26
- const autoRegister = process.argv.includes("--register");
27
-
28
- // Check common config locations for existing registration
29
- const home = process.env.HOME || process.env.USERPROFILE || "~";
30
- const configPaths = [
31
- resolve(home, ".claude", "mcp.json"),
32
- resolve(home, ".claude", "settings.json"),
33
- resolve(home, ".config", "claude", "mcp.json"),
34
- ];
35
-
36
- let alreadyRegistered = false;
37
- for (const configPath of configPaths) {
38
- if (existsSync(configPath)) {
39
- try {
40
- const content = readFileSync(configPath, "utf-8");
41
- if (content.includes("devlens")) {
42
- alreadyRegistered = true;
43
- console.log(`DevLens already registered in: ${configPath}`);
44
- break;
45
- }
46
- } catch {
47
- // Ignore read errors
48
- }
49
- }
50
- }
51
-
52
- if (alreadyRegistered) {
53
- console.log("DevLens MCP server is already registered. No action needed.");
54
- return;
55
- }
56
-
57
- const command = `claude mcp add devlens -- node ${cliPath}`;
58
-
59
- if (autoRegister) {
60
- console.log("Registering DevLens MCP server...");
61
- console.log(`Running: ${command}`);
62
- try {
63
- const { stdout, stderr } = await execFileAsync("claude", [
64
- "mcp",
65
- "add",
66
- "devlens",
67
- "--",
68
- "node",
69
- cliPath,
70
- ]);
71
- if (stdout) console.log(stdout);
72
- if (stderr) console.error(stderr);
73
- console.log("DevLens registered successfully!");
74
- } catch (error: any) {
75
- console.error(`Registration failed: ${error.message}`);
76
- console.log(`\nPlease register manually:\n ${command}`);
77
- process.exit(1);
78
- }
79
- } else {
80
- console.log("");
81
- console.log("=== DevLens MCP Registration ===");
82
- console.log("");
83
- console.log("DevLens is not registered as an MCP tool.");
84
- console.log("To register, run one of these commands:");
85
- console.log("");
86
- console.log(` npm run register`);
87
- console.log(` # or manually:`);
88
- console.log(` ${command}`);
89
- console.log("");
90
- }
91
- }
92
-
93
- register().catch((err) => {
94
- console.error("Registration error:", err);
95
- process.exit(1);
96
- });
@@ -1,76 +0,0 @@
1
- import { z } from "zod";
2
- import { readFile } from "fs/promises";
3
- import { resolve } from "path";
4
-
5
- /**
6
- * DevLens configuration file support.
7
- *
8
- * Loaded from (in priority order):
9
- * 1. Path pointed to by DEVLENS_CONFIG env var
10
- * 2. ./devlens.config.json in CWD
11
- *
12
- * Place devlens.config.json in your consumer app root and point to it
13
- * via the MCP env block:
14
- *
15
- * "env": {
16
- * "METRO_PORT": "8081",
17
- * "FIGMA_TOKEN": "figd_xxx",
18
- * "DEVLENS_CONFIG": "/path/to/your-app/devlens.config.json"
19
- * }
20
- */
21
-
22
- export const DesignSystemConfigSchema = z.object({
23
- /** Design system identifier, e.g. "jds3" */
24
- name: z.string(),
25
- /** Absolute or CWD-relative path to the consumer app root */
26
- projectRoot: z.string(),
27
- /** Relative path from projectRoot to the tokens file, e.g. "src/constants/figmaTokens.ts" */
28
- tokensFile: z.string(),
29
- /** Relative path from projectRoot to the generated component interface directory */
30
- componentsDir: z.string().optional(),
31
- /** Glob pattern for component source files (default: "src/**\/*.tsx") */
32
- componentsGlob: z.string().default("src/**/*.tsx"),
33
- });
34
-
35
- export const DevLensConfigSchema = z.object({
36
- designSystem: DesignSystemConfigSchema.optional(),
37
- });
38
-
39
- export type DevLensConfig = z.infer<typeof DevLensConfigSchema>;
40
- export type DesignSystemConfig = z.infer<typeof DesignSystemConfigSchema>;
41
-
42
- /**
43
- * Load DevLens config from DEVLENS_CONFIG env var path, or ./devlens.config.json in CWD.
44
- * Returns empty config {} if neither is found or both are invalid.
45
- * Never throws — invalid config is logged and skipped.
46
- */
47
- export async function loadDevLensConfig(): Promise<DevLensConfig> {
48
- const candidates: string[] = [];
49
-
50
- const envPath = process.env.DEVLENS_CONFIG;
51
- if (envPath) {
52
- candidates.push(resolve(envPath));
53
- }
54
- candidates.push(resolve(process.cwd(), "devlens.config.json"));
55
-
56
- for (const candidate of candidates) {
57
- try {
58
- const raw = await readFile(candidate, "utf-8");
59
- const parsed = JSON.parse(raw);
60
- const result = DevLensConfigSchema.safeParse(parsed);
61
- if (result.success) {
62
- console.error(`[devlens] Config loaded from: ${candidate}`);
63
- return result.data;
64
- } else {
65
- console.error(
66
- `[devlens] Config at ${candidate} is invalid:`,
67
- JSON.stringify(result.error.format(), null, 2)
68
- );
69
- }
70
- } catch {
71
- // File not found or JSON parse error — try next candidate
72
- }
73
- }
74
-
75
- return {};
76
- }
package/src/index.ts DELETED
@@ -1,5 +0,0 @@
1
- import { startServer } from "./server.js";
2
-
3
- export async function createServer() {
4
- return startServer();
5
- }
@@ -1,160 +0,0 @@
1
- import WebSocket from "ws";
2
-
3
- /**
4
- * Chrome DevTools Protocol (CDP) client for communicating with the
5
- * Metro/Hermes debugger. Connects via WebSocket and sends JSON-RPC messages.
6
- *
7
- * Metro exposes the debugger endpoint at:
8
- * ws://localhost:{METRO_PORT}/inspector/device?device=0&page=-1
9
- *
10
- * Through CDP we can:
11
- * - Get console logs (Console.enable → Console.messageAdded)
12
- * - Intercept network requests (Network.enable → Network.requestWillBeSent)
13
- * - Execute JavaScript in the RN context (Runtime.evaluate)
14
- * - Trigger hot reload (via Runtime.evaluate)
15
- */
16
-
17
- export interface CdpMessage {
18
- id?: number;
19
- method?: string;
20
- params?: any;
21
- result?: any;
22
- error?: { message: string; code?: number };
23
- }
24
-
25
- type MessageHandler = (message: CdpMessage) => void;
26
-
27
- export class CdpClient {
28
- private ws: WebSocket | null = null;
29
- private messageId: number = 0;
30
- private pendingRequests: Map<
31
- number,
32
- { resolve: (value: any) => void; reject: (error: Error) => void }
33
- > = new Map();
34
- private eventHandlers: Map<string, MessageHandler[]> = new Map();
35
- private connected: boolean = false;
36
-
37
- constructor(private endpoint: string) {}
38
-
39
- /** Connect to the CDP endpoint */
40
- async connect(): Promise<void> {
41
- return new Promise((resolve, reject) => {
42
- this.ws = new WebSocket(this.endpoint);
43
-
44
- const timeout = setTimeout(() => {
45
- reject(new Error(`CDP connection timeout to ${this.endpoint}`));
46
- }, 5000);
47
-
48
- this.ws.on("open", () => {
49
- clearTimeout(timeout);
50
- this.connected = true;
51
- resolve();
52
- });
53
-
54
- this.ws.on("message", (data: WebSocket.Data) => {
55
- try {
56
- const message: CdpMessage = JSON.parse(data.toString());
57
- this.handleMessage(message);
58
- } catch {
59
- // Ignore malformed messages
60
- }
61
- });
62
-
63
- this.ws.on("close", () => {
64
- this.connected = false;
65
- // Reject all pending requests
66
- for (const [, { reject }] of this.pendingRequests) {
67
- reject(new Error("CDP connection closed"));
68
- }
69
- this.pendingRequests.clear();
70
- });
71
-
72
- this.ws.on("error", (err) => {
73
- clearTimeout(timeout);
74
- if (!this.connected) {
75
- reject(err);
76
- }
77
- });
78
- });
79
- }
80
-
81
- /** Send a CDP command and wait for the response */
82
- async send(method: string, params?: any): Promise<any> {
83
- if (!this.ws || !this.connected) {
84
- throw new Error("CDP client not connected");
85
- }
86
-
87
- const id = ++this.messageId;
88
- const message = JSON.stringify({ id, method, params });
89
-
90
- return new Promise((resolve, reject) => {
91
- const timeout = setTimeout(() => {
92
- this.pendingRequests.delete(id);
93
- reject(new Error(`CDP request timeout: ${method}`));
94
- }, 10000);
95
-
96
- this.pendingRequests.set(id, {
97
- resolve: (result) => {
98
- clearTimeout(timeout);
99
- resolve(result);
100
- },
101
- reject: (error) => {
102
- clearTimeout(timeout);
103
- reject(error);
104
- },
105
- });
106
-
107
- this.ws!.send(message);
108
- });
109
- }
110
-
111
- /** Register an event handler for CDP events */
112
- on(method: string, handler: MessageHandler): void {
113
- const handlers = this.eventHandlers.get(method) || [];
114
- handlers.push(handler);
115
- this.eventHandlers.set(method, handlers);
116
- }
117
-
118
- /** Remove event handlers for a method */
119
- off(method: string): void {
120
- this.eventHandlers.delete(method);
121
- }
122
-
123
- /** Check if connected */
124
- isConnected(): boolean {
125
- return this.connected;
126
- }
127
-
128
- /** Close the connection */
129
- close(): void {
130
- if (this.ws) {
131
- this.ws.close();
132
- this.ws = null;
133
- this.connected = false;
134
- }
135
- }
136
-
137
- private handleMessage(message: CdpMessage): void {
138
- // Response to a request
139
- if (message.id !== undefined) {
140
- const pending = this.pendingRequests.get(message.id);
141
- if (pending) {
142
- this.pendingRequests.delete(message.id);
143
- if (message.error) {
144
- pending.reject(new Error(message.error.message));
145
- } else {
146
- pending.resolve(message.result);
147
- }
148
- }
149
- return;
150
- }
151
-
152
- // Event notification
153
- if (message.method) {
154
- const handlers = this.eventHandlers.get(message.method) || [];
155
- for (const handler of handlers) {
156
- handler(message);
157
- }
158
- }
159
- }
160
- }