@hypothesi/tauri-mcp-server 0.11.1 → 0.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -44,10 +44,10 @@ fn main() {
44
44
 
45
45
  ### 2. Configure Your AI Assistant
46
46
 
47
- Use [install-mcp](https://www.npmjs.com/package/install-mcp) to add the server to your AI assistant:
47
+ Use [aix](https://aix.a1st.dev/cli/add/#aix-add-mcp) to add the server to your AI assistant:
48
48
 
49
49
  ```bash
50
- npx -y install-mcp @hypothesi/tauri-mcp-server --client claude-code
50
+ npx -y @a1st/aix add mcp tauri --command 'npx @hypothesi/tauri-mcp-server' --user
51
51
  ```
52
52
 
53
53
  Supported clients: `claude-code`, `cursor`, `windsurf`, `vscode`, `cline`, `roo-cline`, `claude`, `zed`, `goose`, `warp`, `codex`
package/dist/config.d.ts CHANGED
@@ -25,6 +25,20 @@ export declare function getDefaultHost(): string;
25
25
  * 2. 9223 (default)
26
26
  */
27
27
  export declare function getDefaultPort(): number;
28
+ /**
29
+ * Gets the CWD hint used to route tool calls to the right Tauri instance
30
+ * when multiple are connected at once.
31
+ *
32
+ * Resolution priority:
33
+ * 1. MCP_BRIDGE_CWD environment variable (explicit override; useful when a
34
+ * wrapper script wants to pin routing to a specific worktree regardless
35
+ * of where the TS server happened to be launched from)
36
+ * 2. process.cwd() (natural inheritance from the calling shell / IDE)
37
+ *
38
+ * Returns null only when both are unavailable, which is extremely rare:
39
+ * process.cwd() is set on every healthy POSIX process.
40
+ */
41
+ export declare function getCwdHint(): string | null;
28
42
  /**
29
43
  * Gets the full bridge configuration from environment variables.
30
44
  */
package/dist/config.js CHANGED
@@ -28,6 +28,32 @@ export function getDefaultPort() {
28
28
  const port = process.env.MCP_BRIDGE_PORT;
29
29
  return port ? parseInt(port, 10) : 9223;
30
30
  }
31
+ /**
32
+ * Gets the CWD hint used to route tool calls to the right Tauri instance
33
+ * when multiple are connected at once.
34
+ *
35
+ * Resolution priority:
36
+ * 1. MCP_BRIDGE_CWD environment variable (explicit override; useful when a
37
+ * wrapper script wants to pin routing to a specific worktree regardless
38
+ * of where the TS server happened to be launched from)
39
+ * 2. process.cwd() (natural inheritance from the calling shell / IDE)
40
+ *
41
+ * Returns null only when both are unavailable, which is extremely rare:
42
+ * process.cwd() is set on every healthy POSIX process.
43
+ */
44
+ export function getCwdHint() {
45
+ // eslint-disable-next-line no-process-env
46
+ const override = process.env.MCP_BRIDGE_CWD;
47
+ if (override && override.length > 0) {
48
+ return override;
49
+ }
50
+ try {
51
+ return process.cwd();
52
+ }
53
+ catch {
54
+ return null;
55
+ }
56
+ }
31
57
  /**
32
58
  * Gets the full bridge configuration from environment variables.
33
59
  */
@@ -17,9 +17,9 @@ export declare const SelectElementSchema: z.ZodObject<{
17
17
  windowId?: string | undefined;
18
18
  appIdentifier?: string | number | undefined;
19
19
  }, {
20
+ timeout?: number | undefined;
20
21
  windowId?: string | undefined;
21
22
  appIdentifier?: string | number | undefined;
22
- timeout?: number | undefined;
23
23
  }>;
24
24
  export declare const GetPointedElementSchema: z.ZodObject<{
25
25
  windowId: z.ZodOptional<z.ZodString>;
@@ -110,6 +110,13 @@ export interface BackendState {
110
110
  visible: boolean;
111
111
  }>;
112
112
  window_count: number;
113
+ /**
114
+ * Working directory of the host Tauri process. Optional for backward
115
+ * compat with plugin versions before this field was added. Used by the
116
+ * TS server to disambiguate concurrent Tauri instances (e.g. moss
117
+ * running from multiple worktrees) that share the same bundle id.
118
+ */
119
+ cwd?: string | null;
113
120
  timestamp: number;
114
121
  }
115
122
  /** Console log entry */
@@ -30,6 +30,13 @@ export declare const ManageDriverSessionSchema: z.ZodObject<{
30
30
  export interface SessionInfo {
31
31
  name: string;
32
32
  identifier: string | null;
33
+ /**
34
+ * Working directory of the host Tauri process at session-start time.
35
+ * `null` when the plugin is too old to advertise it (pre-v0.11) or
36
+ * when the lookup failed; in that case CWD-based routing skips this
37
+ * session and falls back to default behavior.
38
+ */
39
+ cwd: string | null;
33
40
  host: string;
34
41
  port: number;
35
42
  client: PluginClient;
@@ -52,9 +59,40 @@ export declare function getDefaultSession(): SessionInfo | null;
52
59
  * Get all active sessions.
53
60
  */
54
61
  export declare function getAllSessions(): SessionInfo[];
62
+ /**
63
+ * Find the session whose `cwd` best matches `hintCwd`.
64
+ *
65
+ * Used to disambiguate multiple concurrent Tauri instances when the caller
66
+ * did not pass an explicit appIdentifier. The "best match" is the longest
67
+ * shared prefix between the hint and a session's CWD, in either direction:
68
+ *
69
+ * - `session.cwd === hintCwd` (exact)
70
+ * - `hintCwd.startsWith(session.cwd + '/')` (TS server runs inside a
71
+ * subdirectory of the host process's CWD; rare)
72
+ * - `session.cwd.startsWith(hintCwd + '/')` (host process runs inside
73
+ * the workspace the TS server was launched from; the common case
74
+ * when VSCode opens a parent dir and moss runs from a worktree)
75
+ *
76
+ * Sessions whose `cwd` is null (older plugin without CWD support) are
77
+ * skipped — they cannot be matched and stay eligible for the existing
78
+ * "use default app" fallback in `resolveTargetApp`.
79
+ *
80
+ * Pure function (takes `sessions` as a parameter) so it can be unit-tested
81
+ * without touching module state.
82
+ *
83
+ * @returns The best-matching session, or null if no session matches.
84
+ */
85
+ export declare function findSessionByCwd(sessions: Iterable<SessionInfo>, hintCwd: string | null): SessionInfo | null;
55
86
  /**
56
87
  * Resolve target app from port or identifier.
57
88
  * Returns the appropriate session based on the routing logic.
89
+ *
90
+ * Order of precedence:
91
+ * 1. Explicit `portOrIdentifier` (port number or bundle id)
92
+ * 2. CWD match via `findSessionByCwd` against `MCP_BRIDGE_CWD` or
93
+ * `process.cwd()` — lets each MCP client target the Tauri instance
94
+ * running in its own workspace/worktree without manual routing
95
+ * 3. The default app (most-recently-connected)
58
96
  */
59
97
  export declare function resolveTargetApp(portOrIdentifier?: string | number): SessionInfo;
60
98
  /**
@@ -1,5 +1,5 @@
1
1
  import { z } from 'zod';
2
- import { getDefaultHost, getDefaultPort } from '../config.js';
2
+ import { getCwdHint, getDefaultHost, getDefaultPort } from '../config.js';
3
3
  import { AppDiscovery } from './app-discovery.js';
4
4
  import { PluginClient } from './plugin-client.js';
5
5
  import { resetInitialization } from './webview-executor.js';
@@ -69,9 +69,65 @@ function getAppDiscovery(host) {
69
69
  }
70
70
  return appDiscovery;
71
71
  }
72
+ /**
73
+ * Find the session whose `cwd` best matches `hintCwd`.
74
+ *
75
+ * Used to disambiguate multiple concurrent Tauri instances when the caller
76
+ * did not pass an explicit appIdentifier. The "best match" is the longest
77
+ * shared prefix between the hint and a session's CWD, in either direction:
78
+ *
79
+ * - `session.cwd === hintCwd` (exact)
80
+ * - `hintCwd.startsWith(session.cwd + '/')` (TS server runs inside a
81
+ * subdirectory of the host process's CWD; rare)
82
+ * - `session.cwd.startsWith(hintCwd + '/')` (host process runs inside
83
+ * the workspace the TS server was launched from; the common case
84
+ * when VSCode opens a parent dir and moss runs from a worktree)
85
+ *
86
+ * Sessions whose `cwd` is null (older plugin without CWD support) are
87
+ * skipped — they cannot be matched and stay eligible for the existing
88
+ * "use default app" fallback in `resolveTargetApp`.
89
+ *
90
+ * Pure function (takes `sessions` as a parameter) so it can be unit-tested
91
+ * without touching module state.
92
+ *
93
+ * @returns The best-matching session, or null if no session matches.
94
+ */
95
+ export function findSessionByCwd(sessions, hintCwd) {
96
+ if (!hintCwd) {
97
+ return null;
98
+ }
99
+ let best = null, bestScore = -1;
100
+ for (const session of sessions) {
101
+ if (!session.cwd) {
102
+ continue;
103
+ }
104
+ let score = -1;
105
+ if (session.cwd === hintCwd) {
106
+ score = session.cwd.length;
107
+ }
108
+ else if (hintCwd.startsWith(session.cwd + '/')) {
109
+ score = session.cwd.length;
110
+ }
111
+ else if (session.cwd.startsWith(hintCwd + '/')) {
112
+ score = hintCwd.length;
113
+ }
114
+ if (score > bestScore) {
115
+ best = session;
116
+ bestScore = score;
117
+ }
118
+ }
119
+ return best;
120
+ }
72
121
  /**
73
122
  * Resolve target app from port or identifier.
74
123
  * Returns the appropriate session based on the routing logic.
124
+ *
125
+ * Order of precedence:
126
+ * 1. Explicit `portOrIdentifier` (port number or bundle id)
127
+ * 2. CWD match via `findSessionByCwd` against `MCP_BRIDGE_CWD` or
128
+ * `process.cwd()` — lets each MCP client target the Tauri instance
129
+ * running in its own workspace/worktree without manual routing
130
+ * 3. The default app (most-recently-connected)
75
131
  */
76
132
  export function resolveTargetApp(portOrIdentifier) {
77
133
  if (activeSessions.size === 0) {
@@ -105,6 +161,12 @@ export function resolveTargetApp(portOrIdentifier) {
105
161
  }
106
162
  throw new Error(formatAppNotFoundError(portOrIdentifier));
107
163
  }
164
+ // No explicit identifier: try CWD-based routing before falling back to
165
+ // "default app". If no session matches, behave as before.
166
+ const cwdMatch = findSessionByCwd(activeSessions.values(), getCwdHint());
167
+ if (cwdMatch) {
168
+ return cwdMatch;
169
+ }
108
170
  // Use default app
109
171
  if (defaultPort !== null && activeSessions.has(defaultPort)) {
110
172
  const session = activeSessions.get(defaultPort);
@@ -174,6 +236,7 @@ async function handleStatusAction() {
174
236
  connected: true,
175
237
  app: session.name,
176
238
  identifier: session.identifier,
239
+ cwd: session.cwd,
177
240
  host: session.host,
178
241
  port: session.port,
179
242
  });
@@ -182,6 +245,7 @@ async function handleStatusAction() {
182
245
  return {
183
246
  name: session.name,
184
247
  identifier: session.identifier,
248
+ cwd: session.cwd,
185
249
  host: session.host,
186
250
  port: session.port,
187
251
  isDefault: session.port === defaultPort,
@@ -232,12 +296,16 @@ async function handleStartAction(host, port) {
232
296
  if (!connectedSession) {
233
297
  return `Session start failed - no Tauri app found at localhost or ${configuredHost}:${configuredPort}`;
234
298
  }
299
+ if (activeSessions.has(connectedSession.port)) {
300
+ return `Already connected to app on port ${connectedSession.port}`;
301
+ }
235
302
  const client = new PluginClient(connectedSession.host, connectedSession.port);
236
303
  await client.connect();
237
- const identifier = await fetchAppIdentifier(client);
304
+ const { identifier, cwd } = await fetchAppMetadata(client);
238
305
  const sessionInfo = {
239
306
  name: connectedSession.name,
240
307
  identifier,
308
+ cwd,
241
309
  host: connectedSession.host,
242
310
  port: connectedSession.port,
243
311
  client,
@@ -292,27 +360,32 @@ async function tryConnect(host, port) {
292
360
  * Must be called after a PluginClient is connected.
293
361
  *
294
362
  * @param client - The PluginClient to query
295
- * @returns The app identifier (bundle ID) or null if not available. Returns null when:
296
- * - The plugin doesn't support the identifier field (older versions)
363
+ * @returns `{ identifier, cwd }`. Each field is null when:
364
+ * - The plugin is too old to advertise it
297
365
  * - The backend state request fails
298
- * - The identifier field is missing from the response
366
+ * - The field is missing from the response
367
+ *
368
+ * Older plugins (pre-identifier) return `{ identifier: null, cwd: null }`.
369
+ * Newer plugins without the cwd field return a real identifier and a null cwd.
370
+ * Routing degrades gracefully in either case.
299
371
  */
300
- async function fetchAppIdentifier(client) {
372
+ async function fetchAppMetadata(client) {
301
373
  try {
302
374
  const response = await client.sendCommand({
303
375
  command: 'invoke_tauri',
304
376
  args: { command: 'plugin:mcp-bridge|get_backend_state', args: {} },
305
377
  });
306
378
  if (!response.success || !response.data) {
307
- return null;
379
+ return { identifier: null, cwd: null };
308
380
  }
309
381
  const state = response.data;
310
- // Return null if identifier is not present (backward compat with older plugins)
311
- return state.app?.identifier ?? null;
382
+ return {
383
+ identifier: state.app?.identifier ?? null,
384
+ cwd: typeof state.cwd === 'string' && state.cwd.length > 0 ? state.cwd : null,
385
+ };
312
386
  }
313
387
  catch {
314
- // Return null on any error (e.g., older plugin version that doesn't support this)
315
- return null;
388
+ return { identifier: null, cwd: null };
316
389
  }
317
390
  }
318
391
  /**
@@ -124,17 +124,17 @@ export declare const WaitForSchema: z.ZodObject<{
124
124
  }, "strip", z.ZodTypeAny, {
125
125
  value: string;
126
126
  type: "text" | "selector" | "ipc-event";
127
- strategy: "css" | "text" | "xpath";
128
127
  timeout: number;
128
+ strategy: "css" | "text" | "xpath";
129
129
  windowId?: string | undefined;
130
130
  appIdentifier?: string | number | undefined;
131
131
  }, {
132
132
  value: string;
133
133
  type: "text" | "selector" | "ipc-event";
134
+ timeout?: number | undefined;
134
135
  windowId?: string | undefined;
135
136
  appIdentifier?: string | number | undefined;
136
137
  strategy?: "css" | "text" | "xpath" | undefined;
137
- timeout?: number | undefined;
138
138
  }>;
139
139
  export declare const GetStylesSchema: z.ZodObject<{
140
140
  windowId: z.ZodOptional<z.ZodString>;
@@ -1,9 +1,10 @@
1
1
  import { z } from 'zod';
2
2
  import { execa } from 'execa';
3
3
  export const ListDevicesSchema = z.object({});
4
+ const DEVICE_LIST_TIMEOUT_MS = 5000;
4
5
  async function getAndroidDevices() {
5
6
  try {
6
- const { stdout } = await execa('adb', ['devices', '-l']);
7
+ const { stdout } = await execa('adb', ['devices', '-l'], { timeout: DEVICE_LIST_TIMEOUT_MS });
7
8
  return stdout
8
9
  .split('\n')
9
10
  .slice(1)
@@ -20,7 +21,7 @@ async function getIOSSimulators() {
20
21
  return [];
21
22
  }
22
23
  try {
23
- const { stdout } = await execa('xcrun', ['simctl', 'list', 'devices', 'booted']);
24
+ const { stdout } = await execa('xcrun', ['simctl', 'list', 'devices', 'booted'], { timeout: DEVICE_LIST_TIMEOUT_MS });
24
25
  return stdout
25
26
  .split('\n')
26
27
  .filter((line) => { return line.trim().length > 0 && !line.includes('== Devices =='); });
@@ -74,11 +74,18 @@ export const PROMPTS = [
74
74
  name: 'select',
75
75
  description: 'Visually select an element in the running Tauri app. ' +
76
76
  'Activates a picker overlay — click an element to send its metadata and an annotated screenshot to the agent. ' +
77
- 'Optionally include a message describing what you want to do with the element.',
77
+ 'Optionally include a message describing what you want to do with the element. ' +
78
+ 'Note for Claude Code users: due to upstream bugs ' +
79
+ '(anthropics/claude-code#5597, #14210), Claude Code requires at least one ' +
80
+ 'character of input and only forwards the first word of the `message` argument. ' +
81
+ 'Type your full message in regular chat before or after invoking the prompt instead.',
78
82
  arguments: [
79
83
  {
80
84
  name: 'message',
81
- description: 'What you want to discuss or do with the selected element (e.g. "this button should be green instead of blue")',
85
+ description: 'What you want to discuss or do with the selected element ' +
86
+ '(e.g. "this button should be green instead of blue"). ' +
87
+ 'In Claude Code, only the first word reaches the server (anthropics/claude-code#14210) — ' +
88
+ 'put your full message in chat instead.',
82
89
  required: false,
83
90
  },
84
91
  ],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hypothesi/tauri-mcp-server",
3
- "version": "0.11.1",
3
+ "version": "0.12.0",
4
4
  "mcpName": "io.github.hypothesi/mcp-server-tauri",
5
5
  "description": "A Model Context Protocol server for use with Tauri v2 applications",
6
6
  "type": "module",