@docyrus/docyrus 0.0.19 → 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 (111) hide show
  1. package/agent-loader.js +37 -3
  2. package/agent-loader.js.map +2 -2
  3. package/main.js +498 -93
  4. package/main.js.map +4 -4
  5. package/package.json +14 -4
  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/prompts/coder-system.md +106 -0
  64. package/resources/pi-agent/skills/changelog-generator/SKILL.md +425 -0
  65. package/resources/pi-agent/skills/docyrus-chrome-devtools-cli/SKILL.md +80 -0
  66. package/resources/pi-agent/skills/docyrus-platform/SKILL.md +71 -0
  67. package/resources/pi-agent/skills/docyrus-platform/references/ai-capabilities.md +43 -0
  68. package/resources/pi-agent/skills/docyrus-platform/references/auth-and-multi-tenancy.md +35 -0
  69. package/resources/pi-agent/skills/docyrus-platform/references/automation-and-workflows.md +30 -0
  70. package/resources/pi-agent/skills/docyrus-platform/references/core-building-blocks.md +53 -0
  71. package/resources/pi-agent/skills/{docyrus-api-dev → docyrus-platform}/references/data-source-query-guide.md +32 -28
  72. package/resources/pi-agent/skills/docyrus-platform/references/developer-tools.md +28 -0
  73. package/resources/pi-agent/skills/docyrus-platform/references/docyrus-cli-usage.md +554 -0
  74. package/resources/pi-agent/skills/{docyrus-api-dev → docyrus-platform}/references/formula-design-guide-llm.md +15 -23
  75. package/resources/pi-agent/skills/docyrus-platform/references/integrations-and-events.md +60 -0
  76. package/resources/pi-agent/skills/docyrus-platform/references/platform-services.md +58 -0
  77. package/resources/pi-agent/skills/docyrus-platform/references/querying-and-data-operations.md +27 -0
  78. package/resources/pi-agent/prompts/coder-append-system.md +0 -19
  79. package/resources/pi-agent/skills/docyrus-ai/SKILL.md +0 -28
  80. package/resources/pi-agent/skills/docyrus-api-dev/SKILL.md +0 -161
  81. package/resources/pi-agent/skills/docyrus-api-dev/references/api-client.md +0 -349
  82. package/resources/pi-agent/skills/docyrus-api-dev/references/authentication.md +0 -238
  83. package/resources/pi-agent/skills/docyrus-api-dev/references/query-and-formulas.md +0 -592
  84. package/resources/pi-agent/skills/docyrus-api-doctor/SKILL.md +0 -70
  85. package/resources/pi-agent/skills/docyrus-api-doctor/references/checklist-details.md +0 -588
  86. package/resources/pi-agent/skills/docyrus-app-dev/SKILL.md +0 -159
  87. package/resources/pi-agent/skills/docyrus-app-dev/references/api-client-and-auth.md +0 -275
  88. package/resources/pi-agent/skills/docyrus-app-dev/references/collections-and-patterns.md +0 -352
  89. package/resources/pi-agent/skills/docyrus-app-dev/references/data-source-query-guide.md +0 -2059
  90. package/resources/pi-agent/skills/docyrus-app-dev/references/formula-design-guide-llm.md +0 -320
  91. package/resources/pi-agent/skills/docyrus-app-dev/references/query-guide.md +0 -525
  92. package/resources/pi-agent/skills/docyrus-app-ui-design/SKILL.md +0 -466
  93. package/resources/pi-agent/skills/docyrus-app-ui-design/references/component-selection-guide.md +0 -602
  94. package/resources/pi-agent/skills/docyrus-app-ui-design/references/icon-usage-guide.md +0 -463
  95. package/resources/pi-agent/skills/docyrus-app-ui-design/references/preferred-components-catalog.md +0 -242
  96. package/resources/pi-agent/skills/docyrus-apps/SKILL.md +0 -54
  97. package/resources/pi-agent/skills/docyrus-architect/SKILL.md +0 -174
  98. package/resources/pi-agent/skills/docyrus-architect/references/custom-query-guide.md +0 -410
  99. package/resources/pi-agent/skills/docyrus-architect/references/data-source-query-guide.md +0 -2059
  100. package/resources/pi-agent/skills/docyrus-architect/references/formula-design-guide-llm.md +0 -320
  101. package/resources/pi-agent/skills/docyrus-architect/references/formula-reference.md +0 -145
  102. package/resources/pi-agent/skills/docyrus-auth/SKILL.md +0 -100
  103. package/resources/pi-agent/skills/docyrus-cli-app/SKILL.md +0 -279
  104. package/resources/pi-agent/skills/docyrus-cli-app/references/cli-manifest.md +0 -532
  105. package/resources/pi-agent/skills/docyrus-cli-app/references/list-query-examples.md +0 -248
  106. package/resources/pi-agent/skills/docyrus-curl/SKILL.md +0 -32
  107. package/resources/pi-agent/skills/docyrus-discover/SKILL.md +0 -63
  108. package/resources/pi-agent/skills/docyrus-ds/SKILL.md +0 -95
  109. package/resources/pi-agent/skills/docyrus-env/SKILL.md +0 -21
  110. package/resources/pi-agent/skills/docyrus-studio/SKILL.md +0 -369
  111. package/resources/pi-agent/skills/docyrus-tui/SKILL.md +0 -15
