@metamask/device-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.
Files changed (107) hide show
  1. package/CHANGELOG.md +28 -1
  2. package/README.md +55 -2
  3. package/dist/backends/adb-backend.cjs +12 -5
  4. package/dist/backends/adb-backend.cjs.map +1 -1
  5. package/dist/backends/adb-backend.d.cts +8 -2
  6. package/dist/backends/adb-backend.d.cts.map +1 -1
  7. package/dist/backends/adb-backend.d.mts +8 -2
  8. package/dist/backends/adb-backend.d.mts.map +1 -1
  9. package/dist/backends/adb-backend.mjs +12 -5
  10. package/dist/backends/adb-backend.mjs.map +1 -1
  11. package/dist/backends/appium-backend.cjs +16 -7
  12. package/dist/backends/appium-backend.cjs.map +1 -1
  13. package/dist/backends/appium-backend.d.cts +8 -2
  14. package/dist/backends/appium-backend.d.cts.map +1 -1
  15. package/dist/backends/appium-backend.d.mts +8 -2
  16. package/dist/backends/appium-backend.d.mts.map +1 -1
  17. package/dist/backends/appium-backend.mjs +16 -7
  18. package/dist/backends/appium-backend.mjs.map +1 -1
  19. package/dist/backends/idb-backend.cjs +12 -5
  20. package/dist/backends/idb-backend.cjs.map +1 -1
  21. package/dist/backends/idb-backend.d.cts +8 -2
  22. package/dist/backends/idb-backend.d.cts.map +1 -1
  23. package/dist/backends/idb-backend.d.mts +8 -2
  24. package/dist/backends/idb-backend.d.mts.map +1 -1
  25. package/dist/backends/idb-backend.mjs +12 -5
  26. package/dist/backends/idb-backend.mjs.map +1 -1
  27. package/dist/backends/index.cjs +4 -3
  28. package/dist/backends/index.cjs.map +1 -1
  29. package/dist/backends/index.d.cts.map +1 -1
  30. package/dist/backends/index.d.mts.map +1 -1
  31. package/dist/backends/index.mjs +4 -3
  32. package/dist/backends/index.mjs.map +1 -1
  33. package/dist/backends/types.cjs.map +1 -1
  34. package/dist/backends/types.d.cts +19 -1
  35. package/dist/backends/types.d.cts.map +1 -1
  36. package/dist/backends/types.d.mts +19 -1
  37. package/dist/backends/types.d.mts.map +1 -1
  38. package/dist/backends/types.mjs.map +1 -1
  39. package/dist/hermes/hermes-cdp.cjs +674 -0
  40. package/dist/hermes/hermes-cdp.cjs.map +1 -0
  41. package/dist/hermes/hermes-cdp.d.cts +248 -0
  42. package/dist/hermes/hermes-cdp.d.cts.map +1 -0
  43. package/dist/hermes/hermes-cdp.d.mts +248 -0
  44. package/dist/hermes/hermes-cdp.d.mts.map +1 -0
  45. package/dist/hermes/hermes-cdp.mjs +665 -0
  46. package/dist/hermes/hermes-cdp.mjs.map +1 -0
  47. package/dist/hermes/session.cjs +196 -0
  48. package/dist/hermes/session.cjs.map +1 -0
  49. package/dist/hermes/session.d.cts +163 -0
  50. package/dist/hermes/session.d.cts.map +1 -0
  51. package/dist/hermes/session.d.mts +163 -0
  52. package/dist/hermes/session.d.mts.map +1 -0
  53. package/dist/hermes/session.mjs +189 -0
  54. package/dist/hermes/session.mjs.map +1 -0
  55. package/dist/index.cjs +21 -1
  56. package/dist/index.cjs.map +1 -1
  57. package/dist/index.d.cts +7 -1
  58. package/dist/index.d.cts.map +1 -1
  59. package/dist/index.d.mts +7 -1
  60. package/dist/index.d.mts.map +1 -1
  61. package/dist/index.mjs +3 -0
  62. package/dist/index.mjs.map +1 -1
  63. package/dist/server.cjs +3 -0
  64. package/dist/server.cjs.map +1 -1
  65. package/dist/server.d.cts.map +1 -1
  66. package/dist/server.d.mts.map +1 -1
  67. package/dist/server.mjs +4 -1
  68. package/dist/server.mjs.map +1 -1
  69. package/dist/tools/hermes-cdp.cjs +102 -0
  70. package/dist/tools/hermes-cdp.cjs.map +1 -0
  71. package/dist/tools/hermes-cdp.d.cts +18 -0
  72. package/dist/tools/hermes-cdp.d.cts.map +1 -0
  73. package/dist/tools/hermes-cdp.d.mts +18 -0
  74. package/dist/tools/hermes-cdp.d.mts.map +1 -0
  75. package/dist/tools/hermes-cdp.mjs +99 -0
  76. package/dist/tools/hermes-cdp.mjs.map +1 -0
  77. package/dist/tools/hermes-targets.cjs +127 -0
  78. package/dist/tools/hermes-targets.cjs.map +1 -0
  79. package/dist/tools/hermes-targets.d.cts +20 -0
  80. package/dist/tools/hermes-targets.d.cts.map +1 -0
  81. package/dist/tools/hermes-targets.d.mts +20 -0
  82. package/dist/tools/hermes-targets.d.mts.map +1 -0
  83. package/dist/tools/hermes-targets.mjs +124 -0
  84. package/dist/tools/hermes-targets.mjs.map +1 -0
  85. package/dist/tools/index.cjs +5 -1
  86. package/dist/tools/index.cjs.map +1 -1
  87. package/dist/tools/index.d.cts +2 -0
  88. package/dist/tools/index.d.cts.map +1 -1
  89. package/dist/tools/index.d.mts +2 -0
  90. package/dist/tools/index.d.mts.map +1 -1
  91. package/dist/tools/index.mjs +2 -0
  92. package/dist/tools/index.mjs.map +1 -1
  93. package/dist/tools/screenshot.cjs +8 -3
  94. package/dist/tools/screenshot.cjs.map +1 -1
  95. package/dist/tools/screenshot.d.cts.map +1 -1
  96. package/dist/tools/screenshot.d.mts.map +1 -1
  97. package/dist/tools/screenshot.mjs +8 -3
  98. package/dist/tools/screenshot.mjs.map +1 -1
  99. package/dist/utils/output-path.cjs +128 -0
  100. package/dist/utils/output-path.cjs.map +1 -0
  101. package/dist/utils/output-path.d.cts +41 -0
  102. package/dist/utils/output-path.d.cts.map +1 -0
  103. package/dist/utils/output-path.d.mts +41 -0
  104. package/dist/utils/output-path.d.mts.map +1 -0
  105. package/dist/utils/output-path.mjs +123 -0
  106. package/dist/utils/output-path.mjs.map +1 -0
  107. package/package.json +1 -1
