react-native-mcp-kit 1.0.1 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (164) hide show
  1. package/README.md +220 -720
  2. package/dist/babel/stripPlugin.d.ts.map +1 -1
  3. package/dist/babel/stripPlugin.js +40 -3
  4. package/dist/babel/stripPlugin.js.map +1 -1
  5. package/dist/bin/ios-hid +0 -0
  6. package/dist/client/contexts/McpContext/McpProvider.d.ts +1 -1
  7. package/dist/client/contexts/McpContext/McpProvider.d.ts.map +1 -1
  8. package/dist/client/contexts/McpContext/McpProvider.js +67 -4
  9. package/dist/client/contexts/McpContext/McpProvider.js.map +1 -1
  10. package/dist/client/contexts/McpContext/types.d.ts +11 -1
  11. package/dist/client/contexts/McpContext/types.d.ts.map +1 -1
  12. package/dist/client/core/McpClient.d.ts +7 -0
  13. package/dist/client/core/McpClient.d.ts.map +1 -1
  14. package/dist/client/core/McpClient.js +73 -2
  15. package/dist/client/core/McpClient.js.map +1 -1
  16. package/dist/index.d.ts +1 -1
  17. package/dist/index.d.ts.map +1 -1
  18. package/dist/index.js +1 -2
  19. package/dist/index.js.map +1 -1
  20. package/dist/modules/device/device.d.ts.map +1 -1
  21. package/dist/modules/device/device.js +16 -4
  22. package/dist/modules/device/device.js.map +1 -1
  23. package/dist/modules/fiberTree/fiberTree.d.ts.map +1 -1
  24. package/dist/modules/fiberTree/fiberTree.js +61 -15
  25. package/dist/modules/fiberTree/fiberTree.js.map +1 -1
  26. package/dist/modules/fiberTree/types.d.ts +9 -0
  27. package/dist/modules/fiberTree/types.d.ts.map +1 -1
  28. package/dist/modules/fiberTree/utils.d.ts +2 -1
  29. package/dist/modules/fiberTree/utils.d.ts.map +1 -1
  30. package/dist/modules/fiberTree/utils.js +57 -1
  31. package/dist/modules/fiberTree/utils.js.map +1 -1
  32. package/dist/modules/index.d.ts +0 -1
  33. package/dist/modules/index.d.ts.map +1 -1
  34. package/dist/modules/index.js +1 -3
  35. package/dist/modules/index.js.map +1 -1
  36. package/dist/modules/navigation/navigation.d.ts.map +1 -1
  37. package/dist/modules/navigation/navigation.js +32 -40
  38. package/dist/modules/navigation/navigation.js.map +1 -1
  39. package/dist/modules/navigation/types.d.ts +2 -1
  40. package/dist/modules/navigation/types.d.ts.map +1 -1
  41. package/dist/server/bridge.d.ts +38 -12
  42. package/dist/server/bridge.d.ts.map +1 -1
  43. package/dist/server/bridge.js +136 -56
  44. package/dist/server/bridge.js.map +1 -1
  45. package/dist/server/cli.js +10 -1
  46. package/dist/server/cli.js.map +1 -1
  47. package/dist/server/host/deviceResolver.d.ts +53 -0
  48. package/dist/server/host/deviceResolver.d.ts.map +1 -0
  49. package/dist/server/host/deviceResolver.js +555 -0
  50. package/dist/server/host/deviceResolver.js.map +1 -0
  51. package/dist/server/host/helpers.d.ts +33 -0
  52. package/dist/server/host/helpers.d.ts.map +1 -0
  53. package/dist/server/host/helpers.js +42 -0
  54. package/dist/server/host/helpers.js.map +1 -0
  55. package/dist/server/host/hostModule.d.ts +4 -0
  56. package/dist/server/host/hostModule.d.ts.map +1 -0
  57. package/dist/server/host/hostModule.js +26 -0
  58. package/dist/server/host/hostModule.js.map +1 -0
  59. package/dist/server/host/index.d.ts +4 -0
  60. package/dist/server/host/index.d.ts.map +1 -0
  61. package/dist/server/host/index.js +8 -0
  62. package/dist/server/host/index.js.map +1 -0
  63. package/dist/server/host/iosInput.d.ts +15 -0
  64. package/dist/server/host/iosInput.d.ts.map +1 -0
  65. package/dist/server/host/iosInput.js +93 -0
  66. package/dist/server/host/iosInput.js.map +1 -0
  67. package/dist/server/host/modules/screenshot.d.ts +4 -0
  68. package/dist/server/host/modules/screenshot.d.ts.map +1 -0
  69. package/dist/server/host/modules/screenshot.js +615 -0
  70. package/dist/server/host/modules/screenshot.js.map +1 -0
  71. package/dist/server/host/processRunner.d.ts +19 -0
  72. package/dist/server/host/processRunner.d.ts.map +1 -0
  73. package/dist/server/host/processRunner.js +58 -0
  74. package/dist/server/host/processRunner.js.map +1 -0
  75. package/dist/server/host/tools/capture.d.ts +6 -0
  76. package/dist/server/host/tools/capture.d.ts.map +1 -0
  77. package/dist/server/host/tools/capture.js +148 -0
  78. package/dist/server/host/tools/capture.js.map +1 -0
  79. package/dist/server/host/tools/devices.d.ts +4 -0
  80. package/dist/server/host/tools/devices.d.ts.map +1 -0
  81. package/dist/server/host/tools/devices.js +17 -0
  82. package/dist/server/host/tools/devices.js.map +1 -0
  83. package/dist/server/host/tools/input.d.ts +7 -0
  84. package/dist/server/host/tools/input.d.ts.map +1 -0
  85. package/dist/server/host/tools/input.js +286 -0
  86. package/dist/server/host/tools/input.js.map +1 -0
  87. package/dist/server/host/tools/lifecycle.d.ts +6 -0
  88. package/dist/server/host/tools/lifecycle.d.ts.map +1 -0
  89. package/dist/server/host/tools/lifecycle.js +271 -0
  90. package/dist/server/host/tools/lifecycle.js.map +1 -0
  91. package/dist/server/host/types.d.ts +17 -0
  92. package/dist/server/host/types.d.ts.map +1 -0
  93. package/dist/{modules/components → server/host}/types.js.map +1 -1
  94. package/dist/server/host/wda.d.ts +15 -0
  95. package/dist/server/host/wda.d.ts.map +1 -0
  96. package/dist/server/host/wda.js +100 -0
  97. package/dist/server/host/wda.js.map +1 -0
  98. package/dist/server/index.d.ts.map +1 -1
  99. package/dist/server/index.js +10 -17
  100. package/dist/server/index.js.map +1 -1
  101. package/dist/server/mcpServer.d.ts +5 -10
  102. package/dist/server/mcpServer.d.ts.map +1 -1
  103. package/dist/server/mcpServer.js +438 -153
  104. package/dist/server/mcpServer.js.map +1 -1
  105. package/dist/server/types.d.ts +2 -8
  106. package/dist/server/types.d.ts.map +1 -1
  107. package/dist/shared/protocol.d.ts +6 -0
  108. package/dist/shared/protocol.d.ts.map +1 -1
  109. package/package.json +6 -12
  110. package/dist/client/contexts/McpTreeContext/McpTracker.d.ts +0 -10
  111. package/dist/client/contexts/McpTreeContext/McpTracker.d.ts.map +0 -1
  112. package/dist/client/contexts/McpTreeContext/McpTracker.js +0 -101
  113. package/dist/client/contexts/McpTreeContext/McpTracker.js.map +0 -1
  114. package/dist/client/contexts/McpTreeContext/McpTreeContext.d.ts +0 -2
  115. package/dist/client/contexts/McpTreeContext/McpTreeContext.d.ts.map +0 -1
  116. package/dist/client/contexts/McpTreeContext/McpTreeContext.js +0 -6
  117. package/dist/client/contexts/McpTreeContext/McpTreeContext.js.map +0 -1
  118. package/dist/client/contexts/McpTreeContext/McpTreeRegistry.d.ts +0 -16
  119. package/dist/client/contexts/McpTreeContext/McpTreeRegistry.d.ts.map +0 -1
  120. package/dist/client/contexts/McpTreeContext/McpTreeRegistry.js +0 -96
  121. package/dist/client/contexts/McpTreeContext/McpTreeRegistry.js.map +0 -1
  122. package/dist/client/contexts/McpTreeContext/index.d.ts +0 -5
  123. package/dist/client/contexts/McpTreeContext/index.d.ts.map +0 -1
  124. package/dist/client/contexts/McpTreeContext/index.js +0 -10
  125. package/dist/client/contexts/McpTreeContext/index.js.map +0 -1
  126. package/dist/client/contexts/McpTreeContext/types.d.ts +0 -14
  127. package/dist/client/contexts/McpTreeContext/types.d.ts.map +0 -1
  128. package/dist/client/contexts/McpTreeContext/types.js.map +0 -1
  129. package/dist/modules/components/components.d.ts +0 -8
  130. package/dist/modules/components/components.d.ts.map +0 -1
  131. package/dist/modules/components/components.js +0 -315
  132. package/dist/modules/components/components.js.map +0 -1
  133. package/dist/modules/components/index.d.ts +0 -3
  134. package/dist/modules/components/index.d.ts.map +0 -1
  135. package/dist/modules/components/index.js +0 -6
  136. package/dist/modules/components/index.js.map +0 -1
  137. package/dist/modules/components/types.d.ts +0 -18
  138. package/dist/modules/components/types.d.ts.map +0 -1
  139. package/dist/modules/components/types.js +0 -3
  140. package/dist/modules/components/utils.d.ts +0 -18
  141. package/dist/modules/components/utils.d.ts.map +0 -1
  142. package/dist/modules/components/utils.js +0 -386
  143. package/dist/modules/components/utils.js.map +0 -1
  144. package/dist/modules/screenshot/index.d.ts +0 -3
  145. package/dist/modules/screenshot/index.d.ts.map +0 -1
  146. package/dist/modules/screenshot/index.js +0 -6
  147. package/dist/modules/screenshot/index.js.map +0 -1
  148. package/dist/modules/screenshot/screenshot.d.ts +0 -4
  149. package/dist/modules/screenshot/screenshot.d.ts.map +0 -1
  150. package/dist/modules/screenshot/screenshot.js +0 -89
  151. package/dist/modules/screenshot/screenshot.js.map +0 -1
  152. package/dist/modules/screenshot/types.d.ts +0 -5
  153. package/dist/modules/screenshot/types.d.ts.map +0 -1
  154. package/dist/modules/screenshot/types.js +0 -3
  155. package/dist/modules/screenshot/types.js.map +0 -1
  156. package/dist/modules/tree/index.d.ts +0 -2
  157. package/dist/modules/tree/index.d.ts.map +0 -1
  158. package/dist/modules/tree/index.js +0 -6
  159. package/dist/modules/tree/index.js.map +0 -1
  160. package/dist/modules/tree/tree.d.ts +0 -3
  161. package/dist/modules/tree/tree.d.ts.map +0 -1
  162. package/dist/modules/tree/tree.js +0 -274
  163. package/dist/modules/tree/tree.js.map +0 -1
  164. /package/dist/{client/contexts/McpTreeContext → server/host}/types.js +0 -0
