qz-browser-agent 0.1.3 → 0.1.5

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/src/terminal.mjs CHANGED
@@ -198,6 +198,29 @@ export function renderInputNotice(input, options = {}) {
198
198
  return out.finish();
199
199
  }
200
200
 
201
+ function formatUsage(usage) {
202
+ if (!usage || typeof usage !== "object") return "";
203
+ const count = value => {
204
+ const number = Number(value);
205
+ return Number.isFinite(number) ? String(Math.max(0, Math.trunc(number))) : "0";
206
+ };
207
+ const cost = Number(usage.costUsd);
208
+ const text = Number.isFinite(cost) ? cost.toFixed(6).replace(/\.?0+$/u, "") : "";
209
+ const shown = ["inputTokens", "outputTokens", "cachedTokens", "costUsd", "modelCalls", "toolCalls", "screenshots", "contextCompressions"]
210
+ .some(key => Number(usage[key]) > 0);
211
+ if (!shown) return "";
212
+ return [
213
+ `input ${count(usage.inputTokens)}`,
214
+ `output ${count(usage.outputTokens)}`,
215
+ `cached ${count(usage.cachedTokens)}`,
216
+ `cost $${text || "0"}`,
217
+ `model calls ${count(usage.modelCalls)}`,
218
+ `tool calls ${count(usage.toolCalls)}`,
219
+ `screenshots ${count(usage.screenshots)}`,
220
+ `compressions ${count(usage.contextCompressions)}`,
221
+ ].join(", ");
222
+ }
223
+
201
224
  function snapshotView(out, snapshot, { streamed = false, clientId = "" } = {}) {
202
225
  const status = taskStatus(snapshot);
203
226
  out.line(label(status), tone(status));
@@ -212,6 +235,8 @@ function snapshotView(out, snapshot, { streamed = false, clientId = "" } = {}) {
212
235
  if (response && !streamed) {
213
236
  out.line(); out.line("Latest response", "muted"); out.line(response);
214
237
  }
238
+ const usage = formatUsage(snapshot.usage);
239
+ if (usage) out.field("Usage", usage);
215
240
  }
216
241
 
217
242
  function taskList(out, sessions, client = "") {
@@ -412,13 +437,14 @@ export function renderResult(parsed, result, options = {}) {
412
437
  if (action === "list" || !action) taskList(out, result?.sessions || [], client);
413
438
  else if (action === "get") snapshotView(out, result || {}, presentation);
414
439
  else if (action === "create") {
440
+ const sessionId = result?.session?.sessionId || result?.sessionId || result?.session?.id;
415
441
  if (result?.snapshot) snapshotView(out, result.snapshot, presentation);
416
442
  else {
417
443
  out.line(result?.started ? "Task queued" : "Task created", "accent");
418
- out.field("Task", result?.session?.id);
444
+ out.field("Task", sessionId);
419
445
  out.field("Title", single(result?.session?.title));
420
446
  }
421
- taskCommands(out, result?.session?.id || result?.snapshot?.sessionId, { client, cancel: !result?.snapshot || ["pending", "running", "waiting_user", "waitingUser", "paused"].includes(taskStatus(result.snapshot)) });
447
+ taskCommands(out, sessionId || result?.snapshot?.sessionId, { client, cancel: !result?.snapshot || ["pending", "running", "waiting_user", "waitingUser", "paused"].includes(taskStatus(result.snapshot)) });
422
448
  } else if (action === "stop") {
423
449
  out.line(result?.stopped ? "Task stopped" : "Task has no active run", "accent");
424
450
  out.field("Task", result?.sessionId || parsed.id);
@@ -456,16 +482,19 @@ export function renderResult(parsed, result, options = {}) {
456
482
  out.line("Service upgrade required for browser pairing.", "waiting");
457
483
  out.line(result.hint);
458
484
  } else if (result?.updateAvailable) {
459
- out.line("Service update available. Restart when your current work is finished:", "waiting");
460
- out.command("qz stop && qz connect");
461
- } else if (!result?.connected) out.command("qz connect");
485
+ out.line("Service update available. Compatible connections remain in use.", "waiting");
486
+ } else if (!result?.connected) out.command("qz connect uri");
487
+ } else if (command === "cleanup") {
488
+ out.line(result?.stopped || result?.removedFiles ? "Cleanup complete" : "Nothing to clean", "accent");
489
+ out.field("Services stopped", result?.stopped ?? 0);
490
+ out.field("Runtime files removed", result?.removedFiles ?? 0);
491
+ for (const service of result?.services || []) {
492
+ if (service.status === "stopped") out.field(`Stopped PID ${service.processId}`, service.endpoint || service.scopeId);
493
+ }
462
494
  } else if (command === "install") {
463
495
  out.line(result?.opened ? "Chrome Web Store opened" : "Install qz from the Chrome Web Store", "accent");
464
496
  if (!result?.opened) out.line(result?.url);
465
- out.line("Click Add to Chrome, then connect:"); out.command("qz connect");
466
- } else if (command === "stop") {
467
- out.line(result?.stopped ? "Local service stopped" : "Local service is already stopped", "accent");
468
- out.command("qz connect");
497
+ out.line("Click Add to Chrome, then create a session:"); out.command('qz session create --title "Browser task"');
469
498
  } else {
470
499
  out.line(JSON.stringify(result ?? null, null, 2));
471
500
  }
@@ -480,13 +509,18 @@ export function renderHelp(topic = "", options = {}) {
480
509
  for (const detail of details) { out.line(); out.line(detail, detail.startsWith("qz ") ? "accent" : undefined); }
481
510
  } else {
482
511
  out.line("qz · Your browser, from the terminal", "accent");
483
- out.line("Run tasks with the qz browser agent, manage results, and connect via MCP.");
512
+ out.line("Drive your browser with sessions and scripts, or delegate a task to the built-in agent.");
484
513
  out.line();
485
- for (const { help, summary: description } of Object.values(COMMANDS)) {
486
- if (!help) continue;
487
- const name = help[0].replace(/^qz /, "").replace(/ \[.*$/, "");
488
- if (out.width >= 64) out.line(` ${pad(name, 23)} ${description}`);
489
- else { out.command(`qz ${name}`); out.line(` ${description}`, "muted"); }
514
+ const entries = Object.entries(COMMANDS);
515
+ for (const direct of [true, false]) {
516
+ out.line(direct ? "Browser scripts" : "Connection, built-in agent and saved results", "accent");
517
+ for (const [command, { help, summary: description }] of entries) {
518
+ if (!help || (["session", "script"].includes(command)) !== direct) continue;
519
+ const name = help[0].replace(/^qz /, "").replace(/ \[.*$/, "");
520
+ if (out.width >= 64) out.line(` ${pad(name, 23)} ${description}`);
521
+ else { out.command(`qz ${name}`); out.line(` ${description}`, "muted"); }
522
+ }
523
+ out.line();
490
524
  }
491
525
  out.line(); out.command('qz create "Summarize this page." --stream');
492
526
  out.command('qz create "Compare these tabs." --title Research --wait');
@@ -503,15 +537,31 @@ export function renderHelp(topic = "", options = {}) {
503
537
  out.line("Version: qz --version (-v)", "muted");
504
538
  out.line("npm: npm run qz -- COMMAND [options]", "muted");
505
539
  out.shellNote();
540
+ if (!topic) {
541
+ out.line();
542
+ out.line("Full browser guide: qz skill", "muted");
543
+ }
506
544
  return out.finish();
507
545
  }
508
546
 
509
547
  export function renderError(payload, options = {}) {
510
548
  const out = output(options);
511
- const error = payload?.error || payload || {};
512
- const waiting = error.code === "connection_pending";
513
- out.line(waiting ? "Waiting for Chrome" : "qz error", waiting ? "waiting" : "error");
549
+ const value = payload?.error || payload || {};
550
+ const error = { ...value.details, ...value };
551
+ const waiting = error.code === "CONNECTION_PENDING";
552
+ out.line(waiting ? "Waiting for browser extension" : "qz error", waiting ? "waiting" : "error");
514
553
  out.line(error.message || "The command could not be completed.");
554
+ if (error.code === "CLEANUP_INCOMPLETE") {
555
+ for (const service of error.services || []) {
556
+ if (service.status === "skipped") out.field(service.endpoint || service.scopeId, service.message || service.code);
557
+ }
558
+ }
559
+ if (waiting || error.code === "CLIENT_OFFLINE") {
560
+ out.field("Stage", error.stage);
561
+ out.field("Browser", error.selectedBrowser?.name);
562
+ out.field("UUID", error.selectedBrowser?.clientId);
563
+ if (Array.isArray(error.attemptedBrowsers)) out.field("Browsers tried", error.attemptedBrowsers.slice(0, 3).join(", ") || "None");
564
+ }
515
565
  if (error.url) { out.line(); out.line(error.url, "accent"); }
516
566
  if (error.hint) { out.line(); out.line(error.hint); }
517
567
  if (error.command) out.script(error.command);
package/src/tools.mjs CHANGED
@@ -5,6 +5,9 @@ import { AjvJsonSchemaValidator } from "@modelcontextprotocol/sdk/validation/ajv
5
5
  import { BridgeError } from "./state.mjs";
6
6
  import { waitForTask } from "./task-stream.mjs";
7
7
  import { PACKAGE_VERSION } from "./package-info.mjs";
8
+ import { driveGuide } from "./drive-guide.mjs";
9
+ import { driveExtensionTimeoutSeconds } from "./drive-timeout.mjs";
10
+ export { driveExtensionTimeoutSeconds } from "./drive-timeout.mjs";
8
11
 
9
12
  // Public MCP tools use the extension's version 1 wire protocol.
10
13
  export const TOOL_CATALOG = JSON.parse(readFileSync(new URL("./catalog.json", import.meta.url), "utf8"))
@@ -50,7 +53,7 @@ function argumentsWithDefaults(definition, input) {
50
53
  }));
51
54
  }
52
55
 
53
- export async function executeMcpTool(state, name, input = {}, { signal, onTaskUpdate } = {}) {
56
+ export async function executeMcpTool(state, name, input = {}, { signal, onTaskUpdate, clientName } = {}) {
54
57
  const args = validateMcpToolInput(name, input);
55
58
  if (name === "qingzhou_bridge_status") return state.status();
56
59
  if (name === "qingzhou_forget_browser") return state.forgetBrowser(args.clientId);
@@ -62,12 +65,12 @@ export async function executeMcpTool(state, name, input = {}, { signal, onTaskUp
62
65
  const connectionSignal = state.signal(connection.controller.signal);
63
66
  const operationSignal = signal ? AbortSignal.any([connectionSignal, signal]) : connectionSignal;
64
67
  operationSignal.throwIfAborted();
65
- const result = await executeBrowserTool(state, name, args, { signal: operationSignal, connectionSignal, onTaskUpdate });
68
+ const result = await executeBrowserTool(state, name, args, { signal: operationSignal, connectionSignal, onTaskUpdate, clientName });
66
69
  return args.clientId && (observableTools.has(name) || taskInputResults.has(name) || ["qingzhou_list_sessions", "qingzhou_get_user_input", "qingzhou_stop_session", "qingzhou_abort_session"].includes(name)) && result && typeof result === "object" && !Array.isArray(result)
67
70
  ? { ...result, clientId: args.clientId } : result;
68
71
  }
69
72
 
70
- async function executeBrowserTool(state, name, args, { signal, connectionSignal, onTaskUpdate }) {
73
+ async function executeBrowserTool(state, name, args, { signal, connectionSignal, onTaskUpdate, clientName }) {
71
74
  const observe = observableTools.has(name) && (name === "qingzhou_watch_task" || args.wait || args.stream);
72
75
  const asyncTool = name === "qingzhou_execute_tool" && args.wait === false;
73
76
  const options = { timeoutSeconds: args.timeoutSeconds, clientId: args.clientId, signal };
@@ -88,21 +91,24 @@ async function executeBrowserTool(state, name, args, { signal, connectionSignal,
88
91
  });
89
92
  } catch (cause) {
90
93
  const code = typeof cause.code === "string" ? cause.code
91
- : cause.name === "TimeoutError" ? "timeout" : cause.name === "AbortError" ? "interrupted" : "task_wait_failed";
94
+ : cause.name === "TimeoutError" ? "TIMEOUT" : cause.name === "AbortError" ? "INTERRUPTED" : "TASK_WAIT_FAILED";
92
95
  const error = Object.assign(new Error(cause.message, { cause }), { code });
93
96
  error.details = { ...cause.details, sessionId, clientId: args.clientId,
94
- recovery: code === "task_deleted"
97
+ recovery: code === "TASK_DELETED"
95
98
  ? { tool: "qingzhou_list_sessions", arguments: { clientId: args.clientId } }
96
- : { tool: code === "task_changed" ? "qingzhou_get_session_snapshot" : "qingzhou_watch_task",
99
+ : { tool: code === "TASK_CHANGED" ? "qingzhou_get_session_snapshot" : "qingzhou_watch_task",
97
100
  arguments: { sessionId, clientId: args.clientId } } };
98
101
  throw error;
99
102
  }
100
103
  };
101
104
  if (name === "qingzhou_watch_task") return { sessionId: args.sessionId, snapshot: await watch(args.sessionId, { allowIdle: true }) };
102
105
  if (name === "qingzhou_get_tool") {
103
- const listed = await state.callExtension("list_tools", { sessionId: args.sessionId, timeoutSeconds: args.timeoutSeconds }, options);
104
- const tool = (Array.isArray(listed) ? listed : listed?.tools || []).find(tool => (tool.name || tool.function?.name) === args.name);
105
- if (!tool) throw new BridgeError("tool_not_found", `No browser tool named ${args.name}. Use qingzhou_list_tools to list available tools.`);
106
+ const tool = await state.callExtension("get_tool", {
107
+ sessionId: args.sessionId, name: args.name, timeoutSeconds: args.timeoutSeconds,
108
+ }, options);
109
+ if (!tool || typeof tool !== "object") {
110
+ throw new BridgeError("TOOL_NOT_FOUND", `No browser tool named ${args.name}. Use qingzhou_list_tools to list available tools.`);
111
+ }
106
112
  return tool;
107
113
  }
108
114
  if (name === "qingzhou_call") return state.callExtension(args.method.trim(), args.params || {}, options);
@@ -116,11 +122,35 @@ async function executeBrowserTool(state, name, args, { signal, connectionSignal,
116
122
  }
117
123
  if (name === "qingzhou_follow_up") params.images ||= [];
118
124
  if (name === "qingzhou_submit_wait_form") params.answers ||= {};
125
+ if (name === "qingzhou_create_drive_session") {
126
+ const displayName = String(clientName || "").trim();
127
+ if (displayName) params.clientName = displayName;
128
+ }
119
129
  if (name === "qingzhou_execute_tool") {
130
+ const scriptApiVersion = params.scriptApiVersion;
131
+ if (scriptApiVersion != null) {
132
+ const capabilities = await state.callExtension("capabilities", {}, options);
133
+ if (capabilities?.scriptApiVersion !== scriptApiVersion) {
134
+ throw Object.assign(new BridgeError("UPGRADE_REQUIRED", "The connected extension does not support this script API. No script was executed."), {
135
+ hint: "Update the CLI and extension together, then reconnect before running the script.",
136
+ });
137
+ }
138
+ }
120
139
  delete params.wait;
121
- params.args ||= {};
122
- return asyncTool ? { ...state.startExtensionJob("execute_tool", params, { ...options, connectionSignal }), sessionId: args.sessionId, clientId }
123
- : state.callExtension("execute_tool", params, options);
140
+ if (!Array.isArray(params.calls)) params.args ||= {};
141
+ const displayName = String(clientName || "").trim();
142
+ if (displayName) params.clientName = displayName;
143
+ const requestTimeout = driveExtensionTimeoutSeconds(params, args.timeoutSeconds || state.requestTimeoutSeconds || 180);
144
+ const callOptions = { ...options, timeoutSeconds: requestTimeout, cancelOnAbort: !asyncTool };
145
+ const invoke = () => asyncTool
146
+ ? { ...state.startExtensionJob("execute_tool", params, { ...callOptions, connectionSignal }), sessionId: args.sessionId, clientId }
147
+ : state.callExtension("execute_tool", params, callOptions);
148
+ try {
149
+ return await invoke();
150
+ } catch (error) {
151
+ if (error?.code === "DRIVE_SESSION_BUSY") return await invoke();
152
+ throw error;
153
+ }
124
154
  }
125
155
  const method = name === "qingzhou_abort_session" ? "stop_session" : name.slice("qingzhou_".length);
126
156
  let result = await state.callExtension(method, params, options);
@@ -133,14 +163,15 @@ async function executeBrowserTool(state, name, args, { signal, connectionSignal,
133
163
  }
134
164
  result = bindInputClient(name, result, clientId);
135
165
  if (observe) {
136
- const sessionId = name === "qingzhou_create_session" ? result?.session?.id || result?.sessionId : args.sessionId;
166
+ const sessionId = name === "qingzhou_create_session"
167
+ ? result?.session?.sessionId || result?.sessionId || result?.session?.id : args.sessionId;
137
168
  if (name === "qingzhou_create_session") {
138
- if (!sessionId) throw new BridgeError("invalid_response", "The task was created without an ID. Use qingzhou_list_sessions to find it before retrying.");
169
+ if (!sessionId) throw new BridgeError("INVALID_RESPONSE", "The task was created without an ID. Use qingzhou_list_sessions to find it before retrying.");
139
170
  await onTaskUpdate?.({ type: "created", sessionId, clientId, ...result });
140
171
  result = { ...result, snapshot: await watch(sessionId, { allowIdle: !args.firstMessage.trim() }) };
141
172
  } else if (name === "qingzhou_send_message" && result?.accepted === true) {
142
173
  if (typeof result.clientMessageId !== "string" || !result.clientMessageId.trim()) {
143
- throw Object.assign(new BridgeError("invalid_response", "The message was accepted without a tracking ID. Continue watching the same task before sending it again."), {
174
+ throw Object.assign(new BridgeError("INVALID_RESPONSE", "The message was accepted without a tracking ID. Continue watching the same task before sending it again."), {
144
175
  details: { sessionId, clientId, recovery: { tool: "qingzhou_watch_task", arguments: { sessionId, clientId } } },
145
176
  });
146
177
  }
@@ -156,44 +187,131 @@ async function executeBrowserTool(state, name, args, { signal, connectionSignal,
156
187
  /** Validate input against the advertised tool schema and apply its defaults. */
157
188
  export function validateMcpToolInput(name, input = {}) {
158
189
  const definition = definitions.get(name);
159
- if (!definition) throw Object.assign(new Error(`Unknown tool: ${name}. Read the MCP tool list for available names.`), { code: "invalid_arguments" });
190
+ if (!definition) throw Object.assign(new Error(`Unknown tool: ${name}. Read the MCP tool list for available names.`), { code: "INVALID_ARGUMENTS" });
160
191
  try {
161
192
  return argumentsWithDefaults(definition, input);
162
193
  } catch (error) {
163
- error.code = "invalid_arguments";
194
+ error.code = "INVALID_ARGUMENTS";
164
195
  throw error;
165
196
  }
166
197
  }
167
198
 
168
- export function resultToContent(result) {
199
+ function imageEntries(value) {
200
+ if (!value || typeof value !== "object" || Array.isArray(value) || !Array.isArray(value.images)) return [];
201
+ return value.images.filter(image => image && typeof image.data === "string" && typeof image.mimeType === "string");
202
+ }
203
+
204
+ function withoutImages(value) {
205
+ if (!imageEntries(value).length) return value;
206
+ const { images: _images, ...rest } = value;
207
+ return rest;
208
+ }
209
+
210
+ function imageBlock(image, meta) {
211
+ return {
212
+ type: "image",
213
+ data: image.data,
214
+ mimeType: image.mimeType,
215
+ ...(meta && Object.keys(meta).length ? { _meta: meta } : {}),
216
+ };
217
+ }
218
+
219
+ function peelCallImages(value, callId, sink) {
220
+ const images = imageEntries(value);
221
+ if (!images.length) return value;
222
+ const placeholders = images.map((image, offset) => {
223
+ const meta = { ...(callId ? { id: callId } : {}), index: offset };
224
+ sink.push(imageBlock(image, meta));
225
+ return meta;
226
+ });
227
+ return { ...withoutImages(value), images: placeholders };
228
+ }
229
+
230
+ export function resultToContent(result, { toolResult = false } = {}) {
169
231
  if (result == null) return [];
170
- if (Array.isArray(result)) return result.flatMap(resultToContent);
171
- return [{ type: "text", text: typeof result === "string" ? result : JSON.stringify(result, null, 2) }];
232
+ if (Array.isArray(result)) return result.flatMap(item => resultToContent(item));
233
+ if (typeof result !== "object") return [{ type: "text", text: String(result) }];
234
+ const nested = [];
235
+ let body = result;
236
+ if (toolResult && Array.isArray(result.results)) {
237
+ body = {
238
+ ...result,
239
+ results: result.results.map(item => {
240
+ if (!item || typeof item !== "object" || Array.isArray(item)) return item;
241
+ const callId = String(item.id || "");
242
+ return peelCallImages(item, callId, nested);
243
+ }),
244
+ };
245
+ }
246
+ const top = toolResult ? imageEntries(body) : [];
247
+ if (!top.length && !nested.length) {
248
+ return [{ type: "text", text: JSON.stringify(body) }];
249
+ }
250
+ return [
251
+ { type: "text", text: JSON.stringify(top.length ? withoutImages(body) : body) },
252
+ ...top.map(image => imageBlock(image)),
253
+ ...nested,
254
+ ];
172
255
  }
173
256
 
174
- export function createMcpServer(state) {
257
+ const DRIVE_TOOL_NAMES = ["qingzhou_guide", "qingzhou_create_drive_session", "qingzhou_list_drive_sessions", "qingzhou_get_drive_session", "qingzhou_stop_session", "qingzhou_execute_tool", "qingzhou_list_tools", "qingzhou_get_tool", "qingzhou_bridge_status"];
258
+ const GUIDE_TOOL = {
259
+ name: "qingzhou_guide",
260
+ description: "Return the qz CLI direct-drive guide as an optional reference.",
261
+ inputSchema: {
262
+ type: "object",
263
+ additionalProperties: false,
264
+ properties: {},
265
+ },
266
+ };
267
+ const DRIVE_INSTRUCTIONS = "先用 qingzhou_create_drive_session 或 qz session create --title TITLE 创建带可读标题的会话,后续每次执行工具或脚本都传入该 sessionId(CLI 必传 --session,缺失退出码为 2,不会自动建会话);list_tools/get_tool 查询基础目录无需会话。页面工具必传 tabRef(open_tab 设置的可选 tabAlias,或 open_tab / list_tabs 返回的 tabRef)。通过 qingzhou_execute_tool(单工具、calls 批次或 run_script 外层编排)执行操作,页面内 DOM 求值使用 evaluate。Create a session with a readable title using qingzhou_create_drive_session or qz session create --title TITLE, then pass that sessionId on every tool or script execution (CLI calls require --session; omitting it exits 2 and does not create a session). list_tools/get_tool can inspect the base catalog without a session. Page tools require tabRef (an optional tabAlias from open_tab, or the tabRef returned by open_tab or list_tabs). Orchestrate with qingzhou_execute_tool (single tool, calls batch, or outer run_script) and use evaluate for in-page DOM execution.";
268
+
269
+ export function mcpCatalogProfile({ clientName = "", profile = "" } = {}) {
270
+ const explicit = String(profile || "").trim().toLowerCase();
271
+ if (explicit === "full" || explicit === "drive") return explicit;
272
+ return clientName === "qz-cli" ? "full" : "drive";
273
+ }
274
+
275
+ export function driveToolCatalog() {
276
+ const byName = new Map(TOOL_CATALOG.map(tool => [tool.name, tool]));
277
+ return DRIVE_TOOL_NAMES.map(name => name === "qingzhou_guide" ? GUIDE_TOOL : byName.get(name));
278
+ }
279
+
280
+ export function createMcpServer(state, { clientName = "", profile = "" } = {}) {
281
+ const catalogProfile = mcpCatalogProfile({ clientName, profile });
175
282
  const server = new Server({ name: "qz-browser-agent-mcp-bridge", version: PACKAGE_VERSION }, {
176
- capabilities: { tools: {} }, instructions: "Control qz tasks and browser tools through the local bridge.",
283
+ capabilities: { tools: {} },
284
+ instructions: catalogProfile === "drive" ? DRIVE_INSTRUCTIONS : "Control qz tasks and browser tools through the local bridge.",
177
285
  });
178
- server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: TOOL_CATALOG }));
286
+ server.setRequestHandler(ListToolsRequestSchema, async () => ({
287
+ tools: catalogProfile === "drive" ? driveToolCatalog() : TOOL_CATALOG,
288
+ }));
179
289
  server.setRequestHandler(CallToolRequestSchema, async (request, extra) => {
180
290
  try {
181
291
  let progress = 0;
182
292
  const progressToken = request.params._meta?.progressToken;
183
- const result = await executeMcpTool(state, request.params.name, request.params.arguments || {}, { signal: extra.signal,
293
+ const input = { ...(request.params.arguments || {}) };
294
+ if (request.params.name === "qingzhou_guide") {
295
+ return { content: [{ type: "text", text: driveGuide() }] };
296
+ }
297
+ if (catalogProfile === "drive" && request.params.name === "qingzhou_list_sessions") {
298
+ if (!String(input.scope || "").trim()) input.scope = "session";
299
+ }
300
+ if (request.params.name === "qingzhou_execute_tool" && !String(input.sessionId || "").trim()) {
301
+ throw new BridgeError("DRIVE_SESSION_REQUIRED", "Pass sessionId from qz session create (MCP: qingzhou_create_drive_session). CLI calls require --session. This call does not create a session.");
302
+ }
303
+ const result = await executeMcpTool(state, request.params.name, input, { signal: extra.signal, clientName,
184
304
  onTaskUpdate: progressToken === undefined ? undefined : event => extra.sendNotification({
185
305
  method: "notifications/progress", params: { progressToken, progress: ++progress,
186
306
  message: ["text", "replace"].includes(event.type) ? event.text : JSON.stringify(event),
187
307
  _meta: { "qz/task-event": event } },
188
308
  }).catch(() => {}),
189
309
  });
190
- return { content: resultToContent(result) };
310
+ return { content: resultToContent(result, { toolResult: request.params.name === "qingzhou_execute_tool" }) };
191
311
  } catch (error) {
192
- const message = error.message || String(error);
193
- const value = typeof error.code === "string" && error.code ? {
194
- code: error.code, message,
195
- ...(error.details && Object.keys(error.details).length ? { details: error.details } : {}),
196
- } : message;
312
+ const value = { code: String(error.code || "TOOL_EXECUTION_FAILED").toUpperCase(), message: error.message || String(error),
313
+ ...(error.hint ? { hint: error.hint } : {}),
314
+ ...(error.details && Object.keys(error.details).length ? { details: error.details } : {}) };
197
315
  return { isError: true, content: resultToContent(value) };
198
316
  }
199
317
  });