pi-soly 1.9.3 → 1.11.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.
Files changed (104) hide show
  1. package/ask/index.ts +12 -11
  2. package/ask/picker.ts +356 -76
  3. package/ask/prompt.ts +6 -2
  4. package/ask/tests/picker.test.ts +273 -82
  5. package/codemap.ts +276 -0
  6. package/hotreload.ts +239 -0
  7. package/init.ts +302 -0
  8. package/mcp/CHANGELOG.md +384 -0
  9. package/mcp/LICENSE +21 -0
  10. package/mcp/OAUTH.md +355 -0
  11. package/mcp/README.md +410 -0
  12. package/mcp/__tests__/agent-dir-paths.upstream-test.ts +80 -0
  13. package/mcp/__tests__/cli.upstream-test.ts +97 -0
  14. package/mcp/__tests__/commands-onboarding.upstream-test.ts +157 -0
  15. package/mcp/__tests__/config.upstream-test.ts +303 -0
  16. package/mcp/__tests__/consent-manager.upstream-test.ts +151 -0
  17. package/mcp/__tests__/direct-tools-auto-auth.upstream-test.ts +218 -0
  18. package/mcp/__tests__/direct-tools.upstream-test.ts +299 -0
  19. package/mcp/__tests__/elicitation-handler.upstream-test.ts +346 -0
  20. package/mcp/__tests__/elicitation-sdk-integration.upstream-test.ts +140 -0
  21. package/mcp/__tests__/errors.upstream-test.ts +218 -0
  22. package/mcp/__tests__/fixtures/elicitation-server.mjs +98 -0
  23. package/mcp/__tests__/host-html-template.upstream-test.ts +278 -0
  24. package/mcp/__tests__/index-lifecycle.upstream-test.ts +484 -0
  25. package/mcp/__tests__/init-elicitation.upstream-test.ts +86 -0
  26. package/mcp/__tests__/interactive-visualizer-server.upstream-test.ts +28 -0
  27. package/mcp/__tests__/logger.upstream-test.ts +175 -0
  28. package/mcp/__tests__/mcp-auth-flow-client-credentials.upstream-test.ts +612 -0
  29. package/mcp/__tests__/mcp-auth-storage.upstream-test.ts +47 -0
  30. package/mcp/__tests__/mcp-callback-server-unref.upstream-test.ts +264 -0
  31. package/mcp/__tests__/mcp-oauth-provider.upstream-test.ts +216 -0
  32. package/mcp/__tests__/mcp-panel-auth.upstream-test.ts +206 -0
  33. package/mcp/__tests__/mcp-panel-exclude-tools.upstream-test.ts +69 -0
  34. package/mcp/__tests__/mcp-panel-keybindings.upstream-test.ts +186 -0
  35. package/mcp/__tests__/npx-resolver.upstream-test.ts +54 -0
  36. package/mcp/__tests__/oauth-handler.upstream-test.ts +77 -0
  37. package/mcp/__tests__/onboarding-state.upstream-test.ts +52 -0
  38. package/mcp/__tests__/package-manifest.upstream-test.ts +22 -0
  39. package/mcp/__tests__/proxy-modes-auto-auth.upstream-test.ts +253 -0
  40. package/mcp/__tests__/proxy-modes-discovery.upstream-test.ts +84 -0
  41. package/mcp/__tests__/proxy-modes-manual-auth.upstream-test.ts +91 -0
  42. package/mcp/__tests__/proxy-modes-ui-messages.upstream-test.ts +72 -0
  43. package/mcp/__tests__/sampling-handler.upstream-test.ts +285 -0
  44. package/mcp/__tests__/server-manager-http-auth.upstream-test.ts +144 -0
  45. package/mcp/__tests__/server-manager-sampling.upstream-test.ts +236 -0
  46. package/mcp/__tests__/tool-metadata.upstream-test.ts +111 -0
  47. package/mcp/__tests__/tool-result-renderer.upstream-test.ts +147 -0
  48. package/mcp/__tests__/ui-integration.upstream-test.ts +551 -0
  49. package/mcp/__tests__/ui-resource-handler.upstream-test.ts +303 -0
  50. package/mcp/__tests__/ui-server.upstream-test.ts +967 -0
  51. package/mcp/__tests__/ui-session-messages.upstream-test.ts +72 -0
  52. package/mcp/__tests__/ui-streaming.upstream-test.ts +543 -0
  53. package/mcp/agent-dir.ts +20 -0
  54. package/mcp/app-bridge.bundle.js +67 -0
  55. package/mcp/cli.js +184 -0
  56. package/mcp/commands.ts +422 -0
  57. package/mcp/config.ts +666 -0
  58. package/mcp/consent-manager.ts +64 -0
  59. package/mcp/direct-tools.ts +439 -0
  60. package/mcp/elicitation-handler.ts +347 -0
  61. package/mcp/errors.ts +219 -0
  62. package/mcp/glimpse-ui.ts +80 -0
  63. package/mcp/host-html-template.ts +427 -0
  64. package/mcp/index.ts +362 -0
  65. package/mcp/init.ts +362 -0
  66. package/mcp/lifecycle.ts +93 -0
  67. package/mcp/logger.ts +169 -0
  68. package/mcp/mcp-auth-flow.ts +559 -0
  69. package/mcp/mcp-auth-flow.upstream-test.ts +259 -0
  70. package/mcp/mcp-auth.ts +302 -0
  71. package/mcp/mcp-auth.upstream-test.ts +373 -0
  72. package/mcp/mcp-callback-server.ts +372 -0
  73. package/mcp/mcp-callback-server.upstream-test.ts +416 -0
  74. package/mcp/mcp-oauth-provider.ts +369 -0
  75. package/mcp/mcp-oauth-provider.upstream-test.ts +518 -0
  76. package/mcp/mcp-panel.ts +829 -0
  77. package/mcp/mcp-setup-panel.ts +580 -0
  78. package/mcp/metadata-cache.ts +201 -0
  79. package/mcp/notify.ts +111 -0
  80. package/mcp/npx-resolver.ts +424 -0
  81. package/mcp/oauth-handler.ts +57 -0
  82. package/mcp/onboarding-state.ts +68 -0
  83. package/mcp/package.json +106 -0
  84. package/mcp/panel-keys.ts +37 -0
  85. package/mcp/proxy-modes.ts +949 -0
  86. package/mcp/resource-tools.ts +17 -0
  87. package/mcp/sampling-handler.ts +268 -0
  88. package/mcp/server-manager.ts +545 -0
  89. package/mcp/state.ts +41 -0
  90. package/mcp/tool-metadata.ts +216 -0
  91. package/mcp/tool-registrar.ts +46 -0
  92. package/mcp/tool-result-renderer.ts +161 -0
  93. package/mcp/types.ts +448 -0
  94. package/mcp/ui-resource-handler.ts +146 -0
  95. package/mcp/ui-server.ts +623 -0
  96. package/mcp/ui-session.ts +386 -0
  97. package/mcp/ui-stream-types.ts +89 -0
  98. package/mcp/utils.ts +129 -0
  99. package/mcp/vitest.config.ts +14 -0
  100. package/migrate.ts +258 -0
  101. package/notification.ts +218 -0
  102. package/notifications-log.ts +83 -0
  103. package/package.json +20 -3
  104. package/status.ts +140 -0