@@ -7,39 +7,117 @@ const zod_1 = require("zod");
7
7
  const protocol_1 = require("../shared/protocol");
8
8
  const BASE_INSTRUCTIONS = `You are connected to a running React Native app via the react-native-mcp-kit bridge.
9
9
 
10
+ Multiple React Native apps can connect simultaneously — each is identified by a short ID like "ios-1", "android-1", or "client-1". Use \`connection_status\` or \`list_tools\` to see which clients are connected and their IDs, platforms, and labels.
11
+
10
12
  ## How to interact
11
13
 
12
- 1. Use \`connection_status\` to check if the app is connected
13
- 2. Use \`list_tools\` to see all available tools with descriptions and examples
14
- 3. Use \`call\` to invoke any tool with format: module${protocol_1.MODULE_SEPARATOR}method (e.g. navigation${protocol_1.MODULE_SEPARATOR}navigate)
15
- 4. Use \`state_list\` / \`state_get\` to read app state exposed by the developer
14
+ 1. Use \`connection_status\` to check which clients are connected.
15
+ 2. Use \`list_tools\` to browse all available tool names and short descriptions. The response is compact — modules that are structurally identical across multiple clients are deduplicated into a single entry with a \`clientIds\` array, and input schemas are omitted.
16
+ 3. Use \`describe_tool\` with \`{ tool, clientId? }\` to fetch the full input schema of a specific tool before calling it. Required when you need to know the argument shape. Host tools are resolved directly (no clientId needed). For in-app tools, omit \`clientId\` to auto-pick; specify it only when multiple clients have the same tool with different schemas.
17
+ 4. Use \`call\` to invoke any tool with format: module${protocol_1.MODULE_SEPARATOR}method (e.g. navigation${protocol_1.MODULE_SEPARATOR}navigate). When more than one client is connected, specify \`clientId\`. When exactly one client is connected, \`clientId\` is optional — it's auto-picked.
18
+ 5. Use \`state_list\` / \`state_get\` to read app state exposed via useMcpState. State is scoped per client; specify \`clientId\` when multiple clients are connected.
19
+
20
+ Some tools run inline on the MCP server host (e.g. \`host${protocol_1.MODULE_SEPARATOR}screenshot\`, \`host${protocol_1.MODULE_SEPARATOR}list_devices\`, \`host${protocol_1.MODULE_SEPARATOR}launch_app\`, \`host${protocol_1.MODULE_SEPARATOR}terminate_app\`, \`host${protocol_1.MODULE_SEPARATOR}restart_app\`) and work even when no React Native client is connected. They use xcrun simctl / adb on the dev machine. When \`clientId\` is provided, host tools use that client's platform/label/deviceId as hints to resolve the target device; otherwise they prefer the device of the single connected client, falling back to the single booted sim / online device. \`launch_app\`, \`terminate_app\`, and \`restart_app\` accept an \`appId\` arg (iOS bundle ID / Android package name); omit it to reuse the target client's registered \`bundleId\` from its connection metadata.
21
+
22
+ ## Driving the UI — pick the right tool
23
+ 1. **\`fiber_tree${protocol_1.MODULE_SEPARATOR}find_all\` with \`select: ["mcpId", "name", "bounds"]\` + \`host${protocol_1.MODULE_SEPARATOR}tap\` with \`bounds.centerX\`/\`bounds.centerY\`** — the default for touch interactions. Exercises the real OS gesture pipeline, so Pressable/TouchableOpacity feedback, gesture responders, and hit-test logic all run. The bounds come back in physical pixels — pass them straight to \`host${protocol_1.MODULE_SEPARATOR}tap\`, no scaling. The \`select\` parameter also saves tokens by omitting heavy \`props\` from the response.
24
+ 2. **\`fiber_tree${protocol_1.MODULE_SEPARATOR}invoke\`** when you need to bypass the gesture pipeline — e.g. for non-tap callbacks like \`onChangeText\` / \`onValueChange\` / \`onRefresh\`, or when a component is inside a scroll/gesture-handler parent that swallows taps. Calls the prop directly — faster and immune to overlay/occlusion, but does not exercise touch handlers.
25
+ 3. **\`host${protocol_1.MODULE_SEPARATOR}screenshot\` + manual coordinate estimation + \`host${protocol_1.MODULE_SEPARATOR}tap\`** ONLY for non-React surfaces: system permission dialogs, native alerts, the on-screen keyboard, WebView content, native splash. These have no fiber and no bounds.
26
+
27
+ \`host${protocol_1.MODULE_SEPARATOR}tap\` / \`host${protocol_1.MODULE_SEPARATOR}swipe\` / \`host${protocol_1.MODULE_SEPARATOR}type_text\` / \`host${protocol_1.MODULE_SEPARATOR}press_key\` work on both platforms with no external daemons: Android via \`adb shell input\`, iOS via a bundled \`ios-hid\` binary that injects HID events directly into iOS Simulator through SimulatorKit.
16
28
  `;
