@docyrus/docyrus 0.0.20 → 0.0.21

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.
Files changed (65) hide show
  1. package/agent-loader.js +32 -1
  2. package/agent-loader.js.map +2 -2
  3. package/main.js +321 -70
  4. package/main.js.map +4 -4
  5. package/package.json +12 -2
  6. package/resources/chrome-tools/browser-content.js +103 -0
  7. package/resources/chrome-tools/browser-cookies.js +35 -0
  8. package/resources/chrome-tools/browser-eval.js +53 -0
  9. package/resources/chrome-tools/browser-hn-scraper.js +108 -0
  10. package/resources/chrome-tools/browser-nav.js +44 -0
  11. package/resources/chrome-tools/browser-pick.js +162 -0
  12. package/resources/chrome-tools/browser-screenshot.js +34 -0
  13. package/resources/chrome-tools/browser-start.js +86 -0
  14. package/resources/pi-agent/extensions/answer.ts +532 -0
  15. package/resources/pi-agent/extensions/context.ts +578 -0
  16. package/resources/pi-agent/extensions/control.ts +1779 -0
  17. package/resources/pi-agent/extensions/diff.ts +218 -0
  18. package/resources/pi-agent/extensions/files.ts +199 -0
  19. package/resources/pi-agent/extensions/loop.ts +446 -0
  20. package/resources/pi-agent/extensions/multi-edit.ts +835 -0
  21. package/resources/pi-agent/extensions/notify.ts +88 -0
  22. package/resources/pi-agent/extensions/pi-mcp-adapter/CHANGELOG.md +192 -0
  23. package/resources/pi-agent/extensions/pi-mcp-adapter/LICENSE +21 -0
  24. package/resources/pi-agent/extensions/pi-mcp-adapter/README.md +296 -0
  25. package/resources/pi-agent/extensions/pi-mcp-adapter/app-bridge.bundle.js +67 -0
  26. package/resources/pi-agent/extensions/pi-mcp-adapter/cli.js +108 -0
  27. package/resources/pi-agent/extensions/pi-mcp-adapter/commands.ts +211 -0
  28. package/resources/pi-agent/extensions/pi-mcp-adapter/config.ts +227 -0
  29. package/resources/pi-agent/extensions/pi-mcp-adapter/consent-manager.ts +64 -0
  30. package/resources/pi-agent/extensions/pi-mcp-adapter/direct-tools.ts +301 -0
  31. package/resources/pi-agent/extensions/pi-mcp-adapter/errors.ts +219 -0
  32. package/resources/pi-agent/extensions/pi-mcp-adapter/glimpse-ui.ts +80 -0
  33. package/resources/pi-agent/extensions/pi-mcp-adapter/host-html-template.ts +427 -0
  34. package/resources/pi-agent/extensions/pi-mcp-adapter/index.ts +232 -0
  35. package/resources/pi-agent/extensions/pi-mcp-adapter/init.ts +319 -0
  36. package/resources/pi-agent/extensions/pi-mcp-adapter/lifecycle.ts +93 -0
  37. package/resources/pi-agent/extensions/pi-mcp-adapter/logger.ts +169 -0
  38. package/resources/pi-agent/extensions/pi-mcp-adapter/mcp-panel.ts +713 -0
  39. package/resources/pi-agent/extensions/pi-mcp-adapter/metadata-cache.ts +191 -0
  40. package/resources/pi-agent/extensions/pi-mcp-adapter/npx-resolver.ts +419 -0
  41. package/resources/pi-agent/extensions/pi-mcp-adapter/oauth-handler.ts +56 -0
  42. package/resources/pi-agent/extensions/pi-mcp-adapter/package.json +85 -0
  43. package/resources/pi-agent/extensions/pi-mcp-adapter/paths.ts +29 -0
  44. package/resources/pi-agent/extensions/pi-mcp-adapter/proxy-modes.ts +635 -0
  45. package/resources/pi-agent/extensions/pi-mcp-adapter/resource-tools.ts +17 -0
  46. package/resources/pi-agent/extensions/pi-mcp-adapter/server-manager.ts +330 -0
  47. package/resources/pi-agent/extensions/pi-mcp-adapter/state.ts +41 -0
  48. package/resources/pi-agent/extensions/pi-mcp-adapter/tool-metadata.ts +144 -0
  49. package/resources/pi-agent/extensions/pi-mcp-adapter/tool-registrar.ts +46 -0
  50. package/resources/pi-agent/extensions/pi-mcp-adapter/types.ts +367 -0
  51. package/resources/pi-agent/extensions/pi-mcp-adapter/ui-resource-handler.ts +145 -0
  52. package/resources/pi-agent/extensions/pi-mcp-adapter/ui-server.ts +623 -0
  53. package/resources/pi-agent/extensions/pi-mcp-adapter/ui-session.ts +384 -0
  54. package/resources/pi-agent/extensions/pi-mcp-adapter/ui-stream-types.ts +89 -0
  55. package/resources/pi-agent/extensions/pi-mcp-adapter/utils.ts +75 -0
  56. package/resources/pi-agent/extensions/prompt-editor.ts +1315 -0
  57. package/resources/pi-agent/extensions/prompt-url-widget.ts +158 -0
  58. package/resources/pi-agent/extensions/redraws.ts +24 -0
  59. package/resources/pi-agent/extensions/review.ts +2160 -0
  60. package/resources/pi-agent/extensions/todos.ts +2076 -0
  61. package/resources/pi-agent/extensions/tps.ts +47 -0
  62. package/resources/pi-agent/extensions/whimsical.ts +474 -0
  63. package/resources/pi-agent/skills/changelog-generator/SKILL.md +425 -0
  64. package/resources/pi-agent/skills/docyrus-chrome-devtools-cli/SKILL.md +80 -0
  65. package/resources/pi-agent/skills/docyrus-platform/references/docyrus-cli-usage.md +51 -0