package/mcp/types.ts ADDED
@@ -0,0 +1,448 @@
1
+ // types.ts - Core type definitions
2
+ import type { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
3
+ import type { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js";
4
+ import type { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
5
+ import type { TextContent, ImageContent } from "@earendil-works/pi-ai";
6
+ import type { UiStreamMode } from "./ui-stream-types.ts";
7
+
8
+ // Transport type (stdio + HTTP)
9
+ export type Transport =
10
+ | StdioClientTransport
11
+ | SSEClientTransport
12
+ | StreamableHTTPClientTransport;
13
+
14
+ // Import sources for config
15
+ export type ImportKind =
16
+ | "cursor"
17
+ | "claude-code"
18
+ | "claude-desktop"
19
+ | "codex"
20
+ | "windsurf"
21
+ | "vscode";
22
+
23
+ // Tool definition from MCP server
24
+ export interface McpTool {
25
+ name: string;
26
+ title?: string;
27
+ description?: string;
28
+ inputSchema?: unknown; // JSON Schema
29
+ _meta?: Record<string, unknown>;
30
+ }
31
+
32
+ // Resource definition from MCP server
33
+ export interface McpResource {
34
+ uri: string;
35
+ name: string;
36
+ description?: string;
37
+ mimeType?: string;
38
+ _meta?: Record<string, unknown>;
39
+ }
40
+
41
+ export interface UiResourceMeta {
42
+ csp?: UiResourceCsp;
43
+ permissions?: UiResourcePermissions;
44
+ domain?: string;
45
+ prefersBorder?: boolean;
46
+ }
47
+
48
+ export interface UiResourceContent {
49
+ uri: string;
50
+ html: string;
51
+ mimeType?: string;
52
+ meta: UiResourceMeta;
53
+ }
54
+
55
+ export interface UiProxyRequestBody<TParams> {
56
+ token: string;
57
+ params: TParams;
58
+ }
59
+
60
+ export interface UiProxyResult<T = Record<string, unknown>> {
61
+ ok: boolean;
62
+ result?: T;
63
+ error?: string;
64
+ }
65
+
66
+ export interface UiResourceCsp {
67
+ connectDomains?: string[];
68
+ scriptDomains?: string[];
69
+ styleDomains?: string[];
70
+ fontDomains?: string[];
71
+ imgDomains?: string[];
72
+ mediaDomains?: string[];
73
+ frameDomains?: string[];
74
+ workerDomains?: string[];
75
+ baseUriDomains?: string[];
76
+ }
77
+
78
+ export interface UiResourcePermissions {
79
+ camera?: {};
80
+ microphone?: {};
81
+ geolocation?: {};
82
+ clipboardWrite?: {};
83
+ }
84
+
85
+ export interface UiToolInfo {
86
+ id?: string | number;
87
+ tool: {
88
+ name: string;
89
+ description?: string;
90
+ inputSchema?: unknown;
91
+ };
92
+ }
93
+
94
+ export interface UiHostContext {
95
+ toolInfo?: UiToolInfo;
96
+ theme?: "light" | "dark";
97
+ styles?: Record<string, unknown>;
98
+ displayMode?: UiDisplayMode;
99
+ availableDisplayModes?: UiDisplayMode[];
100
+ containerDimensions?: {
101
+ width?: number;
102
+ maxWidth?: number;
103
+ height?: number;
104
+ maxHeight?: number;
105
+ };
106
+ [key: string]: unknown;
107
+ }
108
+
109
+ export type UiDisplayMode = "inline" | "fullscreen" | "pip";
110
+
111
+ // Re-export stream types from the shared lightweight module.
112
+ // This allows the example package to import stream schemas without pulling the full types.ts dependency graph.
113
+ export {
114
+ UI_STREAM_HOST_CONTEXT_KEY,
115
+ UI_STREAM_REQUEST_META_KEY,
116
+ UI_STREAM_RESULT_PATCH_METHOD,
117
+ SERVER_STREAM_RESULT_PATCH_METHOD,
118
+ UI_STREAM_STRUCTURED_CONTENT_KEY,
119
+ uiStreamModeSchema,
120
+ visualizationStreamPhaseSchema,
121
+ visualizationStreamFrameTypeSchema,
122
+ visualizationStreamStatusSchema,
123
+ uiStreamHostContextSchema,
124
+ visualizationStreamEnvelopeSchema,
125
+ uiStreamCallToolResultSchema,
126
+ uiStreamResultPatchNotificationSchema,
127
+ serverStreamResultPatchNotificationSchema,
128
+ getUiStreamHostContext,
129
+ getVisualizationStreamEnvelope,
130
+ type UiStreamMode,
131
+ type VisualizationStreamPhase,
132
+ type VisualizationStreamFrameType,
133
+ type VisualizationStreamStatus,
134
+ type UiStreamHostContext,
135
+ type VisualizationStreamEnvelope,
136
+ type UiStreamCallToolResult,
137
+ type UiStreamResultPatchNotification,
138
+ type ServerStreamResultPatchNotification,
139
+ type UiStreamSummary,
140
+ } from "./ui-stream-types.ts";
141
+
142
+ export interface UiMessageParams {
143
+ role?: string;
144
+ content?: unknown[];
145
+ type?: "prompt" | "notify" | "intent" | "message";
146
+ message?: string;
147
+ prompt?: string;
148
+ intent?: string;
149
+ params?: Record<string, unknown>;
150
+ [key: string]: unknown;
151
+ }
152
+
153
+ /**
154
+ * Extract prompt text from either legacy MCP UI message shapes or native AppBridge user messages.
155
+ */
156
+ export function extractUiPromptText(params: UiMessageParams): string | undefined {
157
+ if (params.type === "prompt" || params.prompt) {
158
+ const prompt = params.prompt ?? String(params.message ?? "");
159
+ return prompt || undefined;
160
+ }
161
+
162
+ if (params.role === "user" && Array.isArray(params.content)) {
163
+ const text = params.content
164
+ .map((block) => (block && typeof block === "object" && "text" in block ? String((block as { text?: unknown }).text ?? "") : ""))
165
+ .filter(Boolean)
166
+ .join("\n\n");
167
+ return text || undefined;
168
+ }
169
+
170
+ return undefined;
171
+ }
172
+
173
+ /**
174
+ * Structured UI handoff recovered from a canonical prompt envelope.
175
+ */
176
+ export interface UiPromptHandoff {
177
+ intent: string;
178
+ params: Record<string, unknown>;
179
+ raw: string;
180
+ }
181
+
182
+ /**
183
+ * Parse a canonical named UI handoff encoded as `intent\n{json}`.
184
+ */
185
+ export function parseUiPromptHandoff(prompt: string): UiPromptHandoff | undefined {
186
+ const newlineIndex = prompt.indexOf("\n");
187
+ if (newlineIndex <= 0) {
188
+ return undefined;
189
+ }
190
+
191
+ const intent = prompt.slice(0, newlineIndex).trim();
192
+ const payloadText = prompt.slice(newlineIndex + 1).trim();
193
+ if (!intent || !payloadText) {
194
+ return undefined;
195
+ }
196
+
197
+ if (!/^[A-Za-z][A-Za-z0-9_-]*$/.test(intent)) {
198
+ return undefined;
199
+ }
200
+
201
+ try {
202
+ const parsed = JSON.parse(payloadText);
203
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
204
+ return undefined;
205
+ }
206
+ return {
207
+ intent,
208
+ params: parsed as Record<string, unknown>,
209
+ raw: prompt,
210
+ };
211
+ } catch {
212
+ return undefined;
213
+ }
214
+ }
215
+
216
+ /**
217
+ * Accumulated messages from a UI session.
218
+ * Collected during the session and available when it ends.
219
+ */
220
+ export interface UiSessionMessages {
221
+ prompts: string[];
222
+ notifications: string[];
223
+ intents: Array<{ intent: string; params?: Record<string, unknown> }>;
224
+ }
225
+
226
+ export interface UiModelContextParams {
227
+ content?: unknown[];
228
+ structuredContent?: Record<string, unknown>;
229
+ [key: string]: unknown;
230
+ }
231
+
232
+ export interface UiOpenLinkResult {
233
+ isError?: boolean;
234
+ [key: string]: unknown;
235
+ }
236
+
237
+ export interface UiDisplayModeRequest {
238
+ mode?: UiDisplayMode;
239
+ }
240
+
241
+ export interface UiDisplayModeResult {
242
+ mode: UiDisplayMode;
243
+ [key: string]: unknown;
244
+ }
245
+
246
+ // Content types from MCP
247
+ export interface McpContent {
248
+ type: "text" | "image" | "audio" | "resource" | "resource_link";
249
+ text?: string;
250
+ data?: string;
251
+ mimeType?: string;
252
+ resource?: {
253
+ uri: string;
254
+ text?: string;
255
+ blob?: string;
256
+ };
257
+ uri?: string;
258
+ name?: string;
259
+ description?: string;
260
+ }
261
+
262
+ // Pi content block type
263
+ export type ContentBlock = TextContent | ImageContent;
264
+
265
+ // OAuth configuration (SDK handles auto-discovery and dynamic registration)
266
+ export interface OAuthConfig {
267
+ /** OAuth grant type (defaults to authorization_code) */
268
+ grantType?: "authorization_code" | "client_credentials";
269
+ /** Pre-registered client ID (optional, dynamic registration used if not provided) */
270
+ clientId?: string;
271
+ /** Client secret for confidential clients */
272
+ clientSecret?: string;
273
+ /** Requested OAuth scopes */
274
+ scope?: string;
275
+ /** Exact authorization-code redirect URI for pre-registered clients */
276
+ redirectUri?: string;
277
+ /** Client display name for dynamic registration */
278
+ clientName?: string;
279
+ /** Client homepage URI for dynamic registration */
280
+ clientUri?: string;
281
+ }
282
+
283
+ // Server configuration
284
+ export interface ServerEntry {
285
+ command?: string;
286
+ args?: string[];
287
+ env?: Record<string, string>;
288
+ cwd?: string;
289
+ // HTTP fields
290
+ url?: string;
291
+ headers?: Record<string, string>;
292
+ /**
293
+ * Authentication type:
294
+ * - 'oauth' - Use OAuth 2.1 (auto-discovers endpoints, supports dynamic client registration)
295
+ * - 'bearer' - Use static Bearer token
296
+ * - false - Disable authentication
297
+ * If not specified and url is present, OAuth will be auto-detected
298
+ */
299
+ auth?: "oauth" | "bearer" | false;
300
+ bearerToken?: string;
301
+ bearerTokenEnv?: string;
302
+ /**
303
+ * OAuth configuration (optional).
304
+ * If not provided, the SDK will attempt dynamic client registration.
305
+ * Set to false to explicitly disable OAuth for this server.
306
+ */
307
+ oauth?: OAuthConfig | false;
308
+ lifecycle?: "keep-alive" | "lazy" | "eager";
309
+ idleTimeout?: number; // minutes, overrides global setting
310
+ // Resource handling
311
+ exposeResources?: boolean;
312
+ // Direct tool registration
313
+ directTools?: boolean | string[];
314
+ // Exclude specific MCP tools/resources by original or prefixed name
315
+ excludeTools?: string[];
316
+ // Debug
317
+ debug?: boolean; // Show server stderr (default: false)
318
+ }
319
+
320
+ // Settings
321
+ export interface McpSettings {
322
+ toolPrefix?: "server" | "none" | "short";
323
+ idleTimeout?: number; // minutes, default 10, 0 to disable
324
+ directTools?: boolean;
325
+ disableProxyTool?: boolean;
326
+ autoAuth?: boolean;
327
+ sampling?: boolean;
328
+ samplingAutoApprove?: boolean;
329
+ elicitation?: boolean;
330
+ /**
331
+ * Message returned in tool results when a server needs (re-)authentication.
332
+ * "${server}" is substituted with the server name. Defaults to a TUI
333
+ * instruction when unset.
334
+ */
335
+ authRequiredMessage?: string;
336
+ }
337
+
338
+ // Root config
339
+ export interface McpConfig {
340
+ mcpServers: Record<string, ServerEntry>;
341
+ imports?: ImportKind[];
342
+ settings?: McpSettings;
343
+ }
344
+
345
+ // Alias for clarity
346
+ export type ServerDefinition = ServerEntry;
347
+
348
+ export interface ToolMetadata {
349
+ name: string; // Prefixed tool name (e.g., "xcodebuild_list_sims")
350
+ originalName: string; // Original MCP tool name (e.g., "list_sims")
351
+ description: string;
352
+ resourceUri?: string; // For resource tools: the URI to read
353
+ uiResourceUri?: string; // For app-enabled tools: the UI resource URI
354
+ inputSchema?: unknown; // JSON Schema for parameters (stored for describe/errors)
355
+ uiStreamMode?: UiStreamMode;
356
+ }
357
+
358
+ export interface DirectToolSpec {
359
+ serverName: string;
360
+ originalName: string;
361
+ prefixedName: string;
362
+ description: string;
363
+ inputSchema?: unknown;
364
+ resourceUri?: string;
365
+ uiResourceUri?: string;
366
+ uiStreamMode?: UiStreamMode;
367
+ }
368
+
369
+ export interface ServerProvenance {
370
+ path: string;
371
+ kind: "user" | "project" | "import";
372
+ importKind?: string;
373
+ }
374
+
375
+ export interface McpAuthResult {
376
+ ok: boolean;
377
+ message?: string;
378
+ }
379
+
380
+ export interface McpPanelCallbacks {
381
+ reconnect: (serverName: string) => Promise<boolean>;
382
+ canAuthenticate: (serverName: string) => boolean;
383
+ authenticate: (serverName: string) => Promise<McpAuthResult>;
384
+ getConnectionStatus: (serverName: string) => "connected" | "idle" | "failed" | "needs-auth";
385
+ refreshCacheAfterReconnect: (serverName: string) => import("./metadata-cache.ts").ServerCacheEntry | null;
386
+ }
387
+
388
+ export interface McpPanelResult {
389
+ changes: Map<string, true | string[] | false>;
390
+ cancelled: boolean;
391
+ }
392
+
393
+ /**
394
+ * Get server prefix based on tool prefix mode.
395
+ */
396
+ export function getServerPrefix(
397
+ serverName: string,
398
+ mode: "server" | "none" | "short"
399
+ ): string {
400
+ if (mode === "none") return "";
401
+ if (mode === "short") {
402
+ let short = serverName.replace(/-?mcp$/i, "").replace(/-/g, "_");
403
+ if (!short) short = "mcp";
404
+ return short;
405
+ }
406
+ return serverName.replace(/-/g, "_");
407
+ }
408
+
409
+ /**
410
+ * Format a tool name with server prefix.
411
+ */
412
+ export function formatToolName(
413
+ toolName: string,
414
+ serverName: string,
415
+ prefix: "server" | "none" | "short"
416
+ ): string {
417
+ const p = getServerPrefix(serverName, prefix);
418
+ return p ? `${p}_${toolName}` : toolName;
419
+ }
420
+
421
+ function normalizeToolName(value: string): string {
422
+ return value.replace(/-/g, "_");
423
+ }
424
+
425
+ export function isToolExcluded(
426
+ toolName: string,
427
+ serverName: string,
428
+ prefix: "server" | "none" | "short",
429
+ excludeTools?: unknown
430
+ ): boolean {
431
+ if (!Array.isArray(excludeTools) || excludeTools.length === 0) return false;
432
+
433
+ const candidates = new Set<string>([
434
+ normalizeToolName(toolName),
435
+ normalizeToolName(formatToolName(toolName, serverName, prefix)),
436
+ normalizeToolName(formatToolName(toolName, serverName, "server")),
437
+ normalizeToolName(formatToolName(toolName, serverName, "short")),
438
+ ]);
439
+
440
+ for (const excluded of excludeTools) {
441
+ if (typeof excluded !== "string") continue;
442
+ if (candidates.has(normalizeToolName(excluded))) {
443
+ return true;
444
+ }
445
+ }
446
+
447
+ return false;
448
+ }
@@ -0,0 +1,146 @@
1
+ import { RESOURCE_MIME_TYPE } from "@modelcontextprotocol/ext-apps/app-bridge";
2
+ import { UrlElicitationRequiredError, type ReadResourceResult } from "@modelcontextprotocol/sdk/types.js";
3
+ import { ResourceFetchError, ResourceParseError } from "./errors.ts";
4
+ import { logger } from "./logger.ts";
5
+ import type { McpServerManager } from "./server-manager.ts";
6
+ import type { UiResourceContent, UiResourceMeta } from "./types.ts";
7
+
8
+ interface ResourceContentRecord {
9
+ uri?: string;
10
+ mimeType?: string;
11
+ text?: string;
12
+ blob?: string;
13
+ _meta?: Record<string, unknown>;
14
+ }
15
+
16
+ export class UiResourceHandler {
17
+ private log = logger.child({ component: "UiResourceHandler" });
18
+
19
+ constructor(private manager: McpServerManager) {}
20
+
21
+ async readUiResource(serverName: string, uri: string): Promise<UiResourceContent> {
22
+ const log = this.log.child({ server: serverName, uri });
23
+
24
+ if (!uri.startsWith("ui://")) {
25
+ throw new ResourceParseError(uri, "URI must start with ui://", { server: serverName });
26
+ }
27
+
28
+ log.debug("Fetching UI resource");
29
+
30
+ let result: ReadResourceResult;
31
+ try {
32
+ result = await this.manager.readResource(serverName, uri);
33
+ } catch (error) {
34
+ if (error instanceof UrlElicitationRequiredError) throw error;
35
+ const message = error instanceof Error ? error.message : String(error);
36
+ log.error("Failed to read resource", error instanceof Error ? error : undefined);
37
+ throw new ResourceFetchError(uri, message, {
38
+ server: serverName,
39
+ cause: error instanceof Error ? error : undefined,
40
+ });
41
+ }
42
+
43
+ const content = selectContent(result, uri);
44
+ const mimeType = content.mimeType;
45
+
46
+ if (mimeType && !isHtmlMimeType(mimeType)) {
47
+ log.warn("Unsupported MIME type", { mimeType });
48
+ throw new ResourceParseError(
49
+ uri,
50
+ `unsupported MIME type "${mimeType}" (expected text/html or ${RESOURCE_MIME_TYPE})`,
51
+ { server: serverName, mimeType }
52
+ );
53
+ }
54
+
55
+ const html = toHtml(content);
56
+ if (!html.trim()) {
57
+ log.warn("Resource content is empty");
58
+ throw new ResourceParseError(uri, "content is empty", { server: serverName });
59
+ }
60
+
61
+ const contentMeta = extractUiMeta(content._meta);
62
+ const listMeta = extractUiMeta(this.getListResourceMeta(serverName, uri));
63
+
64
+ log.debug("Resource loaded successfully", {
65
+ contentLength: html.length,
66
+ hasCsp: !!contentMeta.csp || !!listMeta.csp,
67
+ });
68
+
69
+ return {
70
+ uri: content.uri ?? uri,
71
+ html,
72
+ mimeType: mimeType ?? RESOURCE_MIME_TYPE,
73
+ meta: {
74
+ csp: contentMeta.csp ?? listMeta.csp,
75
+ permissions: contentMeta.permissions ?? listMeta.permissions,
76
+ domain: contentMeta.domain ?? listMeta.domain,
77
+ prefersBorder: contentMeta.prefersBorder ?? listMeta.prefersBorder,
78
+ },
79
+ };
80
+ }
81
+
82
+ private getListResourceMeta(serverName: string, uri: string): Record<string, unknown> | undefined {
83
+ const connection = this.manager.getConnection(serverName);
84
+ if (!connection?.resources?.length) return undefined;
85
+ const resource = connection.resources.find((entry) => entry.uri === uri);
86
+ if (!resource || !resource._meta || typeof resource._meta !== "object") return undefined;
87
+ return resource._meta;
88
+ }
89
+ }
90
+
91
+ function selectContent(result: ReadResourceResult, preferredUri: string): ResourceContentRecord {
92
+ const contents = (result.contents ?? []) as ResourceContentRecord[];
93
+ if (contents.length === 0) {
94
+ throw new Error(`No contents returned for UI resource: ${preferredUri}`);
95
+ }
96
+
97
+ const byUri = contents.find((content) => content.uri === preferredUri);
98
+ if (byUri) return byUri;
99
+
100
+ const byHtmlMime = contents.find(
101
+ (content) => content.mimeType && isHtmlMimeType(content.mimeType)
102
+ );
103
+ if (byHtmlMime) return byHtmlMime;
104
+
105
+ return contents[0];
106
+ }
107
+
108
+ function isHtmlMimeType(mimeType: string): boolean {
109
+ const normalized = mimeType.toLowerCase();
110
+ return normalized.startsWith("text/html") || normalized === RESOURCE_MIME_TYPE.toLowerCase();
111
+ }
112
+
113
+ function toHtml(content: ResourceContentRecord): string {
114
+ if (typeof content.text === "string") {
115
+ return content.text;
116
+ }
117
+
118
+ if (typeof content.blob === "string") {
119
+ return Buffer.from(content.blob, "base64").toString("utf-8");
120
+ }
121
+
122
+ throw new Error(`UI resource ${content.uri ?? "(unknown)"} did not include text or blob content`);
123
+ }
124
+
125
+ function extractUiMeta(meta: Record<string, unknown> | undefined): UiResourceMeta {
126
+ if (!meta || typeof meta !== "object") return {};
127
+ const ui = meta.ui as Record<string, unknown> | undefined;
128
+ if (!ui || typeof ui !== "object") return {};
129
+
130
+ const out: UiResourceMeta = {};
131
+
132
+ if (ui.csp && typeof ui.csp === "object") {
133
+ out.csp = ui.csp as UiResourceMeta["csp"];
134
+ }
135
+ if (ui.permissions && typeof ui.permissions === "object") {
136
+ out.permissions = ui.permissions as UiResourceMeta["permissions"];
137
+ }
138
+ if (typeof ui.domain === "string") {
139
+ out.domain = ui.domain;
140
+ }
141
+ if (typeof ui.prefersBorder === "boolean") {
142
+ out.prefersBorder = ui.prefersBorder;
143
+ }
144
+
145
+ return out;
146
+ }