29
+ const jsonError = (msg) => {
30
+ return {
31
+ content: [{ text: JSON.stringify({ error: msg }), type: 'text' }],
32
+ };
33
+ };
34
+ /**
35
+ * Recursively serializes a value to JSON with sorted object keys, producing a
36
+ * stable canonical form that's safe to use as a dedup Map key. Arrays keep
37
+ * their original order — caller is responsible for normalizing them when
38
+ * order-independence is desired.
39
+ */
40
+ const canonicalize = (value) => {
41
+ return JSON.stringify(value, (_key, v) => {
42
+ if (v && typeof v === 'object' && !Array.isArray(v)) {
43
+ const sorted = {};
44
+ for (const k of Object.keys(v).sort()) {
45
+ sorted[k] = v[k];
46
+ }
47
+ return sorted;
48
+ }
49
+ return v;
50
+ });
51
+ };
52
+ /**
53
+ * Produces a canonical key for a ToolGroup that's independent of tool
54
+ * registration order. Two modules with the same name + tools (regardless of
55
+ * order) + descriptions + schemas produce the same key.
56
+ */
57
+ const canonicalizeGroup = (group) => {
58
+ const normalized = {
59
+ description: group.description,
60
+ module: group.module,
61
+ tools: [...group.tools].sort((a, b) => {
62
+ return a.name.localeCompare(b.name);
63
+ }),
64
+ };
65
+ return canonicalize(normalized);
66
+ };
67
+ /**
68
+ * Looks up a full tool descriptor on a client by its full name
69
+ * (`module__method`). Checks both static modules and dynamic tools registered
70
+ * via useMcpTool. Returns null if the tool is not on this client.
71
+ */
72
+ const findToolInClient = (client, toolFullName) => {
73
+ for (const mod of client.modules) {
74
+ const prefix = `${mod.name}${protocol_1.MODULE_SEPARATOR}`;
75
+ if (toolFullName.startsWith(prefix)) {
76
+ const methodName = toolFullName.slice(prefix.length);
77
+ const toolDef = mod.tools.find((t) => {
78
+ return t.name === methodName;
79
+ });
80
+ if (toolDef) {
81
+ return {
82
+ description: toolDef.description,
83
+ inputSchema: toolDef.inputSchema,
84
+ name: toolFullName,
85
+ };
86
+ }
87
+ }
88
+ }
89
+ const dynamicEntry = client.dynamicTools.get(toolFullName);
90
+ if (dynamicEntry) {
91
+ return {
92
+ description: dynamicEntry.description,
93
+ inputSchema: dynamicEntry.inputSchema,
94
+ name: toolFullName,
95
+ };
96
+ }
97
+ return null;
98
+ };
17
99
  class McpServerWrapper {
18
100
  bridge;
19
- dynamicTools = new Map();
101
+ hostModules;
102
+ hostToolMap = new Map();
20
103
  mcp;
21
- modules = [];
22
- stateStore = new Map();
23
- constructor(bridge) {
104
+ constructor(bridge, hostModules = []) {
24
105
  this.bridge = bridge;
106
+ this.hostModules = hostModules;
107
+ for (const mod of hostModules) {
108
+ for (const [toolName, tool] of Object.entries(mod.tools)) {
109
+ const fullName = `${mod.name}${protocol_1.MODULE_SEPARATOR}${toolName}`;
110
+ this.hostToolMap.set(fullName, {
111
+ handler: tool.handler,
112
+ moduleName: mod.name,
113
+ timeout: tool.timeout,
114
+ toolName,
115
+ });
116
+ }
117
+ }
25
118
  this.mcp = new mcp_js_1.McpServer({ name: 'react-native-mcp-kit', version: '1.0.0' }, { instructions: BASE_INSTRUCTIONS });
26
119
  this.registerTools();
27
120
  }
28
- addDynamicTool(module, name, description) {
29
- this.dynamicTools.set(`${module}${protocol_1.MODULE_SEPARATOR}${name}`, { description, module });
30
- }
31
- removeDynamicTool(module, name) {
32
- this.dynamicTools.delete(`${module}${protocol_1.MODULE_SEPARATOR}${name}`);
33
- }
34
- setModules(modules) {
35
- this.modules = modules;
36
- }
37
- setState(key, value) {
38
- this.stateStore.set(key, value);
39
- }
40
- removeState(key) {
41
- this.stateStore.delete(key);
42
- }
43
121
  async start() {
44
122
  const transport = new stdio_js_1.StdioServerTransport();
45
123
  await this.mcp.connect(transport);
@@ -50,32 +128,54 @@ class McpServerWrapper {
50
128
  openWorldHint: true,
51
129
  title: 'Call Tool',
52
130
  },
53
- description: 'Call a tool registered by the React Native app. Use list_tools first to see available tools.',
131
+ description: 'Call a tool registered by a React Native app client. Use list_tools first to see available tools. When multiple clients are connected, specify clientId; otherwise it is auto-picked.',
54
132
  inputSchema: {
55
133
  args: zod_1.z
56
134
  .string()
57
135
  .optional()
58
136
  .describe('Arguments as JSON string (e.g. {"screen": "AUTH_LOGIN_SCREEN"})'),
137
+ clientId: zod_1.z
138
+ .string()
139
+ .optional()
140
+ .describe('Target client ID (e.g. "ios-1", "android-1"). Optional when exactly one client is connected.'),
59
141
  tool: zod_1.z
60
142
  .string()
61
143
  .describe(`Tool name in format "module${protocol_1.MODULE_SEPARATOR}method" (e.g. "navigation${protocol_1.MODULE_SEPARATOR}navigate")`),
62
144
  },
63
- }, async ({ args, tool }) => {
64
- if (!this.bridge.isClientConnected()) {
65
- return {
66
- content: [
67
- {
68
- text: JSON.stringify({ error: 'React Native app is not connected' }),
69
- type: 'text',
70
- },
71
- ],
72
- };
145
+ }, async ({ args, clientId, tool }) => {
146
+ let parsedArgs = {};
147
+ if (args) {
148
+ try {
149
+ parsedArgs = JSON.parse(args);
150
+ }
151
+ catch {
152
+ return jsonError('Invalid JSON in args');
153
+ }
73
154
  }
74
- // Find the module by matching prefix
155
+ // Host dispatch — runs inline on the Node server, may work without any connected client
156
+ const hostEntry = this.hostToolMap.get(tool);
157
+ if (hostEntry) {
158
+ try {
159
+ const result = await hostEntry.handler(parsedArgs, {
160
+ bridge: this.bridge,
161
+ requestedClientId: clientId,
162
+ });
163
+ return { content: this.formatResult(result) };
164
+ }
165
+ catch (err) {
166
+ return jsonError(`Host tool "${tool}" threw: ${err.message}`);
167
+ }
168
+ }
169
+ const resolution = this.bridge.resolveClient(clientId);
170
+ if (!resolution.ok) {
171
+ return jsonError(resolution.error);
172
+ }
173
+ const client = resolution.client;
174
+ // Find the module by matching prefix in this client's modules
75
175
  let mod;
76
176
  let moduleName = '';
77
177
  let methodName = '';
78
- for (const m of this.modules) {
178
+ for (const m of client.modules) {
79
179
  const prefix = `${m.name}${protocol_1.MODULE_SEPARATOR}`;
80
180
  if (tool.startsWith(prefix)) {
81
181
  mod = m;
@@ -84,7 +184,7 @@ class McpServerWrapper {
84
184
  break;
85
185
  }
86
186
  }
87
- // If no module matched, check for dynamic tool prefix
187
+ // If no module matched, check for dynamic tool prefix or generic module__method
88
188
  if (!mod) {
89
189
  if (tool.startsWith(protocol_1.DYNAMIC_PREFIX)) {
90
190
  moduleName = `${protocol_1.MODULE_SEPARATOR}dynamic`;
@@ -93,79 +193,37 @@ class McpServerWrapper {
93
193
  else {
94
194
  const idx = tool.indexOf(protocol_1.MODULE_SEPARATOR);
95
195
  if (idx <= 0) {
96
- return {
97
- content: [
98
- {
99
- text: JSON.stringify({
100
- error: `Invalid tool name "${tool}". Use "module${protocol_1.MODULE_SEPARATOR}method" format.`,
101
- }),
102
- type: 'text',
103
- },
104
- ],
105
- };
196
+ return jsonError(`Invalid tool name "${tool}". Use "module${protocol_1.MODULE_SEPARATOR}method" format.`);
106
197
  }
107
198
  moduleName = tool.slice(0, idx);
108
199
  methodName = tool.slice(idx + protocol_1.MODULE_SEPARATOR.length);
109
200
  }
110
- }
111
- let parsedArgs = {};
112
- if (args) {
201
+ // Try dispatching via bridge — might be a dynamic tool registered on this client
113
202
  try {
114
- parsedArgs = JSON.parse(args);
115
- }
116
- catch {
117
- return {
118
- content: [
119
- { text: JSON.stringify({ error: 'Invalid JSON in args' }), type: 'text' },
120
- ],
121
- };
122
- }
123
- }
124
- if (!mod) {
125
- // No module matched — try as dynamic tool via bridge
126
- try {
127
- const result = await this.bridge.call(moduleName, methodName, parsedArgs);
203
+ const result = await this.bridge.call(client.id, moduleName, methodName, parsedArgs);
128
204
  return { content: this.formatResult(result) };
129
205
  }
130
206
  catch {
131
- const allModules = this.modules
207
+ const allModules = client.modules
132
208
  .map((m) => {
133
209
  return m.name;
134
210
  })
135
211
  .join(', ');
136
- const dynNames = [...this.dynamicTools.keys()].join(', ');
137
- return {
138
- content: [
139
- {
140
- text: JSON.stringify({
141
- error: `Tool "${tool}" not found. Modules: ${allModules}. Dynamic: ${dynNames || 'none'}`,
142
- }),
143
- type: 'text',
144
- },
145
- ],
146
- };
212
+ const dynNames = [...client.dynamicTools.keys()].join(', ');
213
+ return jsonError(`Tool "${tool}" not found on client '${client.id}'. Modules: ${allModules || '(none)'}. Dynamic: ${dynNames || '(none)'}`);
147
214
  }
148
215
  }
149
216
  const toolDef = mod.tools.find((t) => {
150
217
  return t.name === methodName;
151
218
  });
152
219
  if (!toolDef) {
153
- return {
154
- content: [
155
- {
156
- text: JSON.stringify({
157
- error: `Tool "${methodName}" not found in module "${moduleName}". Available: ${mod.tools
158
- .map((t) => {
159
- return t.name;
160
- })
161
- .join(', ')}`,
162
- }),
163
- type: 'text',
164
- },
165
- ],
166
- };
220
+ return jsonError(`Tool "${methodName}" not found in module "${moduleName}" on client '${client.id}'. Available: ${mod.tools
221
+ .map((t) => {
222
+ return t.name;
223
+ })
224
+ .join(', ')}`);
167
225
  }
168
- const result = await this.bridge.call(moduleName, methodName, parsedArgs, toolDef.timeout);
226
+ const result = await this.bridge.call(client.id, moduleName, methodName, parsedArgs, toolDef.timeout);
169
227
  return { content: this.formatResult(result) };
170
228
  });
171
229
  this.mcp.registerTool('list_tools', {
@@ -173,56 +231,71 @@ class McpServerWrapper {
173
231
  readOnlyHint: true,
174
232
  title: 'List Tools',
175
233
  },
176
- description: 'List all tools registered by the React Native app, grouped by module',
234
+ description: 'Browse available tools with compact (schema-free) descriptions. Modules with identical shape across multiple clients are deduplicated into a single entry with a clientIds array. Use describe_tool to fetch the full input schema for a specific tool before calling it.',
177
235
  }, async () => {
178
- if (!this.bridge.isClientConnected()) {
236
+ const clients = this.bridge.listClients();
237
+ // Dedup tool groups across clients by canonical shape
238
+ const dedupMap = new Map();
239
+ for (const client of clients) {
240
+ const groups = this.buildToolGroups(client);
241
+ for (const group of groups) {
242
+ const key = canonicalizeGroup(group);
243
+ const existing = dedupMap.get(key);
244
+ if (existing) {
245
+ existing.clientIds.push(client.id);
246
+ }
247
+ else {
248
+ dedupMap.set(key, { clientIds: [client.id], group });
249
+ }
250
+ }
251
+ }
252
+ const modulesPayload = [...dedupMap.values()].map(({ clientIds, group }) => {
179
253
  return {
180
- content: [
181
- {
182
- text: JSON.stringify({
183
- connected: false,
184
- error: 'React Native app is not connected',
185
- }),
186
- type: 'text',
187
- },
188
- ],
254
+ clientIds,
255
+ description: group.description,
256
+ name: group.module,
257
+ tools: group.tools.map((t) => {
258
+ return {
259
+ description: t.description,
260
+ name: t.name,
261
+ };
262
+ }),
189
263
  };
190
- }
191
- const moduleTools = this.modules.map((mod) => {
264
+ });
265
+ const hostToolsPayload = this.hostModules.map((mod) => {
192
266
  return {
193
267
  description: mod.description,
194
- module: mod.name,
195
- tools: mod.tools.map((t) => {
268
+ name: mod.name,
269
+ tools: Object.entries(mod.tools).map(([toolName, tool]) => {
196
270
  return {
197
- description: t.description,
198
- inputSchema: t.inputSchema,
199
- name: `${mod.name}${protocol_1.MODULE_SEPARATOR}${t.name}`,
271
+ description: tool.description,
272
+ name: `${mod.name}${protocol_1.MODULE_SEPARATOR}${toolName}`,
200
273
  };
201
274
  }),
202
275
  };
203
276
  });
204
- // Add dynamic tools (from useMcpTool hooks)
205
- if (this.dynamicTools.size > 0) {
206
- const dynamicByModule = new Map();
207
- for (const [fullName, info] of this.dynamicTools) {
208
- const existing = dynamicByModule.get(info.module) ?? [];
209
- existing.push({
210
- description: info.description,
211
- inputSchema: undefined,
212
- name: fullName,
213
- });
214
- dynamicByModule.set(info.module, existing);
215
- }
216
- for (const [module, dynTools] of dynamicByModule) {
217
- moduleTools.push({
218
- description: 'Dynamically registered tools from useMcpTool hooks',
219
- module: `${module} (dynamic)`,
220
- tools: dynTools,
221
- });
222
- }
277
+ const clientsPayload = clients.map((client) => {
278
+ return {
279
+ appName: client.appName,
280
+ appVersion: client.appVersion,
281
+ bundleId: client.bundleId,
282
+ deviceId: client.deviceId,
283
+ id: client.id,
284
+ label: client.label,
285
+ platform: client.platform,
286
+ };
287
+ });
288
+ const payload = {
289
+ clientCount: clients.length,
290
+ clients: clientsPayload,
291
+ hostTools: hostToolsPayload,
292
+ modules: modulesPayload,
293
+ };
294
+ if (clients.length === 0) {
295
+ payload.clientError = 'No React Native clients connected';
223
296
  }
224
297
  return {
225
- content: [{ text: JSON.stringify(moduleTools, null, 2), type: 'text' }],
298
+ content: [{ text: JSON.stringify(payload, null, 2), type: 'text' }],
226
299
  };
227
300
  });
228
301
  this.mcp.registerTool('connection_status', {
@@ -230,20 +303,32 @@ class McpServerWrapper {
230
303
  readOnlyHint: true,
231
304
  title: 'Connection Status',
232
305
  },
233
- description: 'Check if the React Native app is connected',
306
+ description: 'List connected React Native clients with their IDs, platforms, labels, and registered module names.',
234
307
  }, async () => {
235
- return {
236
- content: [
237
- {
238
- text: JSON.stringify({
239
- connected: this.bridge.isClientConnected(),
240
- modules: this.modules.map((m) => {
241
- return m.name;
242
- }),
308
+ const clients = this.bridge.listClients();
309
+ const payload = {
310
+ clientCount: clients.length,
311
+ clients: clients.map((c) => {
312
+ return {
313
+ appName: c.appName,
314
+ appVersion: c.appVersion,
315
+ bundleId: c.bundleId,
316
+ connectedAt: new Date(c.connectedAt).toISOString(),
317
+ deviceId: c.deviceId,
318
+ id: c.id,
319
+ label: c.label,
320
+ modules: c.modules.map((m) => {
321
+ return m.name;
243
322
  }),
244
- type: 'text',
245
- },
246
- ],
323
+ platform: c.platform,
324
+ };
325
+ }),
326
+ hostModules: this.hostModules.map((m) => {
327
+ return m.name;
328
+ }),
329
+ };
330
+ return {
331
+ content: [{ text: JSON.stringify(payload, null, 2), type: 'text' }],
247
332
  };
248
333
  });
249
334
  this.mcp.registerTool('state_get', {
@@ -251,40 +336,240 @@ class McpServerWrapper {
251
336
  readOnlyHint: true,
252
337
  title: 'Get State',
253
338
  },
254
- description: 'Read a state value exposed by the React Native app via useMcpState',
339
+ description: 'Read a state value exposed by a React Native client via useMcpState. State is scoped per client; specify clientId when multiple clients are connected.',
255
340
  inputSchema: {
341
+ clientId: zod_1.z
342
+ .string()
343
+ .optional()
344
+ .describe('Target client ID. Optional when exactly one client is connected.'),
256
345
  key: zod_1.z.string().describe('State key to read (e.g. "cart", "auth")'),
257
346
  },
258
- }, async ({ key }) => {
259
- const value = this.stateStore.get(key);
347
+ }, async ({ clientId, key }) => {
348
+ const resolution = this.bridge.resolveClient(clientId);
349
+ if (!resolution.ok) {
350
+ return jsonError(resolution.error);
351
+ }
352
+ const value = resolution.client.stateStore.get(key);
260
353
  if (value === undefined) {
354
+ return jsonError(`State "${key}" not found on client '${resolution.client.id}'. Use state_list to see available keys.`);
355
+ }
356
+ return {
357
+ content: [{ text: JSON.stringify(value, null, 2), type: 'text' }],
358
+ };
359
+ });
360
+ this.mcp.registerTool('state_list', {
361
+ annotations: {
362
+ readOnlyHint: true,
363
+ title: 'List State',
364
+ },
365
+ description: "List all available state keys. When a specific clientId is given, returns that client's keys; otherwise auto-picks the sole connected client or groups by client when multiple are connected.",
366
+ inputSchema: {
367
+ clientId: zod_1.z
368
+ .string()
369
+ .optional()
370
+ .describe('Target client ID. Optional when exactly one client is connected.'),
371
+ },
372
+ }, async ({ clientId }) => {
373
+ if (clientId) {
374
+ const resolution = this.bridge.resolveClient(clientId);
375
+ if (!resolution.ok) {
376
+ return jsonError(resolution.error);
377
+ }
261
378
  return {
262
379
  content: [
263
380
  {
264
381
  text: JSON.stringify({
265
- error: `State "${key}" not found. Use state_list to see available keys.`,
266
- }),
382
+ clientId: resolution.client.id,
383
+ keys: [...resolution.client.stateStore.keys()],
384
+ }, null, 2),
385
+ type: 'text',
386
+ },
387
+ ],
388
+ };
389
+ }
390
+ const clients = this.bridge.listClients();
391
+ if (clients.length === 0) {
392
+ return jsonError('No React Native clients connected');
393
+ }
394
+ if (clients.length === 1) {
395
+ const client = clients[0];
396
+ return {
397
+ content: [
398
+ {
399
+ text: JSON.stringify({ clientId: client.id, keys: [...client.stateStore.keys()] }, null, 2),
267
400
  type: 'text',
268
401
  },
269
402
  ],
270
403
  };
271
404
  }
272
405
  return {
273
- content: [{ text: JSON.stringify(value, null, 2), type: 'text' }],
406
+ content: [
407
+ {
408
+ text: JSON.stringify({
409
+ clients: clients.map((c) => {
410
+ return { id: c.id, keys: [...c.stateStore.keys()] };
411
+ }),
412
+ }, null, 2),
413
+ type: 'text',
414
+ },
415
+ ],
274
416
  };
275
417
  });
276
- this.mcp.registerTool('state_list', {
418
+ this.mcp.registerTool('describe_tool', {
277
419
  annotations: {
278
420
  readOnlyHint: true,
279
- title: 'List State',
421
+ title: 'Describe Tool',
280
422
  },
281
- description: 'List all available state keys exposed by the React Native app',
282
- }, async () => {
283
- const keys = Array.from(this.stateStore.keys());
423
+ description: 'Fetch the full description and input schema for a single tool. Use this after list_tools to learn how to construct arguments for a tool before calling it. For host tools, clientId is ignored. For in-app tools, omit clientId to auto-pick the shared descriptor; specify it only when multiple clients have the same tool with different schemas.',
424
+ inputSchema: {
425
+ clientId: zod_1.z
426
+ .string()
427
+ .optional()
428
+ .describe('Target client ID for in-app tools. Required only when multiple clients have the same tool with different schemas. Ignored for host tools.'),
429
+ tool: zod_1.z
430
+ .string()
431
+ .describe(`Full tool name in the format "module${protocol_1.MODULE_SEPARATOR}method" (e.g. "navigation${protocol_1.MODULE_SEPARATOR}navigate", "host${protocol_1.MODULE_SEPARATOR}screenshot").`),
432
+ },
433
+ }, async ({ clientId, tool }) => {
434
+ // 1. Host tool path — resolved via hostToolMap, clientId is ignored
435
+ const hostEntry = this.hostToolMap.get(tool);
436
+ if (hostEntry) {
437
+ const mod = this.hostModules.find((m) => {
438
+ return m.name === hostEntry.moduleName;
439
+ });
440
+ const hostTool = mod?.tools[hostEntry.toolName];
441
+ if (!hostTool) {
442
+ return jsonError(`Host tool '${tool}' metadata inconsistent — entry in hostToolMap but missing from hostModules.`);
443
+ }
444
+ return {
445
+ content: [
446
+ {
447
+ text: JSON.stringify({
448
+ description: hostTool.description,
449
+ inputSchema: hostTool.inputSchema,
450
+ name: tool,
451
+ scope: 'host',
452
+ }, null, 2),
453
+ type: 'text',
454
+ },
455
+ ],
456
+ };
457
+ }
458
+ // 2. Explicit clientId — look up the specific client
459
+ if (clientId) {
460
+ const client = this.bridge.getClient(clientId);
461
+ if (!client) {
462
+ const available = this.bridge
463
+ .listClients()
464
+ .map((c) => {
465
+ return c.id;
466
+ })
467
+ .join(', ') || '(none)';
468
+ return jsonError(`Client '${clientId}' not connected. Available: ${available}`);
469
+ }
470
+ const found = findToolInClient(client, tool);
471
+ if (!found) {
472
+ return jsonError(`Tool '${tool}' not found on client '${clientId}'.`);
473
+ }
474
+ return {
475
+ content: [
476
+ {
477
+ text: JSON.stringify({
478
+ clientIds: [clientId],
479
+ description: found.description,
480
+ inputSchema: found.inputSchema,
481
+ name: tool,
482
+ scope: 'client',
483
+ }, null, 2),
484
+ type: 'text',
485
+ },
486
+ ],
487
+ };
488
+ }
489
+ // 3. Auto-pick across all connected clients
490
+ const clients = this.bridge.listClients();
491
+ const matches = [];
492
+ for (const c of clients) {
493
+ const found = findToolInClient(c, tool);
494
+ if (found) {
495
+ matches.push({ clientId: c.id, descriptor: found });
496
+ }
497
+ }
498
+ if (matches.length === 0) {
499
+ return jsonError(`Tool '${tool}' not found on any client. Use list_tools to see available tools.`);
500
+ }
501
+ // Group by canonical descriptor shape — same shape across clients is not ambiguous
502
+ const byShape = new Map();
503
+ for (const match of matches) {
504
+ const key = canonicalize(match.descriptor);
505
+ const existing = byShape.get(key);
506
+ if (existing) {
507
+ existing.clientIds.push(match.clientId);
508
+ }
509
+ else {
510
+ byShape.set(key, { clientIds: [match.clientId], descriptor: match.descriptor });
511
+ }
512
+ }
513
+ if (byShape.size === 1) {
514
+ const [first] = byShape.values();
515
+ const { clientIds, descriptor } = first;
516
+ return {
517
+ content: [
518
+ {
519
+ text: JSON.stringify({
520
+ clientIds,
521
+ description: descriptor.description,
522
+ inputSchema: descriptor.inputSchema,
523
+ name: tool,
524
+ scope: 'client',
525
+ }, null, 2),
526
+ type: 'text',
527
+ },
528
+ ],
529
+ };
530
+ }
531
+ const candidates = [...byShape.values()]
532
+ .map(({ clientIds }) => {
533
+ return clientIds.join('+');
534
+ })
535
+ .join('; ');
536
+ return jsonError(`Tool '${tool}' exists on multiple clients with different schemas: ${candidates}. Specify clientId.`);
537
+ });
538
+ }
539
+ buildToolGroups(client) {
540
+ const groups = client.modules.map((mod) => {
284
541
  return {
285
- content: [{ text: JSON.stringify({ keys }, null, 2), type: 'text' }],
542
+ description: mod.description,
543
+ module: mod.name,
544
+ tools: mod.tools.map((t) => {
545
+ return {
546
+ description: t.description,
547
+ inputSchema: t.inputSchema,
548
+ name: `${mod.name}${protocol_1.MODULE_SEPARATOR}${t.name}`,
549
+ };
550
+ }),
286
551
  };
287
552
  });
553
+ if (client.dynamicTools.size > 0) {
554
+ const dynamicByModule = new Map();
555
+ for (const [fullName, info] of client.dynamicTools) {
556
+ const existing = dynamicByModule.get(info.module) ?? [];
557
+ existing.push({
558
+ description: info.description,
559
+ inputSchema: info.inputSchema,
560
+ name: fullName,
561
+ });
562
+ dynamicByModule.set(info.module, existing);
563
+ }
564
+ for (const [module, dynTools] of dynamicByModule) {
565
+ groups.push({
566
+ description: 'Dynamically registered tools from useMcpTool hooks',
567
+ module: `${module} (dynamic)`,
568
+ tools: dynTools,
569
+ });
570
+ }
571
+ }
572
+ return groups;
288
573
  }
289
574
  formatResult(result) {
290
575
  if (Array.isArray(result) && result.length > 0) {