@@ -0,0 +1,301 @@
1
+ import type { ToolDefinition } from "@mariozechner/pi-coding-agent";
2
+ import type { McpExtensionState } from "./state.js";
3
+ import type { DirectToolSpec, McpConfig, McpContent } from "./types.js";
4
+ import type { MetadataCache } from "./metadata-cache.js";
5
+ import { lazyConnect, getFailureAgeSeconds } from "./init.js";
6
+ import { isServerCacheValid } from "./metadata-cache.js";
7
+ import { formatSchema } from "./tool-metadata.js";
8
+ import { transformMcpContent } from "./tool-registrar.js";
9
+ import { maybeStartUiSession, type UiSessionRuntime } from "./ui-session.js";
10
+ import { formatToolName } from "./types.js";
11
+ import { resourceNameToToolName } from "./resource-tools.js";
12
+
13
+ const BUILTIN_NAMES = new Set(["read", "bash", "edit", "write", "grep", "find", "ls", "mcp"]);
14
+
15
+ export function resolveDirectTools(
16
+ config: McpConfig,
17
+ cache: MetadataCache | null,
18
+ prefix: "server" | "none" | "short",
19
+ envOverride?: string[],
20
+ ): DirectToolSpec[] {
21
+ const specs: DirectToolSpec[] = [];
22
+ if (!cache) return specs;
23
+
24
+ const seenNames = new Set<string>();
25
+
26
+ const envServers = new Set<string>();
27
+ const envTools = new Map<string, Set<string>>();
28
+ if (envOverride) {
29
+ for (let item of envOverride) {
30
+ item = item.replace(/\/+$/, "");
31
+ if (item.includes("/")) {
32
+ const [server, tool] = item.split("/", 2);
33
+ if (server && tool) {
34
+ if (!envTools.has(server)) envTools.set(server, new Set());
35
+ envTools.get(server)!.add(tool);
36
+ } else if (server) {
37
+ envServers.add(server);
38
+ }
39
+ } else if (item) {
40
+ envServers.add(item);
41
+ }
42
+ }
43
+ }
44
+
45
+ const globalDirect = config.settings?.directTools;
46
+
47
+ for (const [serverName, definition] of Object.entries(config.mcpServers)) {
48
+ const serverCache = cache.servers[serverName];
49
+ if (!serverCache || !isServerCacheValid(serverCache, definition)) continue;
50
+
51
+ let toolFilter: true | string[] | false = false;
52
+
53
+ if (envOverride) {
54
+ if (envServers.has(serverName)) {
55
+ toolFilter = true;
56
+ } else if (envTools.has(serverName)) {
57
+ toolFilter = [...envTools.get(serverName)!];
58
+ }
59
+ } else {
60
+ if (definition.directTools !== undefined) {
61
+ toolFilter = definition.directTools;
62
+ } else if (globalDirect) {
63
+ toolFilter = globalDirect;
64
+ }
65
+ }
66
+
67
+ if (!toolFilter) continue;
68
+
69
+ for (const tool of serverCache.tools ?? []) {
70
+ if (toolFilter !== true && !toolFilter.includes(tool.name)) continue;
71
+ const prefixedName = formatToolName(tool.name, serverName, prefix);
72
+ if (BUILTIN_NAMES.has(prefixedName)) {
73
+ console.warn(`MCP: skipping direct tool "${prefixedName}" (collides with builtin)`);
74
+ continue;
75
+ }
76
+ if (seenNames.has(prefixedName)) {
77
+ console.warn(`MCP: skipping duplicate direct tool "${prefixedName}" from "${serverName}"`);
78
+ continue;
79
+ }
80
+ seenNames.add(prefixedName);
81
+ specs.push({
82
+ serverName,
83
+ originalName: tool.name,
84
+ prefixedName,
85
+ description: tool.description ?? "",
86
+ inputSchema: tool.inputSchema,
87
+ uiResourceUri: tool.uiResourceUri,
88
+ uiStreamMode: tool.uiStreamMode,
89
+ });
90
+ }
91
+
92
+ if (definition.exposeResources !== false) {
93
+ for (const resource of serverCache.resources ?? []) {
94
+ const baseName = `get_${resourceNameToToolName(resource.name)}`;
95
+ if (toolFilter !== true && !toolFilter.includes(baseName)) continue;
96
+ const prefixedName = formatToolName(baseName, serverName, prefix);
97
+ if (BUILTIN_NAMES.has(prefixedName)) {
98
+ console.warn(`MCP: skipping direct resource tool "${prefixedName}" (collides with builtin)`);
99
+ continue;
100
+ }
101
+ if (seenNames.has(prefixedName)) {
102
+ console.warn(`MCP: skipping duplicate direct resource tool "${prefixedName}" from "${serverName}"`);
103
+ continue;
104
+ }
105
+ seenNames.add(prefixedName);
106
+ specs.push({
107
+ serverName,
108
+ originalName: baseName,
109
+ prefixedName,
110
+ description: resource.description ?? `Read resource: ${resource.uri}`,
111
+ resourceUri: resource.uri,
112
+ });
113
+ }
114
+ }
115
+ }
116
+
117
+ return specs;
118
+ }
119
+
120
+ export function buildProxyDescription(
121
+ config: McpConfig,
122
+ cache: MetadataCache | null,
123
+ directSpecs: DirectToolSpec[],
124
+ ): string {
125
+ let desc = `MCP gateway - connect to MCP servers and call their tools.\n`;
126
+
127
+ const directByServer = new Map<string, number>();
128
+ for (const spec of directSpecs) {
129
+ directByServer.set(spec.serverName, (directByServer.get(spec.serverName) ?? 0) + 1);
130
+ }
131
+ if (directByServer.size > 0) {
132
+ const parts = [...directByServer.entries()].map(
133
+ ([server, count]) => `${server} (${count})`,
134
+ );
135
+ desc += `\nDirect tools available (call as normal tools): ${parts.join(", ")}\n`;
136
+ }
137
+
138
+ const serverSummaries: string[] = [];
139
+ for (const serverName of Object.keys(config.mcpServers)) {
140
+ const entry = cache?.servers?.[serverName];
141
+ const definition = config.mcpServers[serverName];
142
+ const toolCount = entry?.tools?.length ?? 0;
143
+ const resourceCount = definition?.exposeResources !== false ? (entry?.resources?.length ?? 0) : 0;
144
+ const totalItems = toolCount + resourceCount;
145
+ if (totalItems === 0) continue;
146
+ const directCount = directByServer.get(serverName) ?? 0;
147
+ const proxyCount = totalItems - directCount;
148
+ if (proxyCount > 0) {
149
+ serverSummaries.push(`${serverName} (${proxyCount} tools)`);
150
+ }
151
+ }
152
+
153
+ if (serverSummaries.length > 0) {
154
+ desc += `\nServers: ${serverSummaries.join(", ")}\n`;
155
+ }
156
+
157
+ desc += `\nUsage:\n`;
158
+ desc += ` mcp({ }) → Show server status\n`;
159
+ desc += ` mcp({ server: "name" }) → List tools from server\n`;
160
+ desc += ` mcp({ search: "query" }) → Search for tools (MCP + pi, space-separated words OR'd)\n`;
161
+ desc += ` mcp({ describe: "tool_name" }) → Show tool details and parameters\n`;
162
+ desc += ` mcp({ connect: "server-name" }) → Connect to a server and refresh metadata\n`;
163
+ desc += ` mcp({ tool: "name", args: '{"key": "value"}' }) → Call a tool (args is JSON string)\n`;
164
+ desc += ` mcp({ action: "ui-messages" }) → Retrieve accumulated messages from completed UI sessions\n`;
165
+ desc += `\nMode: tool (call) > connect > describe > search > server (list) > action > nothing (status)`;
166
+
167
+ return desc;
168
+ }
169
+
170
+ type DirectToolExecute = ToolDefinition["execute"];
171
+
172
+ export function createDirectToolExecutor(
173
+ getState: () => McpExtensionState | null,
174
+ getInitPromise: () => Promise<McpExtensionState> | null,
175
+ spec: DirectToolSpec
176
+ ): DirectToolExecute {
177
+ return async function execute(_toolCallId, params) {
178
+ let state = getState();
179
+ const initPromise = getInitPromise();
180
+
181
+ if (!state && initPromise) {
182
+ try {
183
+ state = await initPromise;
184
+ } catch {
185
+ return {
186
+ content: [{ type: "text" as const, text: "MCP initialization failed" }],
187
+ details: { error: "init_failed" },
188
+ };
189
+ }
190
+ }
191
+ if (!state) {
192
+ return {
193
+ content: [{ type: "text" as const, text: "MCP not initialized" }],
194
+ details: { error: "not_initialized" },
195
+ };
196
+ }
197
+
198
+ const connected = await lazyConnect(state, spec.serverName);
199
+ if (!connected) {
200
+ const failedAgo = getFailureAgeSeconds(state, spec.serverName);
201
+ return {
202
+ content: [{ type: "text" as const, text: `MCP server "${spec.serverName}" not available${failedAgo !== null ? ` (failed ${failedAgo}s ago)` : ""}` }],
203
+ details: { error: "server_unavailable", server: spec.serverName },
204
+ };
205
+ }
206
+
207
+ const connection = state.manager.getConnection(spec.serverName);
208
+ if (!connection || connection.status !== "connected") {
209
+ return {
210
+ content: [{ type: "text" as const, text: `MCP server "${spec.serverName}" not connected` }],
211
+ details: { error: "not_connected", server: spec.serverName },
212
+ };
213
+ }
214
+
215
+ let uiSession: UiSessionRuntime | null = null;
216
+
217
+ try {
218
+ state.manager.touch(spec.serverName);
219
+ state.manager.incrementInFlight(spec.serverName);
220
+
221
+ if (spec.resourceUri) {
222
+ const result = await connection.client.readResource({ uri: spec.resourceUri });
223
+ const content = (result.contents ?? []).map(c => ({
224
+ type: "text" as const,
225
+ text: "text" in c ? c.text : ("blob" in c ? `[Binary data: ${(c as { mimeType?: string }).mimeType ?? "unknown"}]` : JSON.stringify(c)),
226
+ }));
227
+ return {
228
+ content: content.length > 0 ? content : [{ type: "text" as const, text: "(empty resource)" }],
229
+ details: { server: spec.serverName, resourceUri: spec.resourceUri },
230
+ };
231
+ }
232
+
233
+ const hasUi = !!spec.uiResourceUri;
234
+ uiSession = hasUi
235
+ ? await maybeStartUiSession(state, {
236
+ serverName: spec.serverName,
237
+ toolName: spec.originalName,
238
+ toolArgs: params ?? {},
239
+ uiResourceUri: spec.uiResourceUri!,
240
+ streamMode: spec.uiStreamMode,
241
+ })
242
+ : null;
243
+
244
+ const resultPromise = connection.client.callTool({
245
+ name: spec.originalName,
246
+ arguments: params ?? {},
247
+ _meta: uiSession?.requestMeta,
248
+ });
249
+
250
+ const result = await resultPromise;
251
+ uiSession?.sendToolResult(result as unknown as import("@modelcontextprotocol/sdk/types.js").CallToolResult);
252
+
253
+ const mcpContent = (result.content ?? []) as McpContent[];
254
+ const content = transformMcpContent(mcpContent);
255
+
256
+ if (result.isError) {
257
+ let errorText = content.filter(c => c.type === "text").map(c => (c as { text: string }).text).join("\n") || "Tool execution failed";
258
+ if (spec.inputSchema) {
259
+ errorText += `\n\nExpected parameters:\n${formatSchema(spec.inputSchema)}`;
260
+ }
261
+ return {
262
+ content: [{ type: "text" as const, text: `Error: ${errorText}` }],
263
+ details: { error: "tool_error", server: spec.serverName },
264
+ };
265
+ }
266
+
267
+ const resultText = content.filter(c => c.type === "text").map(c => (c as { text: string }).text).join("\n") || "(empty result)";
268
+ if (hasUi) {
269
+ const uiMessage = uiSession?.reused
270
+ ? "Updated the open UI."
271
+ : "📺 Interactive UI is now open in your browser. I'll respond to your prompts and intents as you interact with it.";
272
+ return {
273
+ content: [{ type: "text" as const, text: `${resultText}\n\n${uiMessage}` }],
274
+ details: { server: spec.serverName, tool: spec.originalName, uiOpen: true },
275
+ };
276
+ }
277
+
278
+ return {
279
+ content: content.length > 0 ? content : [{ type: "text" as const, text: "(empty result)" }],
280
+ details: { server: spec.serverName, tool: spec.originalName },
281
+ };
282
+ } catch (error) {
283
+ const message = error instanceof Error ? error.message : String(error);
284
+ uiSession?.sendToolCancelled(message);
285
+ let errorText = `Failed to call tool: ${message}`;
286
+ if (spec.inputSchema) {
287
+ errorText += `\n\nExpected parameters:\n${formatSchema(spec.inputSchema)}`;
288
+ }
289
+ return {
290
+ content: [{ type: "text" as const, text: errorText }],
291
+ details: { error: "call_failed", server: spec.serverName },
292
+ };
293
+ } finally {
294
+ if (uiSession?.reused) {
295
+ uiSession.close();
296
+ }
297
+ state.manager.decrementInFlight(spec.serverName);
298
+ state.manager.touch(spec.serverName);
299
+ }
300
+ };
301
+ }
@@ -0,0 +1,219 @@
1
+ /**
2
+ * Custom error types for MCP UI operations.
3
+ * Provides structured errors with context and recovery hints.
4
+ */
5
+
6
+ export interface McpUiErrorContext {
7
+ server?: string;
8
+ tool?: string;
9
+ uri?: string;
10
+ session?: string;
11
+ [key: string]: unknown;
12
+ }
13
+
14
+ /**
15
+ * Base error class for MCP UI errors.
16
+ */
17
+ export class McpUiError extends Error {
18
+ readonly code: string;
19
+ readonly context: McpUiErrorContext;
20
+ readonly recoveryHint?: string;
21
+ readonly cause?: Error;
22
+
23
+ constructor(
24
+ message: string,
25
+ options: {
26
+ code: string;
27
+ context?: McpUiErrorContext;
28
+ recoveryHint?: string;
29
+ cause?: Error;
30
+ }
31
+ ) {
32
+ super(message);
33
+ this.name = "McpUiError";
34
+ this.code = options.code;
35
+ this.context = options.context ?? {};
36
+ this.recoveryHint = options.recoveryHint;
37
+ this.cause = options.cause;
38
+
39
+ // Maintain proper stack trace
40
+ if (Error.captureStackTrace) {
41
+ Error.captureStackTrace(this, this.constructor);
42
+ }
43
+ }
44
+
45
+ toJSON(): Record<string, unknown> {
46
+ return {
47
+ name: this.name,
48
+ code: this.code,
49
+ message: this.message,
50
+ context: this.context,
51
+ recoveryHint: this.recoveryHint,
52
+ stack: this.stack,
53
+ };
54
+ }
55
+ }
56
+
57
+ /**
58
+ * Error fetching a UI resource from the MCP server.
59
+ */
60
+ export class ResourceFetchError extends McpUiError {
61
+ constructor(
62
+ uri: string,
63
+ reason: string,
64
+ options?: { server?: string; cause?: Error }
65
+ ) {
66
+ super(`Failed to fetch UI resource "${uri}": ${reason}`, {
67
+ code: "RESOURCE_FETCH_ERROR",
68
+ context: { uri, server: options?.server },
69
+ recoveryHint: "Check that the MCP server is connected and the resource URI is valid.",
70
+ cause: options?.cause,
71
+ });
72
+ this.name = "ResourceFetchError";
73
+ }
74
+ }
75
+
76
+ /**
77
+ * Error parsing or validating UI resource content.
78
+ */
79
+ export class ResourceParseError extends McpUiError {
80
+ constructor(
81
+ uri: string,
82
+ reason: string,
83
+ options?: { server?: string; mimeType?: string }
84
+ ) {
85
+ super(`Invalid UI resource "${uri}": ${reason}`, {
86
+ code: "RESOURCE_PARSE_ERROR",
87
+ context: { uri, server: options?.server, mimeType: options?.mimeType },
88
+ recoveryHint: "Ensure the resource returns valid HTML with the correct MIME type.",
89
+ });
90
+ this.name = "ResourceParseError";
91
+ }
92
+ }
93
+
94
+ /**
95
+ * Error connecting to the AppBridge.
96
+ */
97
+ export class BridgeConnectionError extends McpUiError {
98
+ constructor(reason: string, options?: { session?: string; cause?: Error }) {
99
+ super(`AppBridge connection failed: ${reason}`, {
100
+ code: "BRIDGE_CONNECTION_ERROR",
101
+ context: { session: options?.session },
102
+ recoveryHint: "Check browser console for detailed errors. The iframe may have failed to load.",
103
+ cause: options?.cause,
104
+ });
105
+ this.name = "BridgeConnectionError";
106
+ }
107
+ }
108
+
109
+ /**
110
+ * Error related to user consent for tool calls.
111
+ */
112
+ export class ConsentError extends McpUiError {
113
+ readonly denied: boolean;
114
+
115
+ constructor(
116
+ server: string,
117
+ options: { denied?: boolean; requiresApproval?: boolean }
118
+ ) {
119
+ const message = options.denied
120
+ ? `Tool calls for "${server}" were denied for this session`
121
+ : `Tool call approval required for "${server}"`;
122
+
123
+ super(message, {
124
+ code: options.denied ? "CONSENT_DENIED" : "CONSENT_REQUIRED",
125
+ context: { server },
126
+ recoveryHint: options.denied
127
+ ? "The user denied tool access. Start a new session to try again."
128
+ : "Prompt the user for consent before calling tools.",
129
+ });
130
+ this.name = "ConsentError";
131
+ this.denied = options.denied ?? false;
132
+ }
133
+ }
134
+
135
+ /**
136
+ * Error with UI server session management.
137
+ */
138
+ export class SessionError extends McpUiError {
139
+ constructor(
140
+ reason: string,
141
+ options?: { session?: string; cause?: Error }
142
+ ) {
143
+ super(`Session error: ${reason}`, {
144
+ code: "SESSION_ERROR",
145
+ context: { session: options?.session },
146
+ recoveryHint: "The session may have expired or been closed. Try opening the UI again.",
147
+ cause: options?.cause,
148
+ });
149
+ this.name = "SessionError";
150
+ }
151
+ }
152
+
153
+ /**
154
+ * Error starting or operating the UI server.
155
+ */
156
+ export class ServerError extends McpUiError {
157
+ constructor(
158
+ reason: string,
159
+ options?: { port?: number; cause?: Error }
160
+ ) {
161
+ super(`UI server error: ${reason}`, {
162
+ code: "SERVER_ERROR",
163
+ context: { port: options?.port },
164
+ recoveryHint: "Check if the port is available. Another process may be using it.",
165
+ cause: options?.cause,
166
+ });
167
+ this.name = "ServerError";
168
+ }
169
+ }
170
+
171
+ /**
172
+ * Error communicating with the MCP server.
173
+ */
174
+ export class McpServerError extends McpUiError {
175
+ constructor(
176
+ server: string,
177
+ reason: string,
178
+ options?: { tool?: string; cause?: Error }
179
+ ) {
180
+ super(`MCP server "${server}" error: ${reason}`, {
181
+ code: "MCP_SERVER_ERROR",
182
+ context: { server, tool: options?.tool },
183
+ recoveryHint: "Check that the MCP server is running and responsive.",
184
+ cause: options?.cause,
185
+ });
186
+ this.name = "McpServerError";
187
+ }
188
+ }
189
+
190
+ /**
191
+ * Wrap an unknown error into an McpUiError.
192
+ */
193
+ export function wrapError(error: unknown, context?: McpUiErrorContext): McpUiError {
194
+ if (error instanceof McpUiError) {
195
+ // Merge contexts
196
+ return new McpUiError(error.message, {
197
+ code: error.code,
198
+ context: { ...error.context, ...context },
199
+ recoveryHint: error.recoveryHint,
200
+ cause: error.cause,
201
+ });
202
+ }
203
+
204
+ const cause = error instanceof Error ? error : undefined;
205
+ const message = error instanceof Error ? error.message : String(error);
206
+
207
+ return new McpUiError(message, {
208
+ code: "UNKNOWN_ERROR",
209
+ context,
210
+ cause,
211
+ });
212
+ }
213
+
214
+ /**
215
+ * Check if an error is a specific MCP UI error type.
216
+ */
217
+ export function isErrorCode(error: unknown, code: string): boolean {
218
+ return error instanceof McpUiError && error.code === code;
219
+ }
@@ -0,0 +1,80 @@
1
+ import { existsSync } from "node:fs";
2
+ import { execFileSync } from "node:child_process";
3
+ import { join, dirname } from "node:path";
4
+ import { platform } from "node:os";
5
+ import { createRequire } from "node:module";
6
+
7
+ let glimpseAvailable: boolean | null = null;
8
+ let resolvedBinaryPath: string | null = null;
9
+
10
+ export function isGlimpseAvailable(): boolean {
11
+ if (glimpseAvailable !== null) return glimpseAvailable;
12
+
13
+ if (platform() !== "darwin") {
14
+ glimpseAvailable = false;
15
+ return false;
16
+ }
17
+
18
+ resolvedBinaryPath = getGlimpseBinaryPath();
19
+ glimpseAvailable = resolvedBinaryPath !== null;
20
+ return glimpseAvailable;
21
+ }
22
+
23
+ function getGlimpseBinaryPath(): string | null {
24
+ if (process.env.GLIMPSE_BINARY && existsSync(process.env.GLIMPSE_BINARY)) {
25
+ return process.env.GLIMPSE_BINARY;
26
+ }
27
+
28
+ // Local node_modules
29
+ try {
30
+ const require = createRequire(import.meta.url);
31
+ const glimpseuiPath = require.resolve("glimpseui");
32
+ const binaryPath = join(dirname(glimpseuiPath), "glimpse");
33
+ if (existsSync(binaryPath)) return binaryPath;
34
+ } catch {}
35
+
36
+ // Global npm install
37
+ try {
38
+ const globalRoot = execFileSync("npm", ["root", "-g"], { encoding: "utf-8" }).trim();
39
+ const binaryPath = join(globalRoot, "glimpseui", "src", "glimpse");
40
+ if (existsSync(binaryPath)) return binaryPath;
41
+ } catch {}
42
+
43
+ return null;
44
+ }
45
+
46
+ export async function openGlimpseWindow(
47
+ html: string,
48
+ options: {
49
+ title: string;
50
+ width?: number;
51
+ height?: number;
52
+ onClosed: () => void;
53
+ },
54
+ ) {
55
+ const modulePath = resolvedBinaryPath
56
+ ? join(dirname(resolvedBinaryPath), "glimpse.mjs")
57
+ : "glimpseui";
58
+ const glimpse = await import(modulePath);
59
+
60
+ let active = true;
61
+ const win = glimpse.open(html, {
62
+ width: options.width ?? 900,
63
+ height: options.height ?? 700,
64
+ title: options.title,
65
+ });
66
+
67
+ win.on("closed", () => {
68
+ if (!active) return;
69
+ active = false;
70
+ options.onClosed();
71
+ });
72
+
73
+ return {
74
+ close: () => {
75
+ if (!active) return;
76
+ active = false;
77
+ win.close();
78
+ },
79
+ };
80
+ }