@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 { getPluginClient, connectPlugin } from './plugin-client.js';
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're connected to the plugin
10
- await connectPlugin();
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
- export async function getBackendState() {
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 connectPlugin();
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 connectPlugin();
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 connectPlugin();
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 { getPluginClient, connectPlugin } from './plugin-client.js';
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 connectPlugin();
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 connectPlugin();
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 connectPlugin();
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 connectPlugin();
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, getPluginClient, connectPlugin } from './plugin-client.js';
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
- const stateJson = await getBackendState();
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 = getPluginClient();
99
- if (client.isConnected() && currentSession) {
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
- * - Connecting to the plugin WebSocket
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
- // Connect to the plugin
35
- await connectPlugin();
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
- const client = getPluginClient();
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({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hypothesi/tauri-mcp-server",
3
- "version": "0.5.0",
3
+ "version": "0.5.1",
4
4
  "description": "A Model Context Protocol server for Tauri v2 development",
5
5
  "type": "module",
6
6
  "bin": {