@@ -0,0 +1,102 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.registerHermesCdpTool = registerHermesCdpTool;
4
+ const zod_1 = require("zod");
5
+ const shared_js_1 = require("./shared.cjs");
6
+ const hermes_cdp_js_1 = require("../hermes/hermes-cdp.cjs");
7
+ const session_js_1 = require("../hermes/session.cjs");
8
+ const DEFAULT_TIMEOUT_MS = 30000;
9
+ const MIN_TIMEOUT_MS = 1000;
10
+ const MAX_TIMEOUT_MS = 120000;
11
+ /**
12
+ * Registers the `hermes_cdp` MCP tool, which speaks raw Chrome DevTools
13
+ * Protocol (CDP) to the React Native Hermes runtime of a running app via
14
+ * Metro's inspector proxy (iOS and Android).
15
+ *
16
+ * The handler is intentionally THIN: it resolves the Metro port/appId and the
17
+ * current device pin from {@link getHermesSession}, delegates ALL discovery,
18
+ * verification, and execution logic to {@link runHermesCdp}, and persists the
19
+ * resolved pin via the core's `onPin` callback.
20
+ *
21
+ * @param server - The MCP server to register the tool on.
22
+ * @param backend - The lazy device backend, used at call time only to read the
23
+ * live platform so the session can resolve the correct default app bundle ID.
24
+ */
25
+ function registerHermesCdpTool(server, backend) {
26
+ server.registerTool('hermes_cdp', {
27
+ title: 'Hermes CDP',
28
+ description: 'Speak raw Chrome DevTools Protocol (CDP) to the React Native Hermes ' +
29
+ 'JS runtime of a running app via Metro\u2019s inspector proxy (iOS and ' +
30
+ 'Android). Requires a DEBUG build with Metro running; release builds ' +
31
+ 'expose no inspector. Blocked methods (for safety): ' +
32
+ 'Runtime.terminateExecution, Inspector.detached. Node 20 must be ' +
33
+ 'launched with NODE_OPTIONS="--experimental-websocket" (Node 22+ works ' +
34
+ 'out of the box). Example: method "Runtime.evaluate" with params ' +
35
+ '{"expression":"1+1","returnByValue":true} \u2014 the raw response nests ' +
36
+ 'the value at result.result.value.',
37
+ inputSchema: {
38
+ method: zod_1.z
39
+ .string()
40
+ .describe('The CDP method to invoke, e.g. "Runtime.evaluate".'),
41
+ params: zod_1.z
42
+ .record(zod_1.z.string(), zod_1.z.unknown())
43
+ .optional()
44
+ .describe('Optional CDP method parameters.'),
45
+ timeoutMs: zod_1.z
46
+ .number()
47
+ .int()
48
+ .positive()
49
+ .optional()
50
+ .describe(`Timeout in milliseconds applied to each phase independently: ` +
51
+ `discovery, socket open, identity probe, and the CDP call ` +
52
+ `(default ${DEFAULT_TIMEOUT_MS}, floored to ${MIN_TIMEOUT_MS}, max ${MAX_TIMEOUT_MS}).`),
53
+ metroPort: zod_1.z
54
+ .number()
55
+ .optional()
56
+ .describe('Override the Metro inspector proxy port for this call.'),
57
+ appId: zod_1.z
58
+ .string()
59
+ .optional()
60
+ .describe('Override the expected app bundle identifier.'),
61
+ },
62
+ }, async ({ method, params, timeoutMs, metroPort, appId }) => {
63
+ try {
64
+ const session = (0, session_js_1.getHermesSession)();
65
+ const resolved = session.resolve({
66
+ metroPort,
67
+ appId,
68
+ platform: backend.platform,
69
+ });
70
+ const clampedTimeoutMs = Math.min(Math.max(timeoutMs ?? DEFAULT_TIMEOUT_MS, MIN_TIMEOUT_MS), MAX_TIMEOUT_MS);
71
+ const result = await (0, hermes_cdp_js_1.runHermesCdp)({
72
+ method,
73
+ params,
74
+ timeoutMs: clampedTimeoutMs,
75
+ metroPort: resolved.metroPort,
76
+ appId: resolved.appId,
77
+ pinnedDeviceId: resolved.pinnedDeviceId,
78
+ onPin: (id) => {
79
+ const currentPin = session.getPinnedHermesDeviceId();
80
+ if (currentPin === undefined) {
81
+ session.setPinnedHermesDeviceId(id);
82
+ }
83
+ },
84
+ });
85
+ if (!result.ok) {
86
+ return (0, shared_js_1.errorResult)(new Error(`[${result.code}] ${result.message}`));
87
+ }
88
+ return {
89
+ content: [
90
+ {
91
+ type: 'text',
92
+ text: JSON.stringify(result.result, null, 2),
93
+ },
94
+ ],
95
+ };
96
+ }
97
+ catch (error) {
98
+ return (0, shared_js_1.errorResult)(error);
99
+ }
100
+ });
101
+ }
102
+ //# sourceMappingURL=hermes-cdp.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"hermes-cdp.cjs","sourceRoot":"","sources":["../../src/tools/hermes-cdp.ts"],"names":[],"mappings":"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","sourcesContent":["import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';\nimport { z } from 'zod';\n\nimport { errorResult } from './shared.js';\nimport type { DeviceBackend } from '../backends/types.js';\nimport { runHermesCdp } from '../hermes/hermes-cdp.js';\nimport { getHermesSession } from '../hermes/session.js';\n\nconst DEFAULT_TIMEOUT_MS = 30_000;\nconst MIN_TIMEOUT_MS = 1_000;\nconst MAX_TIMEOUT_MS = 120_000;\n\n/**\n * Registers the `hermes_cdp` MCP tool, which speaks raw Chrome DevTools\n * Protocol (CDP) to the React Native Hermes runtime of a running app via\n * Metro's inspector proxy (iOS and Android).\n *\n * The handler is intentionally THIN: it resolves the Metro port/appId and the\n * current device pin from {@link getHermesSession}, delegates ALL discovery,\n * verification, and execution logic to {@link runHermesCdp}, and persists the\n * resolved pin via the core's `onPin` callback.\n *\n * @param server - The MCP server to register the tool on.\n * @param backend - The lazy device backend, used at call time only to read the\n * live platform so the session can resolve the correct default app bundle ID.\n */\nexport function registerHermesCdpTool(\n server: McpServer,\n backend: DeviceBackend,\n): void {\n server.registerTool(\n 'hermes_cdp',\n {\n title: 'Hermes CDP',\n description:\n 'Speak raw Chrome DevTools Protocol (CDP) to the React Native Hermes ' +\n 'JS runtime of a running app via Metro\\u2019s inspector proxy (iOS and ' +\n 'Android). Requires a DEBUG build with Metro running; release builds ' +\n 'expose no inspector. Blocked methods (for safety): ' +\n 'Runtime.terminateExecution, Inspector.detached. Node 20 must be ' +\n 'launched with NODE_OPTIONS=\"--experimental-websocket\" (Node 22+ works ' +\n 'out of the box). Example: method \"Runtime.evaluate\" with params ' +\n '{\"expression\":\"1+1\",\"returnByValue\":true} \\u2014 the raw response nests ' +\n 'the value at result.result.value.',\n inputSchema: {\n method: z\n .string()\n .describe('The CDP method to invoke, e.g. \"Runtime.evaluate\".'),\n params: z\n .record(z.string(), z.unknown())\n .optional()\n .describe('Optional CDP method parameters.'),\n timeoutMs: z\n .number()\n .int()\n .positive()\n .optional()\n .describe(\n `Timeout in milliseconds applied to each phase independently: ` +\n `discovery, socket open, identity probe, and the CDP call ` +\n `(default ${DEFAULT_TIMEOUT_MS}, floored to ${MIN_TIMEOUT_MS}, max ${MAX_TIMEOUT_MS}).`,\n ),\n metroPort: z\n .number()\n .optional()\n .describe('Override the Metro inspector proxy port for this call.'),\n appId: z\n .string()\n .optional()\n .describe('Override the expected app bundle identifier.'),\n },\n },\n async ({ method, params, timeoutMs, metroPort, appId }) => {\n try {\n const session = getHermesSession();\n const resolved = session.resolve({\n metroPort,\n appId,\n platform: backend.platform,\n });\n const clampedTimeoutMs = Math.min(\n Math.max(timeoutMs ?? DEFAULT_TIMEOUT_MS, MIN_TIMEOUT_MS),\n MAX_TIMEOUT_MS,\n );\n\n const result = await runHermesCdp({\n method,\n params,\n timeoutMs: clampedTimeoutMs,\n metroPort: resolved.metroPort,\n appId: resolved.appId,\n pinnedDeviceId: resolved.pinnedDeviceId,\n onPin: (id) => {\n const currentPin = session.getPinnedHermesDeviceId();\n if (currentPin === undefined) {\n session.setPinnedHermesDeviceId(id);\n }\n },\n });\n\n if (!result.ok) {\n return errorResult(new Error(`[${result.code}] ${result.message}`));\n }\n\n return {\n content: [\n {\n type: 'text' as const,\n text: JSON.stringify(result.result, null, 2),\n },\n ],\n };\n } catch (error) {\n return errorResult(error);\n }\n },\n );\n}\n"]}
@@ -0,0 +1,18 @@
1
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import type { DeviceBackend } from "../backends/types.cjs";
3
+ /**
4
+ * Registers the `hermes_cdp` MCP tool, which speaks raw Chrome DevTools
5
+ * Protocol (CDP) to the React Native Hermes runtime of a running app via
6
+ * Metro's inspector proxy (iOS and Android).
7
+ *
8
+ * The handler is intentionally THIN: it resolves the Metro port/appId and the
9
+ * current device pin from {@link getHermesSession}, delegates ALL discovery,
10
+ * verification, and execution logic to {@link runHermesCdp}, and persists the
11
+ * resolved pin via the core's `onPin` callback.
12
+ *
13
+ * @param server - The MCP server to register the tool on.
14
+ * @param backend - The lazy device backend, used at call time only to read the
15
+ * live platform so the session can resolve the correct default app bundle ID.
16
+ */
17
+ export declare function registerHermesCdpTool(server: McpServer, backend: DeviceBackend): void;
18
+ //# sourceMappingURL=hermes-cdp.d.cts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"hermes-cdp.d.cts","sourceRoot":"","sources":["../../src/tools/hermes-cdp.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,gDAAgD;AAIzE,OAAO,KAAK,EAAE,aAAa,EAAE,8BAA6B;AAQ1D;;;;;;;;;;;;;GAaG;AACH,wBAAgB,qBAAqB,CACnC,MAAM,EAAE,SAAS,EACjB,OAAO,EAAE,aAAa,GACrB,IAAI,CAwFN"}
@@ -0,0 +1,18 @@
1
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import type { DeviceBackend } from "../backends/types.mjs";
3
+ /**
4
+ * Registers the `hermes_cdp` MCP tool, which speaks raw Chrome DevTools
5
+ * Protocol (CDP) to the React Native Hermes runtime of a running app via
6
+ * Metro's inspector proxy (iOS and Android).
7
+ *
8
+ * The handler is intentionally THIN: it resolves the Metro port/appId and the
9
+ * current device pin from {@link getHermesSession}, delegates ALL discovery,
10
+ * verification, and execution logic to {@link runHermesCdp}, and persists the
11
+ * resolved pin via the core's `onPin` callback.
12
+ *
13
+ * @param server - The MCP server to register the tool on.
14
+ * @param backend - The lazy device backend, used at call time only to read the
15
+ * live platform so the session can resolve the correct default app bundle ID.
16
+ */
17
+ export declare function registerHermesCdpTool(server: McpServer, backend: DeviceBackend): void;
18
+ //# sourceMappingURL=hermes-cdp.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"hermes-cdp.d.mts","sourceRoot":"","sources":["../../src/tools/hermes-cdp.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,gDAAgD;AAIzE,OAAO,KAAK,EAAE,aAAa,EAAE,8BAA6B;AAQ1D;;;;;;;;;;;;;GAaG;AACH,wBAAgB,qBAAqB,CACnC,MAAM,EAAE,SAAS,EACjB,OAAO,EAAE,aAAa,GACrB,IAAI,CAwFN"}
@@ -0,0 +1,99 @@
1
+ import { z } from "zod";
2
+ import { errorResult } from "./shared.mjs";
3
+ import { runHermesCdp } from "../hermes/hermes-cdp.mjs";
4
+ import { getHermesSession } from "../hermes/session.mjs";
5
+ const DEFAULT_TIMEOUT_MS = 30000;
6
+ const MIN_TIMEOUT_MS = 1000;
7
+ const MAX_TIMEOUT_MS = 120000;
8
+ /**
9
+ * Registers the `hermes_cdp` MCP tool, which speaks raw Chrome DevTools
10
+ * Protocol (CDP) to the React Native Hermes runtime of a running app via
11
+ * Metro's inspector proxy (iOS and Android).
12
+ *
13
+ * The handler is intentionally THIN: it resolves the Metro port/appId and the
14
+ * current device pin from {@link getHermesSession}, delegates ALL discovery,
15
+ * verification, and execution logic to {@link runHermesCdp}, and persists the
16
+ * resolved pin via the core's `onPin` callback.
17
+ *
18
+ * @param server - The MCP server to register the tool on.
19
+ * @param backend - The lazy device backend, used at call time only to read the
20
+ * live platform so the session can resolve the correct default app bundle ID.
21
+ */
22
+ export function registerHermesCdpTool(server, backend) {
23
+ server.registerTool('hermes_cdp', {
24
+ title: 'Hermes CDP',
25
+ description: 'Speak raw Chrome DevTools Protocol (CDP) to the React Native Hermes ' +
26
+ 'JS runtime of a running app via Metro\u2019s inspector proxy (iOS and ' +
27
+ 'Android). Requires a DEBUG build with Metro running; release builds ' +
28
+ 'expose no inspector. Blocked methods (for safety): ' +
29
+ 'Runtime.terminateExecution, Inspector.detached. Node 20 must be ' +
30
+ 'launched with NODE_OPTIONS="--experimental-websocket" (Node 22+ works ' +
31
+ 'out of the box). Example: method "Runtime.evaluate" with params ' +
32
+ '{"expression":"1+1","returnByValue":true} \u2014 the raw response nests ' +
33
+ 'the value at result.result.value.',
34
+ inputSchema: {
35
+ method: z
36
+ .string()
37
+ .describe('The CDP method to invoke, e.g. "Runtime.evaluate".'),
38
+ params: z
39
+ .record(z.string(), z.unknown())
40
+ .optional()
41
+ .describe('Optional CDP method parameters.'),
42
+ timeoutMs: z
43
+ .number()
44
+ .int()
45
+ .positive()
46
+ .optional()
47
+ .describe(`Timeout in milliseconds applied to each phase independently: ` +
48
+ `discovery, socket open, identity probe, and the CDP call ` +
49
+ `(default ${DEFAULT_TIMEOUT_MS}, floored to ${MIN_TIMEOUT_MS}, max ${MAX_TIMEOUT_MS}).`),
50
+ metroPort: z
51
+ .number()
52
+ .optional()
53
+ .describe('Override the Metro inspector proxy port for this call.'),
54
+ appId: z
55
+ .string()
56
+ .optional()
57
+ .describe('Override the expected app bundle identifier.'),
58
+ },
59
+ }, async ({ method, params, timeoutMs, metroPort, appId }) => {
60
+ try {
61
+ const session = getHermesSession();
62
+ const resolved = session.resolve({
63
+ metroPort,
64
+ appId,
65
+ platform: backend.platform,
66
+ });
67
+ const clampedTimeoutMs = Math.min(Math.max(timeoutMs ?? DEFAULT_TIMEOUT_MS, MIN_TIMEOUT_MS), MAX_TIMEOUT_MS);
68
+ const result = await runHermesCdp({
69
+ method,
70
+ params,
71
+ timeoutMs: clampedTimeoutMs,
72
+ metroPort: resolved.metroPort,
73
+ appId: resolved.appId,
74
+ pinnedDeviceId: resolved.pinnedDeviceId,
75
+ onPin: (id) => {
76
+ const currentPin = session.getPinnedHermesDeviceId();
77
+ if (currentPin === undefined) {
78
+ session.setPinnedHermesDeviceId(id);
79
+ }
80
+ },
81
+ });
82
+ if (!result.ok) {
83
+ return errorResult(new Error(`[${result.code}] ${result.message}`));
84
+ }
85
+ return {
86
+ content: [
87
+ {
88
+ type: 'text',
89
+ text: JSON.stringify(result.result, null, 2),
90
+ },
91
+ ],
92
+ };
93
+ }
94
+ catch (error) {
95
+ return errorResult(error);
96
+ }
97
+ });
98
+ }
99
+ //# sourceMappingURL=hermes-cdp.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"hermes-cdp.mjs","sourceRoot":"","sources":["../../src/tools/hermes-cdp.ts"],"names":[],"mappings":"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","sourcesContent":["import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';\nimport { z } from 'zod';\n\nimport { errorResult } from './shared.js';\nimport type { DeviceBackend } from '../backends/types.js';\nimport { runHermesCdp } from '../hermes/hermes-cdp.js';\nimport { getHermesSession } from '../hermes/session.js';\n\nconst DEFAULT_TIMEOUT_MS = 30_000;\nconst MIN_TIMEOUT_MS = 1_000;\nconst MAX_TIMEOUT_MS = 120_000;\n\n/**\n * Registers the `hermes_cdp` MCP tool, which speaks raw Chrome DevTools\n * Protocol (CDP) to the React Native Hermes runtime of a running app via\n * Metro's inspector proxy (iOS and Android).\n *\n * The handler is intentionally THIN: it resolves the Metro port/appId and the\n * current device pin from {@link getHermesSession}, delegates ALL discovery,\n * verification, and execution logic to {@link runHermesCdp}, and persists the\n * resolved pin via the core's `onPin` callback.\n *\n * @param server - The MCP server to register the tool on.\n * @param backend - The lazy device backend, used at call time only to read the\n * live platform so the session can resolve the correct default app bundle ID.\n */\nexport function registerHermesCdpTool(\n server: McpServer,\n backend: DeviceBackend,\n): void {\n server.registerTool(\n 'hermes_cdp',\n {\n title: 'Hermes CDP',\n description:\n 'Speak raw Chrome DevTools Protocol (CDP) to the React Native Hermes ' +\n 'JS runtime of a running app via Metro\\u2019s inspector proxy (iOS and ' +\n 'Android). Requires a DEBUG build with Metro running; release builds ' +\n 'expose no inspector. Blocked methods (for safety): ' +\n 'Runtime.terminateExecution, Inspector.detached. Node 20 must be ' +\n 'launched with NODE_OPTIONS=\"--experimental-websocket\" (Node 22+ works ' +\n 'out of the box). Example: method \"Runtime.evaluate\" with params ' +\n '{\"expression\":\"1+1\",\"returnByValue\":true} \\u2014 the raw response nests ' +\n 'the value at result.result.value.',\n inputSchema: {\n method: z\n .string()\n .describe('The CDP method to invoke, e.g. \"Runtime.evaluate\".'),\n params: z\n .record(z.string(), z.unknown())\n .optional()\n .describe('Optional CDP method parameters.'),\n timeoutMs: z\n .number()\n .int()\n .positive()\n .optional()\n .describe(\n `Timeout in milliseconds applied to each phase independently: ` +\n `discovery, socket open, identity probe, and the CDP call ` +\n `(default ${DEFAULT_TIMEOUT_MS}, floored to ${MIN_TIMEOUT_MS}, max ${MAX_TIMEOUT_MS}).`,\n ),\n metroPort: z\n .number()\n .optional()\n .describe('Override the Metro inspector proxy port for this call.'),\n appId: z\n .string()\n .optional()\n .describe('Override the expected app bundle identifier.'),\n },\n },\n async ({ method, params, timeoutMs, metroPort, appId }) => {\n try {\n const session = getHermesSession();\n const resolved = session.resolve({\n metroPort,\n appId,\n platform: backend.platform,\n });\n const clampedTimeoutMs = Math.min(\n Math.max(timeoutMs ?? DEFAULT_TIMEOUT_MS, MIN_TIMEOUT_MS),\n MAX_TIMEOUT_MS,\n );\n\n const result = await runHermesCdp({\n method,\n params,\n timeoutMs: clampedTimeoutMs,\n metroPort: resolved.metroPort,\n appId: resolved.appId,\n pinnedDeviceId: resolved.pinnedDeviceId,\n onPin: (id) => {\n const currentPin = session.getPinnedHermesDeviceId();\n if (currentPin === undefined) {\n session.setPinnedHermesDeviceId(id);\n }\n },\n });\n\n if (!result.ok) {\n return errorResult(new Error(`[${result.code}] ${result.message}`));\n }\n\n return {\n content: [\n {\n type: 'text' as const,\n text: JSON.stringify(result.result, null, 2),\n },\n ],\n };\n } catch (error) {\n return errorResult(error);\n }\n },\n );\n}\n"]}
@@ -0,0 +1,127 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.registerHermesTargetsTool = registerHermesTargetsTool;
4
+ const zod_1 = require("zod");
5
+ const shared_js_1 = require("./shared.cjs");
6
+ const hermes_cdp_js_1 = require("../hermes/hermes-cdp.cjs");
7
+ const session_js_1 = require("../hermes/session.cjs");
8
+ const DISCOVERY_TIMEOUT_MS = 10000;
9
+ function formatTarget(target) {
10
+ const { reactNative } = target;
11
+ return [
12
+ ` id: ${target.id ?? '<unknown>'}`,
13
+ ` title: ${target.title ?? '<unknown>'}`,
14
+ ` appId: ${target.appId ?? '<unknown>'}`,
15
+ ` logicalDeviceId: ${reactNative?.logicalDeviceId ?? '<missing>'}`,
16
+ ` nativePageReloads: ${reactNative?.capabilities?.nativePageReloads ?? '<unset>'}`,
17
+ ` wsUrl: ${target.webSocketDebuggerUrl ?? '<missing>'}`,
18
+ ].join('\n');
19
+ }
20
+ function describeMetroDown(error) {
21
+ const message = error instanceof Error ? error.message : String(error);
22
+ if (message.includes('ECONNREFUSED') || message.includes('fetch failed')) {
23
+ return 'Metro not running or no debuggable app registered.';
24
+ }
25
+ return undefined;
26
+ }
27
+ /**
28
+ * Registers the `hermes_targets` MCP tool, which lists and diagnoses the
29
+ * debuggable Hermes targets that Metro's inspector proxy currently exposes
30
+ * (iOS and Android).
31
+ *
32
+ * The handler is THIN: it resolves the Metro port/appId from
33
+ * {@link getHermesSession}, delegates discovery to {@link fetchDiscoveryTargets}
34
+ * and selection/ambiguity reporting to {@link selectHermesTarget} /
35
+ * {@link hasAmbiguousTarget}, and formats a readable report. With `all` set,
36
+ * it bypasses the appId filter so agents can discover the real appId / confirm
37
+ * Metro is up. No discovery or selection LOGIC lives in this file.
38
+ *
39
+ * @param server - The MCP server to register the tool on.
40
+ * @param backend - The lazy device backend, used at call time only to read the
41
+ * live platform so the session can resolve the correct default app bundle ID.
42
+ */
43
+ function registerHermesTargetsTool(server, backend) {
44
+ server.registerTool('hermes_targets', {
45
+ title: 'Hermes Targets',
46
+ description: 'List and diagnose the debuggable React Native Hermes targets that ' +
47
+ 'Metro\u2019s inspector proxy currently exposes (iOS and Android). ' +
48
+ 'Reports each candidate (id / title / appId / logicalDeviceId / ' +
49
+ 'nativePageReloads / wsUrl) plus which target would be chosen or why ' +
50
+ 'it is ambiguous. Use it to confirm Metro is running and your app is ' +
51
+ 'registered. Requires a DEBUG build with Metro running.',
52
+ inputSchema: {
53
+ metroPort: zod_1.z
54
+ .number()
55
+ .optional()
56
+ .describe('Override the Metro inspector proxy port for this call.'),
57
+ appId: zod_1.z
58
+ .string()
59
+ .optional()
60
+ .describe('Override the expected app bundle identifier.'),
61
+ all: zod_1.z
62
+ .boolean()
63
+ .optional()
64
+ .describe('Bypass the appId filter and list ALL candidates (diagnostics).'),
65
+ },
66
+ annotations: { readOnlyHint: true },
67
+ }, async ({ metroPort, appId, all }) => {
68
+ try {
69
+ const resolved = (0, session_js_1.getHermesSession)().resolve({
70
+ metroPort,
71
+ appId,
72
+ platform: backend.platform,
73
+ });
74
+ const targets = await (0, hermes_cdp_js_1.fetchDiscoveryTargets)(resolved.metroPort, DISCOVERY_TIMEOUT_MS);
75
+ if (targets.length === 0) {
76
+ return {
77
+ content: [
78
+ {
79
+ type: 'text',
80
+ text: 'Metro not running or no debuggable app registered.',
81
+ },
82
+ ],
83
+ };
84
+ }
85
+ const candidates = all
86
+ ? targets
87
+ : targets
88
+ .filter((target) => Boolean(target.webSocketDebuggerUrl))
89
+ .filter((target) => target.title !== hermes_cdp_js_1.LEGACY_SYNTHETIC_TITLE)
90
+ .filter((target) => target.appId === resolved.appId);
91
+ const lines = [
92
+ `Metro port: ${resolved.metroPort}`,
93
+ `Expected appId: ${resolved.appId}${all ? ' (filter bypassed by all=true)' : ''}`,
94
+ `Targets discovered: ${targets.length}`,
95
+ `Candidates listed: ${candidates.length}`,
96
+ '',
97
+ ];
98
+ candidates.forEach((target, index) => {
99
+ lines.push(`Candidate ${index + 1}:`, formatTarget(target), '');
100
+ });
101
+ const selection = (0, hermes_cdp_js_1.selectHermesTarget)(targets, resolved.appId, resolved.pinnedDeviceId);
102
+ if (selection.ok) {
103
+ lines.push(`Chosen target: ${selection.target.id ?? '<unknown>'} (device=${selection.target.reactNative?.logicalDeviceId ?? '<missing>'})`);
104
+ }
105
+ else if (selection.code === 'HERMES_MULTIPLE_DEVICES' &&
106
+ (0, hermes_cdp_js_1.hasAmbiguousTarget)(candidates)) {
107
+ lines.push(`Ambiguous: ${selection.message}`);
108
+ }
109
+ else {
110
+ lines.push(`No target chosen: [${selection.code}] ${selection.message}`);
111
+ }
112
+ return {
113
+ content: [{ type: 'text', text: lines.join('\n') }],
114
+ };
115
+ }
116
+ catch (error) {
117
+ const metroDown = describeMetroDown(error);
118
+ if (metroDown) {
119
+ return {
120
+ content: [{ type: 'text', text: metroDown }],
121
+ };
122
+ }
123
+ return (0, shared_js_1.errorResult)(error);
124
+ }
125
+ });
126
+ }
127
+ //# sourceMappingURL=hermes-targets.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"hermes-targets.cjs","sourceRoot":"","sources":["../../src/tools/hermes-targets.ts"],"names":[],"mappings":"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","sourcesContent":["import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';\nimport { z } from 'zod';\n\nimport { errorResult } from './shared.js';\nimport type { DeviceBackend } from '../backends/types.js';\nimport {\n fetchDiscoveryTargets,\n hasAmbiguousTarget,\n LEGACY_SYNTHETIC_TITLE,\n selectHermesTarget,\n} from '../hermes/hermes-cdp.js';\nimport type { HermesTarget } from '../hermes/hermes-cdp.js';\nimport { getHermesSession } from '../hermes/session.js';\n\nconst DISCOVERY_TIMEOUT_MS = 10_000;\n\nfunction formatTarget(target: HermesTarget): string {\n const { reactNative } = target;\n return [\n ` id: ${target.id ?? '<unknown>'}`,\n ` title: ${target.title ?? '<unknown>'}`,\n ` appId: ${target.appId ?? '<unknown>'}`,\n ` logicalDeviceId: ${reactNative?.logicalDeviceId ?? '<missing>'}`,\n ` nativePageReloads: ${\n reactNative?.capabilities?.nativePageReloads ?? '<unset>'\n }`,\n ` wsUrl: ${target.webSocketDebuggerUrl ?? '<missing>'}`,\n ].join('\\n');\n}\n\nfunction describeMetroDown(error: unknown): string | undefined {\n const message = error instanceof Error ? error.message : String(error);\n if (message.includes('ECONNREFUSED') || message.includes('fetch failed')) {\n return 'Metro not running or no debuggable app registered.';\n }\n return undefined;\n}\n\n/**\n * Registers the `hermes_targets` MCP tool, which lists and diagnoses the\n * debuggable Hermes targets that Metro's inspector proxy currently exposes\n * (iOS and Android).\n *\n * The handler is THIN: it resolves the Metro port/appId from\n * {@link getHermesSession}, delegates discovery to {@link fetchDiscoveryTargets}\n * and selection/ambiguity reporting to {@link selectHermesTarget} /\n * {@link hasAmbiguousTarget}, and formats a readable report. With `all` set,\n * it bypasses the appId filter so agents can discover the real appId / confirm\n * Metro is up. No discovery or selection LOGIC lives in this file.\n *\n * @param server - The MCP server to register the tool on.\n * @param backend - The lazy device backend, used at call time only to read the\n * live platform so the session can resolve the correct default app bundle ID.\n */\nexport function registerHermesTargetsTool(\n server: McpServer,\n backend: DeviceBackend,\n): void {\n server.registerTool(\n 'hermes_targets',\n {\n title: 'Hermes Targets',\n description:\n 'List and diagnose the debuggable React Native Hermes targets that ' +\n 'Metro\\u2019s inspector proxy currently exposes (iOS and Android). ' +\n 'Reports each candidate (id / title / appId / logicalDeviceId / ' +\n 'nativePageReloads / wsUrl) plus which target would be chosen or why ' +\n 'it is ambiguous. Use it to confirm Metro is running and your app is ' +\n 'registered. Requires a DEBUG build with Metro running.',\n inputSchema: {\n metroPort: z\n .number()\n .optional()\n .describe('Override the Metro inspector proxy port for this call.'),\n appId: z\n .string()\n .optional()\n .describe('Override the expected app bundle identifier.'),\n all: z\n .boolean()\n .optional()\n .describe(\n 'Bypass the appId filter and list ALL candidates (diagnostics).',\n ),\n },\n annotations: { readOnlyHint: true },\n },\n async ({ metroPort, appId, all }) => {\n try {\n const resolved = getHermesSession().resolve({\n metroPort,\n appId,\n platform: backend.platform,\n });\n const targets = await fetchDiscoveryTargets(\n resolved.metroPort,\n DISCOVERY_TIMEOUT_MS,\n );\n\n if (targets.length === 0) {\n return {\n content: [\n {\n type: 'text' as const,\n text: 'Metro not running or no debuggable app registered.',\n },\n ],\n };\n }\n\n const candidates = all\n ? targets\n : targets\n .filter((target) => Boolean(target.webSocketDebuggerUrl))\n .filter((target) => target.title !== LEGACY_SYNTHETIC_TITLE)\n .filter((target) => target.appId === resolved.appId);\n\n const lines: string[] = [\n `Metro port: ${resolved.metroPort}`,\n `Expected appId: ${resolved.appId}${all ? ' (filter bypassed by all=true)' : ''}`,\n `Targets discovered: ${targets.length}`,\n `Candidates listed: ${candidates.length}`,\n '',\n ];\n\n candidates.forEach((target, index) => {\n lines.push(`Candidate ${index + 1}:`, formatTarget(target), '');\n });\n\n const selection = selectHermesTarget(\n targets,\n resolved.appId,\n resolved.pinnedDeviceId,\n );\n if (selection.ok) {\n lines.push(\n `Chosen target: ${selection.target.id ?? '<unknown>'} (device=${\n selection.target.reactNative?.logicalDeviceId ?? '<missing>'\n })`,\n );\n } else if (\n selection.code === 'HERMES_MULTIPLE_DEVICES' &&\n hasAmbiguousTarget(candidates)\n ) {\n lines.push(`Ambiguous: ${selection.message}`);\n } else {\n lines.push(\n `No target chosen: [${selection.code}] ${selection.message}`,\n );\n }\n\n return {\n content: [{ type: 'text' as const, text: lines.join('\\n') }],\n };\n } catch (error) {\n const metroDown = describeMetroDown(error);\n if (metroDown) {\n return {\n content: [{ type: 'text' as const, text: metroDown }],\n };\n }\n return errorResult(error);\n }\n },\n );\n}\n"]}
@@ -0,0 +1,20 @@
1
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import type { DeviceBackend } from "../backends/types.cjs";
3
+ /**
4
+ * Registers the `hermes_targets` MCP tool, which lists and diagnoses the
5
+ * debuggable Hermes targets that Metro's inspector proxy currently exposes
6
+ * (iOS and Android).
7
+ *
8
+ * The handler is THIN: it resolves the Metro port/appId from
9
+ * {@link getHermesSession}, delegates discovery to {@link fetchDiscoveryTargets}
10
+ * and selection/ambiguity reporting to {@link selectHermesTarget} /
11
+ * {@link hasAmbiguousTarget}, and formats a readable report. With `all` set,
12
+ * it bypasses the appId filter so agents can discover the real appId / confirm
13
+ * Metro is up. No discovery or selection LOGIC lives in this file.
14
+ *
15
+ * @param server - The MCP server to register the tool on.
16
+ * @param backend - The lazy device backend, used at call time only to read the
17
+ * live platform so the session can resolve the correct default app bundle ID.
18
+ */
19
+ export declare function registerHermesTargetsTool(server: McpServer, backend: DeviceBackend): void;
20
+ //# sourceMappingURL=hermes-targets.d.cts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"hermes-targets.d.cts","sourceRoot":"","sources":["../../src/tools/hermes-targets.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,gDAAgD;AAIzE,OAAO,KAAK,EAAE,aAAa,EAAE,8BAA6B;AAkC1D;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,yBAAyB,CACvC,MAAM,EAAE,SAAS,EACjB,OAAO,EAAE,aAAa,GACrB,IAAI,CA4GN"}
@@ -0,0 +1,20 @@
1
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import type { DeviceBackend } from "../backends/types.mjs";
3
+ /**
4
+ * Registers the `hermes_targets` MCP tool, which lists and diagnoses the
5
+ * debuggable Hermes targets that Metro's inspector proxy currently exposes
6
+ * (iOS and Android).
7
+ *
8
+ * The handler is THIN: it resolves the Metro port/appId from
9
+ * {@link getHermesSession}, delegates discovery to {@link fetchDiscoveryTargets}
10
+ * and selection/ambiguity reporting to {@link selectHermesTarget} /
11
+ * {@link hasAmbiguousTarget}, and formats a readable report. With `all` set,
12
+ * it bypasses the appId filter so agents can discover the real appId / confirm
13
+ * Metro is up. No discovery or selection LOGIC lives in this file.
14
+ *
15
+ * @param server - The MCP server to register the tool on.
16
+ * @param backend - The lazy device backend, used at call time only to read the
17
+ * live platform so the session can resolve the correct default app bundle ID.
18
+ */
19
+ export declare function registerHermesTargetsTool(server: McpServer, backend: DeviceBackend): void;
20
+ //# sourceMappingURL=hermes-targets.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"hermes-targets.d.mts","sourceRoot":"","sources":["../../src/tools/hermes-targets.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,gDAAgD;AAIzE,OAAO,KAAK,EAAE,aAAa,EAAE,8BAA6B;AAkC1D;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,yBAAyB,CACvC,MAAM,EAAE,SAAS,EACjB,OAAO,EAAE,aAAa,GACrB,IAAI,CA4GN"}
@@ -0,0 +1,124 @@
1
+ import { z } from "zod";
2
+ import { errorResult } from "./shared.mjs";
3
+ import { fetchDiscoveryTargets, hasAmbiguousTarget, LEGACY_SYNTHETIC_TITLE, selectHermesTarget } from "../hermes/hermes-cdp.mjs";
4
+ import { getHermesSession } from "../hermes/session.mjs";
5
+ const DISCOVERY_TIMEOUT_MS = 10000;
6
+ function formatTarget(target) {
7
+ const { reactNative } = target;
8
+ return [
9
+ ` id: ${target.id ?? '<unknown>'}`,
10
+ ` title: ${target.title ?? '<unknown>'}`,
11
+ ` appId: ${target.appId ?? '<unknown>'}`,
12
+ ` logicalDeviceId: ${reactNative?.logicalDeviceId ?? '<missing>'}`,
13
+ ` nativePageReloads: ${reactNative?.capabilities?.nativePageReloads ?? '<unset>'}`,
14
+ ` wsUrl: ${target.webSocketDebuggerUrl ?? '<missing>'}`,
15
+ ].join('\n');
16
+ }
17
+ function describeMetroDown(error) {
18
+ const message = error instanceof Error ? error.message : String(error);
19
+ if (message.includes('ECONNREFUSED') || message.includes('fetch failed')) {
20
+ return 'Metro not running or no debuggable app registered.';
21
+ }
22
+ return undefined;
23
+ }
24
+ /**
25
+ * Registers the `hermes_targets` MCP tool, which lists and diagnoses the
26
+ * debuggable Hermes targets that Metro's inspector proxy currently exposes
27
+ * (iOS and Android).
28
+ *
29
+ * The handler is THIN: it resolves the Metro port/appId from
30
+ * {@link getHermesSession}, delegates discovery to {@link fetchDiscoveryTargets}
31
+ * and selection/ambiguity reporting to {@link selectHermesTarget} /
32
+ * {@link hasAmbiguousTarget}, and formats a readable report. With `all` set,
33
+ * it bypasses the appId filter so agents can discover the real appId / confirm
34
+ * Metro is up. No discovery or selection LOGIC lives in this file.
35
+ *
36
+ * @param server - The MCP server to register the tool on.
37
+ * @param backend - The lazy device backend, used at call time only to read the
38
+ * live platform so the session can resolve the correct default app bundle ID.
39
+ */
40
+ export function registerHermesTargetsTool(server, backend) {
41
+ server.registerTool('hermes_targets', {
42
+ title: 'Hermes Targets',
43
+ description: 'List and diagnose the debuggable React Native Hermes targets that ' +
44
+ 'Metro\u2019s inspector proxy currently exposes (iOS and Android). ' +
45
+ 'Reports each candidate (id / title / appId / logicalDeviceId / ' +
46
+ 'nativePageReloads / wsUrl) plus which target would be chosen or why ' +
47
+ 'it is ambiguous. Use it to confirm Metro is running and your app is ' +
48
+ 'registered. Requires a DEBUG build with Metro running.',
49
+ inputSchema: {
50
+ metroPort: z
51
+ .number()
52
+ .optional()
53
+ .describe('Override the Metro inspector proxy port for this call.'),
54
+ appId: z
55
+ .string()
56
+ .optional()
57
+ .describe('Override the expected app bundle identifier.'),
58
+ all: z
59
+ .boolean()
60
+ .optional()
61
+ .describe('Bypass the appId filter and list ALL candidates (diagnostics).'),
62
+ },
63
+ annotations: { readOnlyHint: true },
64
+ }, async ({ metroPort, appId, all }) => {
65
+ try {
66
+ const resolved = getHermesSession().resolve({
67
+ metroPort,
68
+ appId,
69
+ platform: backend.platform,
70
+ });
71
+ const targets = await fetchDiscoveryTargets(resolved.metroPort, DISCOVERY_TIMEOUT_MS);
72
+ if (targets.length === 0) {
73
+ return {
74
+ content: [
75
+ {
76
+ type: 'text',
77
+ text: 'Metro not running or no debuggable app registered.',
78
+ },
79
+ ],
80
+ };
81
+ }
82
+ const candidates = all
83
+ ? targets
84
+ : targets
85
+ .filter((target) => Boolean(target.webSocketDebuggerUrl))
86
+ .filter((target) => target.title !== LEGACY_SYNTHETIC_TITLE)
87
+ .filter((target) => target.appId === resolved.appId);
88
+ const lines = [
89
+ `Metro port: ${resolved.metroPort}`,
90
+ `Expected appId: ${resolved.appId}${all ? ' (filter bypassed by all=true)' : ''}`,
91
+ `Targets discovered: ${targets.length}`,
92
+ `Candidates listed: ${candidates.length}`,
93
+ '',
94
+ ];
95
+ candidates.forEach((target, index) => {
96
+ lines.push(`Candidate ${index + 1}:`, formatTarget(target), '');
97
+ });
98
+ const selection = selectHermesTarget(targets, resolved.appId, resolved.pinnedDeviceId);
99
+ if (selection.ok) {
100
+ lines.push(`Chosen target: ${selection.target.id ?? '<unknown>'} (device=${selection.target.reactNative?.logicalDeviceId ?? '<missing>'})`);
101
+ }
102
+ else if (selection.code === 'HERMES_MULTIPLE_DEVICES' &&
103
+ hasAmbiguousTarget(candidates)) {
104
+ lines.push(`Ambiguous: ${selection.message}`);
105
+ }
106
+ else {
107
+ lines.push(`No target chosen: [${selection.code}] ${selection.message}`);
108
+ }
109
+ return {
110
+ content: [{ type: 'text', text: lines.join('\n') }],
111
+ };
112
+ }
113
+ catch (error) {
114
+ const metroDown = describeMetroDown(error);
115
+ if (metroDown) {
116
+ return {
117
+ content: [{ type: 'text', text: metroDown }],
118
+ };
119
+ }
120
+ return errorResult(error);
121
+ }
122
+ });
123
+ }
124
+ //# sourceMappingURL=hermes-targets.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"hermes-targets.mjs","sourceRoot":"","sources":["../../src/tools/hermes-targets.ts"],"names":[],"mappings":"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","sourcesContent":["import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';\nimport { z } from 'zod';\n\nimport { errorResult } from './shared.js';\nimport type { DeviceBackend } from '../backends/types.js';\nimport {\n fetchDiscoveryTargets,\n hasAmbiguousTarget,\n LEGACY_SYNTHETIC_TITLE,\n selectHermesTarget,\n} from '../hermes/hermes-cdp.js';\nimport type { HermesTarget } from '../hermes/hermes-cdp.js';\nimport { getHermesSession } from '../hermes/session.js';\n\nconst DISCOVERY_TIMEOUT_MS = 10_000;\n\nfunction formatTarget(target: HermesTarget): string {\n const { reactNative } = target;\n return [\n ` id: ${target.id ?? '<unknown>'}`,\n ` title: ${target.title ?? '<unknown>'}`,\n ` appId: ${target.appId ?? '<unknown>'}`,\n ` logicalDeviceId: ${reactNative?.logicalDeviceId ?? '<missing>'}`,\n ` nativePageReloads: ${\n reactNative?.capabilities?.nativePageReloads ?? '<unset>'\n }`,\n ` wsUrl: ${target.webSocketDebuggerUrl ?? '<missing>'}`,\n ].join('\\n');\n}\n\nfunction describeMetroDown(error: unknown): string | undefined {\n const message = error instanceof Error ? error.message : String(error);\n if (message.includes('ECONNREFUSED') || message.includes('fetch failed')) {\n return 'Metro not running or no debuggable app registered.';\n }\n return undefined;\n}\n\n/**\n * Registers the `hermes_targets` MCP tool, which lists and diagnoses the\n * debuggable Hermes targets that Metro's inspector proxy currently exposes\n * (iOS and Android).\n *\n * The handler is THIN: it resolves the Metro port/appId from\n * {@link getHermesSession}, delegates discovery to {@link fetchDiscoveryTargets}\n * and selection/ambiguity reporting to {@link selectHermesTarget} /\n * {@link hasAmbiguousTarget}, and formats a readable report. With `all` set,\n * it bypasses the appId filter so agents can discover the real appId / confirm\n * Metro is up. No discovery or selection LOGIC lives in this file.\n *\n * @param server - The MCP server to register the tool on.\n * @param backend - The lazy device backend, used at call time only to read the\n * live platform so the session can resolve the correct default app bundle ID.\n */\nexport function registerHermesTargetsTool(\n server: McpServer,\n backend: DeviceBackend,\n): void {\n server.registerTool(\n 'hermes_targets',\n {\n title: 'Hermes Targets',\n description:\n 'List and diagnose the debuggable React Native Hermes targets that ' +\n 'Metro\\u2019s inspector proxy currently exposes (iOS and Android). ' +\n 'Reports each candidate (id / title / appId / logicalDeviceId / ' +\n 'nativePageReloads / wsUrl) plus which target would be chosen or why ' +\n 'it is ambiguous. Use it to confirm Metro is running and your app is ' +\n 'registered. Requires a DEBUG build with Metro running.',\n inputSchema: {\n metroPort: z\n .number()\n .optional()\n .describe('Override the Metro inspector proxy port for this call.'),\n appId: z\n .string()\n .optional()\n .describe('Override the expected app bundle identifier.'),\n all: z\n .boolean()\n .optional()\n .describe(\n 'Bypass the appId filter and list ALL candidates (diagnostics).',\n ),\n },\n annotations: { readOnlyHint: true },\n },\n async ({ metroPort, appId, all }) => {\n try {\n const resolved = getHermesSession().resolve({\n metroPort,\n appId,\n platform: backend.platform,\n });\n const targets = await fetchDiscoveryTargets(\n resolved.metroPort,\n DISCOVERY_TIMEOUT_MS,\n );\n\n if (targets.length === 0) {\n return {\n content: [\n {\n type: 'text' as const,\n text: 'Metro not running or no debuggable app registered.',\n },\n ],\n };\n }\n\n const candidates = all\n ? targets\n : targets\n .filter((target) => Boolean(target.webSocketDebuggerUrl))\n .filter((target) => target.title !== LEGACY_SYNTHETIC_TITLE)\n .filter((target) => target.appId === resolved.appId);\n\n const lines: string[] = [\n `Metro port: ${resolved.metroPort}`,\n `Expected appId: ${resolved.appId}${all ? ' (filter bypassed by all=true)' : ''}`,\n `Targets discovered: ${targets.length}`,\n `Candidates listed: ${candidates.length}`,\n '',\n ];\n\n candidates.forEach((target, index) => {\n lines.push(`Candidate ${index + 1}:`, formatTarget(target), '');\n });\n\n const selection = selectHermesTarget(\n targets,\n resolved.appId,\n resolved.pinnedDeviceId,\n );\n if (selection.ok) {\n lines.push(\n `Chosen target: ${selection.target.id ?? '<unknown>'} (device=${\n selection.target.reactNative?.logicalDeviceId ?? '<missing>'\n })`,\n );\n } else if (\n selection.code === 'HERMES_MULTIPLE_DEVICES' &&\n hasAmbiguousTarget(candidates)\n ) {\n lines.push(`Ambiguous: ${selection.message}`);\n } else {\n lines.push(\n `No target chosen: [${selection.code}] ${selection.message}`,\n );\n }\n\n return {\n content: [{ type: 'text' as const, text: lines.join('\\n') }],\n };\n } catch (error) {\n const metroDown = describeMetroDown(error);\n if (metroDown) {\n return {\n content: [{ type: 'text' as const, text: metroDown }],\n };\n }\n return errorResult(error);\n }\n },\n );\n}\n"]}