@hypothesi/tauri-mcp-server 0.11.2 → 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 +2 -2
- package/dist/config.d.ts +14 -0
- package/dist/config.js +26 -0
- package/dist/driver/element-picker.d.ts +1 -1
- package/dist/driver/protocol.d.ts +7 -0
- package/dist/driver/session-manager.d.ts +38 -0
- package/dist/driver/session-manager.js +84 -11
- package/dist/driver/webview-interactions.d.ts +2 -2
- package/dist/manager/mobile.js +3 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -44,10 +44,10 @@ fn main() {
|
|
|
44
44
|
|
|
45
45
|
### 2. Configure Your AI Assistant
|
|
46
46
|
|
|
47
|
-
Use [
|
|
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
|
|
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
|
|
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
|
|
296
|
-
* - The plugin
|
|
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
|
|
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
|
|
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
|
-
|
|
311
|
-
|
|
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
|
-
|
|
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>;
|
package/dist/manager/mobile.js
CHANGED
|
@@ -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 =='); });
|
package/package.json
CHANGED