@@ -0,0 +1,367 @@
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 "@mariozechner/pi-ai";
6
+
7
+ // Transport type (stdio + HTTP)
8
+ export type Transport =
9
+ | StdioClientTransport
10
+ | SSEClientTransport
11
+ | StreamableHTTPClientTransport;
12
+
13
+ // Import sources for config
14
+ export type ImportKind =
15
+ | "cursor"
16
+ | "claude-code"
17
+ | "claude-desktop"
18
+ | "codex"
19
+ | "windsurf"
20
+ | "vscode";
21
+
22
+ // Tool definition from MCP server
23
+ export interface McpTool {
24
+ name: string;
25
+ title?: string;
26
+ description?: string;
27
+ inputSchema?: unknown; // JSON Schema
28
+ _meta?: Record<string, unknown>;
29
+ }
30
+
31
+ // Resource definition from MCP server
32
+ export interface McpResource {
33
+ uri: string;
34
+ name: string;
35
+ description?: string;
36
+ mimeType?: string;
37
+ _meta?: Record<string, unknown>;
38
+ }
39
+
40
+ export interface UiResourceMeta {
41
+ csp?: UiResourceCsp;
42
+ permissions?: UiResourcePermissions;
43
+ domain?: string;
44
+ prefersBorder?: boolean;
45
+ }
46
+
47
+ export interface UiResourceContent {
48
+ uri: string;
49
+ html: string;
50
+ mimeType?: string;
51
+ meta: UiResourceMeta;
52
+ }
53
+
54
+ export interface UiProxyRequestBody<TParams> {
55
+ token: string;
56
+ params: TParams;
57
+ }
58
+
59
+ export interface UiProxyResult<T = Record<string, unknown>> {
60
+ ok: boolean;
61
+ result?: T;
62
+ error?: string;
63
+ }
64
+
65
+ export interface UiResourceCsp {
66
+ connectDomains?: string[];
67
+ scriptDomains?: string[];
68
+ styleDomains?: string[];
69
+ fontDomains?: string[];
70
+ imgDomains?: string[];
71
+ mediaDomains?: string[];
72
+ frameDomains?: string[];
73
+ workerDomains?: string[];
74
+ baseUriDomains?: string[];
75
+ }
76
+
77
+ export interface UiResourcePermissions {
78
+ camera?: {};
79
+ microphone?: {};
80
+ geolocation?: {};
81
+ clipboardWrite?: {};
82
+ }
83
+
84
+ export interface UiToolInfo {
85
+ id?: string | number;
86
+ tool: {
87
+ name: string;
88
+ description?: string;
89
+ inputSchema?: unknown;
90
+ };
91
+ }
92
+
93
+ export interface UiHostContext {
94
+ toolInfo?: UiToolInfo;
95
+ theme?: "light" | "dark";
96
+ styles?: Record<string, unknown>;
97
+ displayMode?: UiDisplayMode;
98
+ availableDisplayModes?: UiDisplayMode[];
99
+ containerDimensions?: {
100
+ width?: number;
101
+ maxWidth?: number;
102
+ height?: number;
103
+ maxHeight?: number;
104
+ };
105
+ [key: string]: unknown;
106
+ }
107
+
108
+ export type UiDisplayMode = "inline" | "fullscreen" | "pip";
109
+
110
+ // Re-export stream types from the shared lightweight module.
111
+ // This allows the example package to import stream schemas without pulling the full types.ts dependency graph.
112
+ export {
113
+ UI_STREAM_HOST_CONTEXT_KEY,
114
+ UI_STREAM_REQUEST_META_KEY,
115
+ UI_STREAM_RESULT_PATCH_METHOD,
116
+ SERVER_STREAM_RESULT_PATCH_METHOD,
117
+ UI_STREAM_STRUCTURED_CONTENT_KEY,
118
+ uiStreamModeSchema,
119
+ visualizationStreamPhaseSchema,
120
+ visualizationStreamFrameTypeSchema,
121
+ visualizationStreamStatusSchema,
122
+ uiStreamHostContextSchema,
123
+ visualizationStreamEnvelopeSchema,
124
+ uiStreamCallToolResultSchema,
125
+ uiStreamResultPatchNotificationSchema,
126
+ serverStreamResultPatchNotificationSchema,
127
+ getUiStreamHostContext,
128
+ getVisualizationStreamEnvelope,
129
+ type UiStreamMode,
130
+ type VisualizationStreamPhase,
131
+ type VisualizationStreamFrameType,
132
+ type VisualizationStreamStatus,
133
+ type UiStreamHostContext,
134
+ type VisualizationStreamEnvelope,
135
+ type UiStreamCallToolResult,
136
+ type UiStreamResultPatchNotification,
137
+ type ServerStreamResultPatchNotification,
138
+ type UiStreamSummary,
139
+ } from "./ui-stream-types.js";
140
+
141
+ export interface UiMessageParams {
142
+ role?: string;
143
+ content?: unknown[];
144
+ type?: "prompt" | "notify" | "intent" | "message";
145
+ message?: string;
146
+ prompt?: string;
147
+ intent?: string;
148
+ params?: Record<string, unknown>;
149
+ [key: string]: unknown;
150
+ }
151
+
152
+ /**
153
+ * Extract prompt text from either legacy MCP UI message shapes or native AppBridge user messages.
154
+ */
155
+ export function extractUiPromptText(params: UiMessageParams): string | undefined {
156
+ if (params.type === "prompt" || params.prompt) {
157
+ const prompt = params.prompt ?? String(params.message ?? "");
158
+ return prompt || undefined;
159
+ }
160
+
161
+ if (params.role === "user" && Array.isArray(params.content)) {
162
+ const text = params.content
163
+ .map((block) => (block && typeof block === "object" && "text" in block ? String((block as { text?: unknown }).text ?? "") : ""))
164
+ .filter(Boolean)
165
+ .join("\n\n");
166
+ return text || undefined;
167
+ }
168
+
169
+ return undefined;
170
+ }
171
+
172
+ /**
173
+ * Structured UI handoff recovered from a canonical prompt envelope.
174
+ */
175
+ export interface UiPromptHandoff {
176
+ intent: string;
177
+ params: Record<string, unknown>;
178
+ raw: string;
179
+ }
180
+
181
+ /**
182
+ * Parse a canonical named UI handoff encoded as `intent\n{json}`.
183
+ */
184
+ export function parseUiPromptHandoff(prompt: string): UiPromptHandoff | undefined {
185
+ const newlineIndex = prompt.indexOf("\n");
186
+ if (newlineIndex <= 0) {
187
+ return undefined;
188
+ }
189
+
190
+ const intent = prompt.slice(0, newlineIndex).trim();
191
+ const payloadText = prompt.slice(newlineIndex + 1).trim();
192
+ if (!intent || !payloadText) {
193
+ return undefined;
194
+ }
195
+
196
+ if (!/^[A-Za-z][A-Za-z0-9_-]*$/.test(intent)) {
197
+ return undefined;
198
+ }
199
+
200
+ try {
201
+ const parsed = JSON.parse(payloadText);
202
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
203
+ return undefined;
204
+ }
205
+ return {
206
+ intent,
207
+ params: parsed as Record<string, unknown>,
208
+ raw: prompt,
209
+ };
210
+ } catch {
211
+ return undefined;
212
+ }
213
+ }
214
+
215
+ /**
216
+ * Accumulated messages from a UI session.
217
+ * Collected during the session and available when it ends.
218
+ */
219
+ export interface UiSessionMessages {
220
+ prompts: string[];
221
+ notifications: string[];
222
+ intents: Array<{ intent: string; params?: Record<string, unknown> }>;
223
+ }
224
+
225
+ export interface UiModelContextParams {
226
+ content?: unknown[];
227
+ structuredContent?: Record<string, unknown>;
228
+ [key: string]: unknown;
229
+ }
230
+
231
+ export interface UiOpenLinkResult {
232
+ isError?: boolean;
233
+ [key: string]: unknown;
234
+ }
235
+
236
+ export interface UiDisplayModeRequest {
237
+ mode?: UiDisplayMode;
238
+ }
239
+
240
+ export interface UiDisplayModeResult {
241
+ mode: UiDisplayMode;
242
+ [key: string]: unknown;
243
+ }
244
+
245
+ // Content types from MCP
246
+ export interface McpContent {
247
+ type: "text" | "image" | "audio" | "resource" | "resource_link";
248
+ text?: string;
249
+ data?: string;
250
+ mimeType?: string;
251
+ resource?: {
252
+ uri: string;
253
+ text?: string;
254
+ blob?: string;
255
+ };
256
+ uri?: string;
257
+ name?: string;
258
+ description?: string;
259
+ }
260
+
261
+ // Pi content block type
262
+ export type ContentBlock = TextContent | ImageContent;
263
+
264
+ // Server configuration
265
+ export interface ServerEntry {
266
+ command?: string;
267
+ args?: string[];
268
+ env?: Record<string, string>;
269
+ cwd?: string;
270
+ // HTTP fields
271
+ url?: string;
272
+ headers?: Record<string, string>;
273
+ auth?: "oauth" | "bearer";
274
+ bearerToken?: string;
275
+ bearerTokenEnv?: string;
276
+ lifecycle?: "keep-alive" | "lazy" | "eager";
277
+ idleTimeout?: number; // minutes, overrides global setting
278
+ // Resource handling
279
+ exposeResources?: boolean;
280
+ // Direct tool registration
281
+ directTools?: boolean | string[];
282
+ // Debug
283
+ debug?: boolean; // Show server stderr (default: false)
284
+ }
285
+
286
+ // Settings
287
+ export interface McpSettings {
288
+ toolPrefix?: "server" | "none" | "short";
289
+ idleTimeout?: number; // minutes, default 10, 0 to disable
290
+ directTools?: boolean;
291
+ }
292
+
293
+ // Root config
294
+ export interface McpConfig {
295
+ mcpServers: Record<string, ServerEntry>;
296
+ imports?: ImportKind[];
297
+ settings?: McpSettings;
298
+ }
299
+
300
+ // Alias for clarity
301
+ export type ServerDefinition = ServerEntry;
302
+
303
+ export interface ToolMetadata {
304
+ name: string; // Prefixed tool name (e.g., "xcodebuild_list_sims")
305
+ originalName: string; // Original MCP tool name (e.g., "list_sims")
306
+ description: string;
307
+ resourceUri?: string; // For resource tools: the URI to read
308
+ uiResourceUri?: string; // For app-enabled tools: the UI resource URI
309
+ inputSchema?: unknown; // JSON Schema for parameters (stored for describe/errors)
310
+ uiStreamMode?: UiStreamMode;
311
+ }
312
+
313
+ export interface DirectToolSpec {
314
+ serverName: string;
315
+ originalName: string;
316
+ prefixedName: string;
317
+ description: string;
318
+ inputSchema?: unknown;
319
+ resourceUri?: string;
320
+ uiResourceUri?: string;
321
+ uiStreamMode?: UiStreamMode;
322
+ }
323
+
324
+ export interface ServerProvenance {
325
+ path: string;
326
+ kind: "user" | "project" | "import";
327
+ importKind?: string;
328
+ }
329
+
330
+ export interface McpPanelCallbacks {
331
+ reconnect: (serverName: string) => Promise<boolean>;
332
+ getConnectionStatus: (serverName: string) => "connected" | "idle" | "failed" | "needs-auth";
333
+ refreshCacheAfterReconnect: (serverName: string) => import("./metadata-cache.js").ServerCacheEntry | null;
334
+ }
335
+
336
+ export interface McpPanelResult {
337
+ changes: Map<string, true | string[] | false>;
338
+ cancelled: boolean;
339
+ }
340
+
341
+ /**
342
+ * Get server prefix based on tool prefix mode.
343
+ */
344
+ export function getServerPrefix(
345
+ serverName: string,
346
+ mode: "server" | "none" | "short"
347
+ ): string {
348
+ if (mode === "none") return "";
349
+ if (mode === "short") {
350
+ let short = serverName.replace(/-?mcp$/i, "").replace(/-/g, "_");
351
+ if (!short) short = "mcp";
352
+ return short;
353
+ }
354
+ return serverName.replace(/-/g, "_");
355
+ }
356
+
357
+ /**
358
+ * Format a tool name with server prefix.
359
+ */
360
+ export function formatToolName(
361
+ toolName: string,
362
+ serverName: string,
363
+ prefix: "server" | "none" | "short"
364
+ ): string {
365
+ const p = getServerPrefix(serverName, prefix);
366
+ return p ? `${p}_${toolName}` : toolName;
367
+ }
@@ -0,0 +1,145 @@
1
+ import { RESOURCE_MIME_TYPE } from "@modelcontextprotocol/ext-apps/app-bridge";
2
+ import type { ReadResourceResult } from "@modelcontextprotocol/sdk/types.js";
3
+ import { ResourceFetchError, ResourceParseError } from "./errors.js";
4
+ import { logger } from "./logger.js";
5
+ import type { McpServerManager } from "./server-manager.js";
6
+ import type { UiResourceContent, UiResourceMeta } from "./types.js";
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
+ const message = error instanceof Error ? error.message : String(error);
35
+ log.error("Failed to read resource", error instanceof Error ? error : undefined);
36
+ throw new ResourceFetchError(uri, message, {
37
+ server: serverName,
38
+ cause: error instanceof Error ? error : undefined,
39
+ });
40
+ }
41
+
42
+ const content = selectContent(result, uri);
43
+ const mimeType = content.mimeType;
44
+
45
+ if (mimeType && !isHtmlMimeType(mimeType)) {
46
+ log.warn("Unsupported MIME type", { mimeType });
47
+ throw new ResourceParseError(
48
+ uri,
49
+ `unsupported MIME type "${mimeType}" (expected text/html or ${RESOURCE_MIME_TYPE})`,
50
+ { server: serverName, mimeType }
51
+ );
52
+ }
53
+
54
+ const html = toHtml(content);
55
+ if (!html.trim()) {
56
+ log.warn("Resource content is empty");
57
+ throw new ResourceParseError(uri, "content is empty", { server: serverName });
58
+ }
59
+
60
+ const contentMeta = extractUiMeta(content._meta);
61
+ const listMeta = extractUiMeta(this.getListResourceMeta(serverName, uri));
62
+
63
+ log.debug("Resource loaded successfully", {
64
+ contentLength: html.length,
65
+ hasCsp: !!contentMeta.csp || !!listMeta.csp,
66
+ });
67
+
68
+ return {
69
+ uri: content.uri ?? uri,
70
+ html,
71
+ mimeType: mimeType ?? RESOURCE_MIME_TYPE,
72
+ meta: {
73
+ csp: contentMeta.csp ?? listMeta.csp,
74
+ permissions: contentMeta.permissions ?? listMeta.permissions,
75
+ domain: contentMeta.domain ?? listMeta.domain,
76
+ prefersBorder: contentMeta.prefersBorder ?? listMeta.prefersBorder,
77
+ },
78
+ };
79
+ }
80
+
81
+ private getListResourceMeta(serverName: string, uri: string): Record<string, unknown> | undefined {
82
+ const connection = this.manager.getConnection(serverName);
83
+ if (!connection?.resources?.length) return undefined;
84
+ const resource = connection.resources.find((entry) => entry.uri === uri);
85
+ if (!resource || !resource._meta || typeof resource._meta !== "object") return undefined;
86
+ return resource._meta;
87
+ }
88
+ }
89
+
90
+ function selectContent(result: ReadResourceResult, preferredUri: string): ResourceContentRecord {
91
+ const contents = (result.contents ?? []) as ResourceContentRecord[];
92
+ if (contents.length === 0) {
93
+ throw new Error(`No contents returned for UI resource: ${preferredUri}`);
94
+ }
95
+
96
+ const byUri = contents.find((content) => content.uri === preferredUri);
97
+ if (byUri) return byUri;
98
+
99
+ const byHtmlMime = contents.find(
100
+ (content) => content.mimeType && isHtmlMimeType(content.mimeType)
101
+ );
102
+ if (byHtmlMime) return byHtmlMime;
103
+
104
+ return contents[0];
105
+ }
106
+
107
+ function isHtmlMimeType(mimeType: string): boolean {
108
+ const normalized = mimeType.toLowerCase();
109
+ return normalized.startsWith("text/html") || normalized === RESOURCE_MIME_TYPE.toLowerCase();
110
+ }
111
+
112
+ function toHtml(content: ResourceContentRecord): string {
113
+ if (typeof content.text === "string") {
114
+ return content.text;
115
+ }
116
+
117
+ if (typeof content.blob === "string") {
118
+ return Buffer.from(content.blob, "base64").toString("utf-8");
119
+ }
120
+
121
+ throw new Error(`UI resource ${content.uri ?? "(unknown)"} did not include text or blob content`);
122
+ }
123
+
124
+ function extractUiMeta(meta: Record<string, unknown> | undefined): UiResourceMeta {
125
+ if (!meta || typeof meta !== "object") return {};
126
+ const ui = meta.ui as Record<string, unknown> | undefined;
127
+ if (!ui || typeof ui !== "object") return {};
128
+
129
+ const out: UiResourceMeta = {};
130
+
131
+ if (ui.csp && typeof ui.csp === "object") {
132
+ out.csp = ui.csp as UiResourceMeta["csp"];
133
+ }
134
+ if (ui.permissions && typeof ui.permissions === "object") {
135
+ out.permissions = ui.permissions as UiResourceMeta["permissions"];
136
+ }
137
+ if (typeof ui.domain === "string") {
138
+ out.domain = ui.domain;
139
+ }
140
+ if (typeof ui.prefersBorder === "boolean") {
141
+ out.prefersBorder = ui.prefersBorder;
142
+ }
143
+
144
+ return out;
145
+ }