@hypothesi/tauri-mcp-server 0.5.0 → 0.5.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.
|
@@ -174,14 +174,33 @@ export class PluginClient extends EventEmitter {
|
|
|
174
174
|
}
|
|
175
175
|
// Singleton instance
|
|
176
176
|
let pluginClient = null;
|
|
177
|
+
/**
|
|
178
|
+
* Gets the existing singleton PluginClient without creating or modifying it.
|
|
179
|
+
* Use this for status checks where you don't want to affect the current connection.
|
|
180
|
+
*
|
|
181
|
+
* @returns The existing PluginClient or null if none exists
|
|
182
|
+
*/
|
|
183
|
+
export function getExistingPluginClient() {
|
|
184
|
+
return pluginClient;
|
|
185
|
+
}
|
|
177
186
|
/**
|
|
178
187
|
* Gets or creates a singleton PluginClient.
|
|
188
|
+
*
|
|
189
|
+
* If host/port are provided and differ from the existing client's configuration,
|
|
190
|
+
* the existing client is disconnected and a new one is created. This ensures
|
|
191
|
+
* that session start with a specific port always uses that port.
|
|
192
|
+
*
|
|
179
193
|
* @param host Optional host override
|
|
180
194
|
* @param port Optional port override
|
|
181
195
|
*/
|
|
182
196
|
export function getPluginClient(host, port) {
|
|
183
197
|
const resolvedHost = host ?? getDefaultHost();
|
|
184
198
|
const resolvedPort = port ?? getDefaultPort();
|
|
199
|
+
// If singleton exists but host/port don't match, reset it
|
|
200
|
+
if (pluginClient && (pluginClient.host !== resolvedHost || pluginClient.port !== resolvedPort)) {
|
|
201
|
+
pluginClient.disconnect();
|
|
202
|
+
pluginClient = null;
|
|
203
|
+
}
|
|
185
204
|
if (!pluginClient) {
|
|
186
205
|
pluginClient = new PluginClient(resolvedHost, resolvedPort);
|
|
187
206
|
}
|
|
@@ -202,6 +221,25 @@ export async function connectPlugin(host, port) {
|
|
|
202
221
|
await client.connect();
|
|
203
222
|
}
|
|
204
223
|
}
|
|
224
|
+
/**
|
|
225
|
+
* Ensures a session is active and connects to the plugin using session config.
|
|
226
|
+
* This should be used by all tools that require a connected Tauri app.
|
|
227
|
+
*
|
|
228
|
+
* @throws Error if no session is active
|
|
229
|
+
*/
|
|
230
|
+
export async function ensureSessionAndConnect() {
|
|
231
|
+
// Import dynamically to avoid circular dependency
|
|
232
|
+
const { hasActiveSession, getCurrentSession } = await import('./session-manager.js');
|
|
233
|
+
if (!hasActiveSession()) {
|
|
234
|
+
throw new Error('No active session. Call tauri_driver_session with action "start" first to connect to a Tauri app.');
|
|
235
|
+
}
|
|
236
|
+
const session = getCurrentSession();
|
|
237
|
+
if (!session) {
|
|
238
|
+
throw new Error('Session state is inconsistent. Please restart the session.');
|
|
239
|
+
}
|
|
240
|
+
await connectPlugin(session.host, session.port);
|
|
241
|
+
return getPluginClient(session.host, session.port);
|
|
242
|
+
}
|
|
205
243
|
export async function disconnectPlugin() {
|
|
206
244
|
const client = getPluginClient();
|
|
207
245
|
client.disconnect();
|
|
@@ -1,14 +1,13 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
-
import {
|
|
2
|
+
import { ensureSessionAndConnect, getExistingPluginClient } from './plugin-client.js';
|
|
3
3
|
export const ExecuteIPCCommandSchema = z.object({
|
|
4
4
|
command: z.string(),
|
|
5
5
|
args: z.unknown().optional(),
|
|
6
6
|
});
|
|
7
7
|
export async function executeIPCCommand(command, args = {}) {
|
|
8
8
|
try {
|
|
9
|
-
// Ensure we
|
|
10
|
-
await
|
|
11
|
-
const client = getPluginClient();
|
|
9
|
+
// Ensure we have an active session and are connected
|
|
10
|
+
const client = await ensureSessionAndConnect();
|
|
12
11
|
// Send IPC command via WebSocket to the mcp-bridge plugin
|
|
13
12
|
const response = await client.sendCommand({
|
|
14
13
|
command: 'invoke_tauri',
|
|
@@ -126,8 +125,34 @@ export async function getWindowInfo() {
|
|
|
126
125
|
}
|
|
127
126
|
}
|
|
128
127
|
export const GetBackendStateSchema = z.object({});
|
|
129
|
-
|
|
128
|
+
/**
|
|
129
|
+
* Get backend state from the connected Tauri app.
|
|
130
|
+
*
|
|
131
|
+
* This function can work in two modes:
|
|
132
|
+
* 1. Normal mode: Requires an active session (for MCP tool calls)
|
|
133
|
+
* 2. Setup mode: Uses existing connected client (for session setup)
|
|
134
|
+
*
|
|
135
|
+
* @param useExistingClient If true, uses the existing connected client without
|
|
136
|
+
* session validation. Used during session setup before currentSession is set.
|
|
137
|
+
*/
|
|
138
|
+
export async function getBackendState(useExistingClient = false) {
|
|
130
139
|
try {
|
|
140
|
+
if (useExistingClient) {
|
|
141
|
+
// During session setup, use the already-connected client directly
|
|
142
|
+
const client = getExistingPluginClient();
|
|
143
|
+
if (!client || !client.isConnected()) {
|
|
144
|
+
throw new Error('No connected client available');
|
|
145
|
+
}
|
|
146
|
+
const response = await client.sendCommand({
|
|
147
|
+
command: 'invoke_tauri',
|
|
148
|
+
args: { command: 'plugin:mcp-bridge|get_backend_state', args: {} },
|
|
149
|
+
});
|
|
150
|
+
if (!response.success) {
|
|
151
|
+
throw new Error(response.error || 'Unknown error');
|
|
152
|
+
}
|
|
153
|
+
return JSON.stringify(response.data);
|
|
154
|
+
}
|
|
155
|
+
// Normal mode: use executeIPCCommand which validates session
|
|
131
156
|
const result = await executeIPCCommand('plugin:mcp-bridge|get_backend_state');
|
|
132
157
|
const parsed = JSON.parse(result);
|
|
133
158
|
if (!parsed.success) {
|
|
@@ -149,8 +174,7 @@ export const ListWindowsSchema = z.object({});
|
|
|
149
174
|
*/
|
|
150
175
|
export async function listWindows() {
|
|
151
176
|
try {
|
|
152
|
-
await
|
|
153
|
-
const client = getPluginClient();
|
|
177
|
+
const client = await ensureSessionAndConnect();
|
|
154
178
|
const response = await client.sendCommand({
|
|
155
179
|
command: 'list_windows',
|
|
156
180
|
});
|
|
@@ -184,8 +208,7 @@ export const ResizeWindowSchema = z.object({
|
|
|
184
208
|
*/
|
|
185
209
|
export async function resizeWindow(options) {
|
|
186
210
|
try {
|
|
187
|
-
await
|
|
188
|
-
const client = getPluginClient();
|
|
211
|
+
const client = await ensureSessionAndConnect();
|
|
189
212
|
const response = await client.sendCommand({
|
|
190
213
|
command: 'resize_window',
|
|
191
214
|
args: {
|
|
@@ -236,8 +259,7 @@ export async function manageWindow(options) {
|
|
|
236
259
|
}
|
|
237
260
|
case 'info': {
|
|
238
261
|
try {
|
|
239
|
-
await
|
|
240
|
-
const client = getPluginClient();
|
|
262
|
+
const client = await ensureSessionAndConnect();
|
|
241
263
|
const response = await client.sendCommand({
|
|
242
264
|
command: 'get_window_info',
|
|
243
265
|
args: { windowId },
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
*
|
|
7
7
|
* @internal This module is for internal use only and is not exposed as MCP tools.
|
|
8
8
|
*/
|
|
9
|
-
import {
|
|
9
|
+
import { ensureSessionAndConnect } from './plugin-client.js';
|
|
10
10
|
/**
|
|
11
11
|
* Registers a script to be injected into the webview.
|
|
12
12
|
*
|
|
@@ -20,8 +20,7 @@ import { getPluginClient, connectPlugin } from './plugin-client.js';
|
|
|
20
20
|
* @returns Promise resolving to registration result
|
|
21
21
|
*/
|
|
22
22
|
export async function registerScript(id, type, content, windowLabel) {
|
|
23
|
-
await
|
|
24
|
-
const client = getPluginClient();
|
|
23
|
+
const client = await ensureSessionAndConnect();
|
|
25
24
|
const response = await client.sendCommand({
|
|
26
25
|
command: 'register_script',
|
|
27
26
|
args: { id, type, content, windowLabel },
|
|
@@ -39,8 +38,7 @@ export async function registerScript(id, type, content, windowLabel) {
|
|
|
39
38
|
* @returns Promise resolving to removal result
|
|
40
39
|
*/
|
|
41
40
|
export async function removeScript(id, windowLabel) {
|
|
42
|
-
await
|
|
43
|
-
const client = getPluginClient();
|
|
41
|
+
const client = await ensureSessionAndConnect();
|
|
44
42
|
const response = await client.sendCommand({
|
|
45
43
|
command: 'remove_script',
|
|
46
44
|
args: { id, windowLabel },
|
|
@@ -57,8 +55,7 @@ export async function removeScript(id, windowLabel) {
|
|
|
57
55
|
* @returns Promise resolving to the number of scripts cleared
|
|
58
56
|
*/
|
|
59
57
|
export async function clearScripts(windowLabel) {
|
|
60
|
-
await
|
|
61
|
-
const client = getPluginClient();
|
|
58
|
+
const client = await ensureSessionAndConnect();
|
|
62
59
|
const response = await client.sendCommand({
|
|
63
60
|
command: 'clear_scripts',
|
|
64
61
|
args: { windowLabel },
|
|
@@ -74,8 +71,7 @@ export async function clearScripts(windowLabel) {
|
|
|
74
71
|
* @returns Promise resolving to the list of registered scripts
|
|
75
72
|
*/
|
|
76
73
|
export async function getScripts() {
|
|
77
|
-
await
|
|
78
|
-
const client = getPluginClient();
|
|
74
|
+
const client = await ensureSessionAndConnect();
|
|
79
75
|
const response = await client.sendCommand({
|
|
80
76
|
command: 'get_scripts',
|
|
81
77
|
args: {},
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
import { getDefaultHost, getDefaultPort } from '../config.js';
|
|
3
3
|
import { AppDiscovery } from './app-discovery.js';
|
|
4
|
-
import { resetPluginClient,
|
|
4
|
+
import { resetPluginClient, getExistingPluginClient, connectPlugin } from './plugin-client.js';
|
|
5
5
|
import { getBackendState } from './plugin-commands.js';
|
|
6
6
|
import { resetInitialization } from './webview-executor.js';
|
|
7
7
|
/**
|
|
@@ -29,6 +29,19 @@ export const ManageDriverSessionSchema = z.object({
|
|
|
29
29
|
// AppDiscovery instance - recreated when host changes
|
|
30
30
|
// Track current session info including app identifier for session reuse
|
|
31
31
|
let appDiscovery = null, currentSession = null;
|
|
32
|
+
/**
|
|
33
|
+
* Check if a session is currently active.
|
|
34
|
+
* @returns true if a session has been started and not stopped
|
|
35
|
+
*/
|
|
36
|
+
export function hasActiveSession() {
|
|
37
|
+
return currentSession !== null;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Get the current session info, or null if no session is active.
|
|
41
|
+
*/
|
|
42
|
+
export function getCurrentSession() {
|
|
43
|
+
return currentSession;
|
|
44
|
+
}
|
|
32
45
|
function getAppDiscovery(host) {
|
|
33
46
|
if (!appDiscovery || appDiscovery.host !== host) {
|
|
34
47
|
appDiscovery = new AppDiscovery(host);
|
|
@@ -62,7 +75,8 @@ async function tryConnect(host, port) {
|
|
|
62
75
|
*/
|
|
63
76
|
async function fetchAppIdentifier() {
|
|
64
77
|
try {
|
|
65
|
-
|
|
78
|
+
// Use existing client - called during session setup before currentSession is set
|
|
79
|
+
const stateJson = await getBackendState(true);
|
|
66
80
|
const state = JSON.parse(stateJson);
|
|
67
81
|
// Return null if identifier is not present (backward compat with older plugins)
|
|
68
82
|
return state.app?.identifier ?? null;
|
|
@@ -95,8 +109,8 @@ async function fetchAppIdentifier() {
|
|
|
95
109
|
export async function manageDriverSession(action, host, port) {
|
|
96
110
|
// Handle status action
|
|
97
111
|
if (action === 'status') {
|
|
98
|
-
const client =
|
|
99
|
-
if (client
|
|
112
|
+
const client = getExistingPluginClient();
|
|
113
|
+
if (client?.isConnected() && currentSession) {
|
|
100
114
|
return JSON.stringify({
|
|
101
115
|
connected: true,
|
|
102
116
|
app: currentSession.name,
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
import { getPluginClient, connectPlugin } from './plugin-client.js';
|
|
3
|
+
import { hasActiveSession, getCurrentSession } from './session-manager.js';
|
|
3
4
|
import { createMcpLogger } from '../logger.js';
|
|
4
5
|
import { buildScreenshotScript, buildScreenshotCaptureScript, getHtml2CanvasSource, HTML2CANVAS_SCRIPT_ID, } from './scripts/html2canvas-loader.js';
|
|
5
6
|
import { registerScript, isScriptRegistered } from './script-manager.js';
|
|
@@ -22,17 +23,27 @@ const driverLogger = createMcpLogger('DRIVER');
|
|
|
22
23
|
* This is called automatically by all tool functions.
|
|
23
24
|
*
|
|
24
25
|
* Initialization includes:
|
|
25
|
-
* -
|
|
26
|
+
* - Verifying an active session exists (via tauri_driver_session)
|
|
27
|
+
* - Connecting to the plugin WebSocket using session config
|
|
26
28
|
* - Console capture is already initialized by bridge.js in the Tauri app
|
|
27
29
|
*
|
|
28
30
|
* This function is idempotent - calling it multiple times is safe.
|
|
31
|
+
*
|
|
32
|
+
* @throws Error if no session is active (tauri_driver_session must be called first)
|
|
29
33
|
*/
|
|
30
34
|
export async function ensureReady() {
|
|
31
35
|
if (isInitialized) {
|
|
32
36
|
return;
|
|
33
37
|
}
|
|
34
|
-
//
|
|
35
|
-
|
|
38
|
+
// Require an active session to prevent connecting to wrong app
|
|
39
|
+
if (!hasActiveSession()) {
|
|
40
|
+
throw new Error('No active session. Call tauri_driver_session with action "start" first to connect to a Tauri app.');
|
|
41
|
+
}
|
|
42
|
+
// Get session config and connect with explicit host/port
|
|
43
|
+
const session = getCurrentSession();
|
|
44
|
+
if (session) {
|
|
45
|
+
await connectPlugin(session.host, session.port);
|
|
46
|
+
}
|
|
36
47
|
isInitialized = true;
|
|
37
48
|
}
|
|
38
49
|
/**
|
|
@@ -63,7 +74,12 @@ export async function executeInWebviewWithContext(script, windowId) {
|
|
|
63
74
|
try {
|
|
64
75
|
// Ensure we're fully initialized
|
|
65
76
|
await ensureReady();
|
|
66
|
-
|
|
77
|
+
// Get session config to use correct host/port
|
|
78
|
+
const session = getCurrentSession();
|
|
79
|
+
if (!session) {
|
|
80
|
+
throw new Error('No active session');
|
|
81
|
+
}
|
|
82
|
+
const client = getPluginClient(session.host, session.port);
|
|
67
83
|
// Send script directly - Rust handles wrapping and IPC callbacks.
|
|
68
84
|
// Use 7s timeout (longer than Rust's 5s) so errors return before Node times out.
|
|
69
85
|
const response = await client.sendCommand({
|