pi-mcp-adapter 2.1.2 → 2.2.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/errors.ts ADDED
@@ -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
+ }
package/glimpse-ui.ts ADDED
@@ -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
+ }