@fitchmultz/pi-mcp-adapter 8.0.6 → 8.0.8

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 (87) hide show
  1. package/CHANGELOG.md +142 -8
  2. package/OAUTH.md +13 -16
  3. package/README.md +8 -8
  4. package/cli.js +67 -28
  5. package/dist/abort.js +18 -9
  6. package/dist/client-operations.js +4 -2
  7. package/dist/commands.js +77 -43
  8. package/dist/config.js +194 -105
  9. package/dist/connection-runtime.d.ts +2 -2
  10. package/dist/connection-runtime.js +29 -18
  11. package/dist/consent-manager.d.ts +2 -2
  12. package/dist/consent-manager.js +5 -2
  13. package/dist/direct-tools.d.ts +1 -1
  14. package/dist/direct-tools.js +13 -7
  15. package/dist/elicitation-handler.d.ts +2 -3
  16. package/dist/elicitation-handler.js +113 -58
  17. package/dist/gateway-arguments.d.ts +1 -1
  18. package/dist/gateway-arguments.js +90 -23
  19. package/dist/glimpse-ui.js +10 -5
  20. package/dist/host-html-template.d.ts +3 -1
  21. package/dist/host-html-template.js +9 -12
  22. package/dist/index.d.ts +1 -1
  23. package/dist/index.js +264 -119
  24. package/dist/init.d.ts +1 -3
  25. package/dist/init.js +91 -51
  26. package/dist/legacy-migration.d.ts +1 -1
  27. package/dist/legacy-migration.js +17 -12
  28. package/dist/lifecycle.d.ts +3 -3
  29. package/dist/lifecycle.js +33 -17
  30. package/dist/logger.d.ts +2 -2
  31. package/dist/logger.js +4 -2
  32. package/dist/mcp-auth-flow.js +150 -58
  33. package/dist/mcp-auth.d.ts +7 -7
  34. package/dist/mcp-auth.js +189 -138
  35. package/dist/mcp-callback-server.js +19 -8
  36. package/dist/mcp-code.js +168 -54
  37. package/dist/mcp-keyring-helper.cjs +57 -23
  38. package/dist/mcp-oauth-provider.d.ts +6 -7
  39. package/dist/mcp-oauth-provider.js +146 -72
  40. package/dist/mcp-oauth-runtime.js +29 -16
  41. package/dist/mcp-output-guard.js +193 -65
  42. package/dist/mcp-output-preview.js +41 -20
  43. package/dist/mcp-panel.d.ts +9 -9
  44. package/dist/mcp-panel.js +141 -70
  45. package/dist/mcp-script-worker.mjs +135 -43
  46. package/dist/mcp-setup-panel.d.ts +8 -8
  47. package/dist/mcp-setup-panel.js +130 -44
  48. package/dist/mcp-status.js +14 -7
  49. package/dist/mcp-trace.d.ts +10 -5
  50. package/dist/mcp-trace.js +127 -61
  51. package/dist/metadata-cache.d.ts +3 -4
  52. package/dist/metadata-cache.js +83 -43
  53. package/dist/npx-resolver.js +101 -60
  54. package/dist/onboarding-state.js +4 -2
  55. package/dist/plain-result-reader.js +78 -31
  56. package/dist/prompts.js +36 -20
  57. package/dist/proxy-modes.js +670 -193
  58. package/dist/resource-tools.js +30 -9
  59. package/dist/runtime-owner.js +36 -18
  60. package/dist/sampling-handler.d.ts +1 -2
  61. package/dist/sampling-handler.js +41 -21
  62. package/dist/search-ranking.d.ts +1 -1
  63. package/dist/search-ranking.js +67 -34
  64. package/dist/server-manager.d.ts +11 -11
  65. package/dist/server-manager.js +283 -166
  66. package/dist/session-recovery.js +113 -61
  67. package/dist/tool-approval.js +9 -3
  68. package/dist/tool-loader.js +154 -55
  69. package/dist/tool-metadata.d.ts +1 -1
  70. package/dist/tool-metadata.js +51 -23
  71. package/dist/tool-registrar.js +32 -10
  72. package/dist/tool-result-renderer.js +58 -33
  73. package/dist/types.d.ts +2 -2
  74. package/dist/types.js +39 -18
  75. package/dist/ui-message-history.js +33 -13
  76. package/dist/ui-resource-handler.d.ts +3 -3
  77. package/dist/ui-resource-handler.js +37 -21
  78. package/dist/ui-server.js +101 -43
  79. package/dist/ui-session.js +66 -29
  80. package/dist/ui-stream-types.js +14 -4
  81. package/dist/ui-tool-visibility.js +12 -6
  82. package/dist/unix-socket-transport.d.ts +1 -2
  83. package/dist/unix-socket-transport.js +19 -12
  84. package/dist/utils.d.ts +2 -0
  85. package/dist/utils.js +60 -35
  86. package/package.json +28 -2
  87. package/skills/mcp-scripting/SKILL.md +6 -6
@@ -34,9 +34,7 @@ class CollapsibleText {
34
34
  const safeWidth = Math.max(1, Math.floor(width));
35
35
  const charBudget = safeWidth * (this.maxCollapsedLines + 1) * COLLAPSED_RENDER_CHAR_SLACK;
36
36
  if (!this.collapsedText || this.collapsedText.charBudget !== charBudget) {
37
- const prefix = this.text.length > charBudget
38
- ? this.text.slice(0, charBudget)
39
- : this.text;
37
+ const prefix = this.text.length > charBudget ? this.text.slice(0, charBudget) : this.text;
40
38
  this.collapsedText = {
41
39
  charBudget,
42
40
  fullyIncluded: prefix === this.text,
@@ -44,18 +42,19 @@ class CollapsibleText {
44
42
  };
45
43
  }
46
44
  const lines = this.collapsedText.text.render(width);
47
- if (!this.preTruncated && this.collapsedText.fullyIncluded && lines.length <= this.maxCollapsedLines)
45
+ if (!this.preTruncated &&
46
+ this.collapsedText.fullyIncluded &&
47
+ lines.length <= this.maxCollapsedLines) {
48
48
  return lines;
49
- return [
50
- ...lines.slice(0, this.maxCollapsedLines),
51
- ...this.footerText.render(width),
52
- ];
49
+ }
50
+ return [...lines.slice(0, this.maxCollapsedLines), ...this.footerText.render(width)];
53
51
  }
54
52
  invalidate() { }
55
53
  }
56
54
  function truncateText(value, maxChars) {
57
- if (value.length <= maxChars)
55
+ if (value.length <= maxChars) {
58
56
  return value;
57
+ }
59
58
  return `${value.slice(0, Math.max(0, maxChars - 1))}…`;
60
59
  }
61
60
  function formatJsonish(value, maxChars) {
@@ -75,51 +74,65 @@ function formatJsonish(value, maxChars) {
75
74
  }
76
75
  }
77
76
  function hasUsefulObjectContent(value) {
78
- return typeof value === "object" && value !== null && !Array.isArray(value) && Object.keys(value).length > 0;
77
+ return (typeof value === "object" &&
78
+ value !== null &&
79
+ !Array.isArray(value) &&
80
+ Object.keys(value).length > 0);
79
81
  }
80
82
  export function formatMcpProxyToolCallLines(args, maxInputChars = DEFAULT_MAX_CALL_INPUT_CHARS) {
81
- if (args.action === "ui-messages" || args.action === "status")
83
+ if (args.action === "ui-messages" || args.action === "status") {
82
84
  return [`mcp ${args.action}`];
85
+ }
83
86
  if (args.action) {
84
87
  const target = args.tool ?? args.query ?? args.uri ?? args.ref;
85
88
  let line = `mcp ${args.action}${target ? ` ${target}` : ""}${args.server ? `${target ? " @" : ""} ${args.server}` : ""}`;
86
- if (args.action === "search" && args.includeSchemas === false)
89
+ if (args.action === "search" && args.includeSchemas === false) {
87
90
  line += " (schemas hidden)";
88
- return args.action === "call" && args.args ? [line, formatJsonish(args.args, maxInputChars)] : [line];
91
+ }
92
+ return args.action === "call" && args.args
93
+ ? [line, formatJsonish(args.args, maxInputChars)]
94
+ : [line];
89
95
  }
90
96
  if (args.tool) {
91
97
  const target = args.server ? `${args.tool} @ ${args.server}` : args.tool;
92
98
  const lines = [`mcp call ${target}`];
93
- if (args.args)
99
+ if (args.args) {
94
100
  lines.push(formatJsonish(args.args, maxInputChars));
101
+ }
95
102
  return lines;
96
103
  }
97
- if (args.connect)
104
+ if (args.connect) {
98
105
  return [`mcp connect ${args.connect}`];
99
- if (args.describe)
106
+ }
107
+ if (args.describe) {
100
108
  return [`mcp describe ${args.describe}`];
109
+ }
101
110
  if (args.search) {
102
111
  let line = `mcp search ${args.search}`;
103
- if (args.server)
112
+ if (args.server) {
104
113
  line += ` @ ${args.server}`;
105
- if (args.includeSchemas === false)
114
+ }
115
+ if (args.includeSchemas === false) {
106
116
  line += " (schemas hidden)";
117
+ }
107
118
  return [line];
108
119
  }
109
- if (args.server)
120
+ if (args.server) {
110
121
  return [`mcp list ${args.server}`];
122
+ }
111
123
  return ["mcp status"];
112
124
  }
113
125
  export function formatMcpDirectToolCallLines(displayName, args, maxInputChars = DEFAULT_MAX_CALL_INPUT_CHARS) {
114
- if (!hasUsefulObjectContent(args))
126
+ if (!hasUsefulObjectContent(args)) {
115
127
  return [displayName];
128
+ }
116
129
  return [displayName, formatJsonish(args, maxInputChars)];
117
130
  }
118
131
  function renderToolCallLines(lines, theme) {
119
132
  const activeTheme = theme ?? plainTheme;
120
133
  const [title = "mcp", ...rest] = lines;
121
134
  const styledTitle = activeTheme.fg("toolTitle", activeTheme.bold(title));
122
- const styledRest = rest.map(line => activeTheme.fg("muted", line));
135
+ const styledRest = rest.map((line) => activeTheme.fg("muted", line));
123
136
  return new Text([styledTitle, ...styledRest].join("\n"), 0, 0);
124
137
  }
125
138
  export function renderMcpProxyToolCall(args, theme) {
@@ -137,8 +150,9 @@ function blockToLines(block) {
137
150
  return [`[image: ${block.mimeType}]`];
138
151
  }
139
152
  function collectCollapsedResultLines(content, maxLines, maxChars) {
140
- if (content.length === 0)
153
+ if (content.length === 0) {
141
154
  return { lines: ["(empty result)"], truncated: false };
155
+ }
142
156
  const lines = [];
143
157
  let remainingChars = maxChars;
144
158
  let truncated = false;
@@ -159,45 +173,56 @@ function collectCollapsedResultLines(content, maxLines, maxChars) {
159
173
  };
160
174
  for (const block of content) {
161
175
  if (block.type !== "text") {
162
- if (!appendLine(`[image: ${block.mimeType}]`))
176
+ if (!appendLine(`[image: ${block.mimeType}]`)) {
163
177
  break;
178
+ }
164
179
  continue;
165
180
  }
166
181
  let start = 0;
167
182
  while (start <= block.text.length) {
168
183
  const newline = block.text.indexOf("\n", start);
169
184
  const line = newline === -1 ? block.text.slice(start) : block.text.slice(start, newline);
170
- if (!appendLine(line))
185
+ if (!appendLine(line)) {
171
186
  break;
172
- if (newline === -1)
187
+ }
188
+ if (newline === -1) {
173
189
  break;
190
+ }
174
191
  start = newline + 1;
175
192
  }
176
- if (truncated)
193
+ if (truncated) {
177
194
  break;
195
+ }
178
196
  }
179
- if (lines.length === 0)
197
+ if (lines.length === 0) {
180
198
  lines.push("");
181
- if (truncated && lines.length >= maxLines)
199
+ }
200
+ if (truncated && lines.length >= maxLines) {
182
201
  lines.push("…");
202
+ }
183
203
  return { lines, truncated };
184
204
  }
185
205
  export function formatMcpToolResultIdentity(details) {
186
- if (details?.mode !== "call")
206
+ if (details?.mode !== "call") {
187
207
  return null;
208
+ }
188
209
  const server = typeof details.server === "string"
189
210
  ? details.server
190
211
  : typeof details.hintServer === "string"
191
212
  ? details.hintServer
192
213
  : null;
193
- if (!server)
214
+ if (!server) {
194
215
  return null;
195
- if (typeof details.tool === "string")
216
+ }
217
+ if (typeof details.tool === "string") {
196
218
  return `MCP ${server}/${details.tool}`;
197
- if (typeof details.resourceUri === "string")
219
+ }
220
+ if (typeof details.resourceUri === "string") {
198
221
  return `MCP ${server} resource ${details.resourceUri}`;
199
- if (typeof details.requestedTool === "string")
222
+ }
223
+ if (typeof details.requestedTool === "string") {
200
224
  return `MCP ${server}/${details.requestedTool}`;
225
+ }
201
226
  return null;
202
227
  }
203
228
  export function formatMcpToolResultLines(result, expanded, maxCollapsedLines = 3, maxCollapsedChars = DEFAULT_MAX_COLLAPSED_CHARS) {
package/dist/types.d.ts CHANGED
@@ -7,7 +7,7 @@ import type { UiToolVisibility } from "./ui-tool-visibility.ts";
7
7
  export type Transport = McpTransport;
8
8
  /** Versioned shared-event-bus channel for read-only MCP runtime snapshots. */
9
9
  export declare const MCP_STATUS_EVENT = "fitch-mcp-adapter/status/v1";
10
- export declare const MCP_STATUS_SNAPSHOT_VERSION: 1;
10
+ export declare const MCP_STATUS_SNAPSHOT_VERSION = 1;
11
11
  export type McpServerRuntimeStatus = "connected" | "cached" | "failed" | "needs-auth" | "not-connected" | "disabled";
12
12
  export interface McpServerStatusSnapshot {
13
13
  readonly name: string;
@@ -209,7 +209,7 @@ export interface OAuthCrossAppAccessConfig {
209
209
  /** Optional IdP client_secret_post credential, also resolved lazily. */
210
210
  clientSecret?: string;
211
211
  }
212
- export declare function isNonInteractiveOAuth(config: OAuthConfig | false | undefined): boolean;
212
+ export declare function isNonInteractiveOAuth(config: OAuthConfig | false | null | undefined): boolean;
213
213
  export interface OAuthConfig {
214
214
  /** Skip only authorization-server metadata issuer validation. Defaults to false. */
215
215
  skipIssuerMetadataValidation?: boolean;
package/dist/types.js CHANGED
@@ -11,7 +11,9 @@ export function extractUiPromptText(params) {
11
11
  }
12
12
  if (params.role === "user" && Array.isArray(params.content)) {
13
13
  const text = params.content
14
- .map((block) => (block && typeof block === "object" && "text" in block ? String(block.text ?? "") : ""))
14
+ .map((block) => block && typeof block === "object" && "text" in block
15
+ ? String(block.text ?? "")
16
+ : "")
15
17
  .filter(Boolean)
16
18
  .join("\n\n");
17
19
  return text || undefined;
@@ -51,8 +53,9 @@ export function parseUiPromptHandoff(prompt) {
51
53
  }
52
54
  export function createUiModelContextUpdate(params, maxChars = 12_000) {
53
55
  const payload = Object.fromEntries(Object.entries(params).filter(([, value]) => value !== undefined));
54
- if (Object.keys(payload).length === 0)
56
+ if (Object.keys(payload).length === 0) {
55
57
  return undefined;
58
+ }
56
59
  const serialized = JSON.stringify(payload);
57
60
  if (serialized.length <= maxChars) {
58
61
  return { payload, summary: serialized, truncated: false };
@@ -63,18 +66,25 @@ export function createUiModelContextUpdate(params, maxChars = 12_000) {
63
66
  };
64
67
  }
65
68
  export function isNonInteractiveOAuth(config) {
66
- return !!config && (config.grantType === "client_credentials" || config.crossAppAccess !== undefined);
69
+ return (config !== false &&
70
+ config !== null &&
71
+ config !== undefined &&
72
+ (config.grantType === "client_credentials" || config.crossAppAccess !== undefined));
67
73
  }
68
74
  /** Validate protocol options for both file and in-memory configurations. */
69
75
  export function validateServerProtocolConfig(definition) {
70
- if (definition.protocolVersion !== undefined && definition.protocolVersion !== "auto" && definition.protocolVersion !== "legacy") {
76
+ if (definition.protocolVersion !== undefined &&
77
+ definition.protocolVersion !== "auto" &&
78
+ definition.protocolVersion !== "legacy") {
71
79
  throw new Error("protocolVersion must be auto or legacy");
72
80
  }
73
- if (definition.retryOnTransportFailure !== undefined && typeof definition.retryOnTransportFailure !== "boolean") {
81
+ if (definition.retryOnTransportFailure !== undefined &&
82
+ typeof definition.retryOnTransportFailure !== "boolean") {
74
83
  throw new Error("retryOnTransportFailure must be a boolean");
75
84
  }
76
- if (definition.oauth && definition.oauth.skipIssuerMetadataValidation !== undefined
77
- && typeof definition.oauth.skipIssuerMetadataValidation !== "boolean") {
85
+ if (definition.oauth &&
86
+ definition.oauth.skipIssuerMetadataValidation !== undefined &&
87
+ typeof definition.oauth.skipIssuerMetadataValidation !== "boolean") {
78
88
  throw new Error("oauth.skipIssuerMetadataValidation must be a boolean");
79
89
  }
80
90
  }
@@ -86,16 +96,19 @@ export function isServerDisabled(definition) {
86
96
  * Get server prefix based on tool prefix mode.
87
97
  */
88
98
  export function getServerPrefix(serverName, mode) {
89
- if (mode === "none")
99
+ if (mode === "none") {
90
100
  return "";
101
+ }
91
102
  if (mode === "short") {
92
103
  let short = serverName.replace(/-?mcp$/i, "").replace(/-/g, "_");
93
- if (!short)
104
+ if (!short) {
94
105
  short = "mcp";
106
+ }
95
107
  return short;
96
108
  }
97
- if (mode === "mcp")
109
+ if (mode === "mcp") {
98
110
  return `mcp__${serverName.replace(/-/g, "_")}`;
111
+ }
99
112
  return serverName.replace(/-/g, "_");
100
113
  }
101
114
  /**
@@ -111,8 +124,9 @@ export function resolveToolPrefix(definition, globalPrefix) {
111
124
  }
112
125
  export function sanitizePromptName(name) {
113
126
  const cleaned = name.replace(/[^A-Za-z0-9_-]+/g, "_").replace(/^[_-]+|[_-]+$/g, "");
114
- if (!cleaned)
127
+ if (!cleaned) {
115
128
  return "prompt";
129
+ }
116
130
  return /^[0-9]/.test(cleaned) ? `_${cleaned}` : cleaned;
117
131
  }
118
132
  export function formatPromptCommandName(promptName, serverName, prefix) {
@@ -132,34 +146,41 @@ export function getToolNameCandidates(toolName, serverName, prefix) {
132
146
  ]);
133
147
  }
134
148
  function globToRegExp(pattern) {
135
- const escaped = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*").replace(/\?/g, ".");
149
+ const escaped = pattern
150
+ .replace(/[.+^${}()|[\]\\]/g, "\\$&")
151
+ .replace(/\*/g, ".*")
152
+ .replace(/\?/g, ".");
136
153
  return new RegExp(`^${escaped}$`);
137
154
  }
138
155
  export function matchesToolPattern(candidates, patterns) {
139
- if (!Array.isArray(patterns) || patterns.length === 0)
156
+ if (!Array.isArray(patterns) || patterns.length === 0) {
140
157
  return false;
158
+ }
141
159
  for (const pattern of patterns) {
142
- if (typeof pattern !== "string")
160
+ if (typeof pattern !== "string") {
143
161
  continue;
162
+ }
144
163
  const normalized = normalizeToolName(pattern);
145
164
  if (!normalized.includes("*") && !normalized.includes("?") && candidates.has(normalized)) {
146
165
  return true;
147
166
  }
148
- if ((normalized.includes("*") || normalized.includes("?")) && [...candidates].some(candidate => globToRegExp(normalized).test(candidate))) {
167
+ if ((normalized.includes("*") || normalized.includes("?")) &&
168
+ [...candidates].some((candidate) => globToRegExp(normalized).test(candidate))) {
149
169
  return true;
150
170
  }
151
171
  }
152
172
  return false;
153
173
  }
154
174
  export function isToolIncluded(toolName, serverName, prefix, includeTools) {
155
- if (!Array.isArray(includeTools) || includeTools.length === 0)
175
+ if (!Array.isArray(includeTools) || includeTools.length === 0) {
156
176
  return true;
177
+ }
157
178
  return matchesToolPattern(getToolNameCandidates(toolName, serverName, prefix), includeTools);
158
179
  }
159
180
  export function isToolExcluded(toolName, serverName, prefix, excludeTools) {
160
181
  return matchesToolPattern(getToolNameCandidates(toolName, serverName, prefix), excludeTools);
161
182
  }
162
183
  export function isToolAllowed(toolName, serverName, prefix, includeTools, excludeTools) {
163
- return isToolIncluded(toolName, serverName, prefix, includeTools)
164
- && !isToolExcluded(toolName, serverName, prefix, excludeTools);
184
+ return (isToolIncluded(toolName, serverName, prefix, includeTools) &&
185
+ !isToolExcluded(toolName, serverName, prefix, excludeTools));
165
186
  }
@@ -12,12 +12,14 @@ export function createUiMessageHistory(outputDirectory) {
12
12
  messages,
13
13
  append(category, value) {
14
14
  try {
15
- if (failed)
15
+ if (failed) {
16
16
  throw new Error(messages.historyError);
17
+ }
17
18
  if (!messages.historyRef) {
18
19
  const root = outputDirectory === undefined ? tmpdir() : resolve(outputDirectory);
19
- if (outputDirectory !== undefined)
20
+ if (outputDirectory !== undefined) {
20
21
  mkdirSync(root, { recursive: true, mode: 0o700 });
22
+ }
21
23
  const dir = mkdtempSync(join(root, "pi-mcp-output-"));
22
24
  const ref = join(dir, `output-${randomBytes(4).toString("hex")}.txt`);
23
25
  writeFileSync(ref, "", { mode: 0o600 });
@@ -35,10 +37,12 @@ export function createUiMessageHistory(outputDirectory) {
35
37
  catch (error) {
36
38
  failed = true;
37
39
  messages.historyError = error instanceof Error ? error.message : String(error);
38
- if (category === "intents")
40
+ if (category === "intents") {
39
41
  messages.intents.push(value);
40
- else
42
+ }
43
+ else {
41
44
  messages[category].push(value);
45
+ }
42
46
  return `UI history could not be saved; messages remain in memory for recovery: ${messages.historyError}. Unsaved messages will be lost on reload or process exit.`;
43
47
  }
44
48
  },
@@ -47,18 +51,25 @@ export function createUiMessageHistory(outputDirectory) {
47
51
  export async function* uiMessages(messages, category) {
48
52
  if (messages.historyRef && messages.historyBytes) {
49
53
  // A failed partial append is not an accepted event; the in-memory tail owns it instead.
50
- const input = createReadStream(messages.historyRef, { encoding: "utf8", end: messages.historyBytes - 1 });
54
+ const input = createReadStream(messages.historyRef, {
55
+ encoding: "utf8",
56
+ end: messages.historyBytes - 1,
57
+ });
51
58
  let bytes = 0, count = 0;
52
- input.on("data", chunk => { bytes += Buffer.byteLength(chunk); });
59
+ input.on("data", (chunk) => {
60
+ bytes += Buffer.byteLength(chunk);
61
+ });
53
62
  const lines = createInterface({ input, crlfDelay: Infinity });
54
63
  try {
55
64
  for await (const line of lines) {
56
65
  const record = JSON.parse(line);
57
66
  count++;
58
- if (record.category === category)
67
+ if (record.category === category) {
59
68
  yield record.value;
69
+ }
60
70
  }
61
- if (bytes !== messages.historyBytes || (messages.historyCount !== undefined && count !== messages.historyCount)) {
71
+ if (bytes !== messages.historyBytes ||
72
+ (messages.historyCount !== undefined && count !== messages.historyCount)) {
62
73
  throw new Error("UI history is incomplete; accepted messages remain pending recovery");
63
74
  }
64
75
  }
@@ -67,16 +78,25 @@ export async function* uiMessages(messages, category) {
67
78
  input.destroy();
68
79
  }
69
80
  }
70
- for (const value of messages[category])
81
+ for (const value of messages[category]) {
71
82
  yield value;
83
+ }
72
84
  }
73
85
  export async function materializeUiMessages(messages) {
74
- const result = { prompts: [], intents: [], notifications: [], contexts: messages.contexts };
75
- for await (const value of uiMessages(messages, "prompts"))
86
+ const result = {
87
+ prompts: [],
88
+ intents: [],
89
+ notifications: [],
90
+ contexts: messages.contexts,
91
+ };
92
+ for await (const value of uiMessages(messages, "prompts")) {
76
93
  result.prompts.push(value);
77
- for await (const value of uiMessages(messages, "intents"))
94
+ }
95
+ for await (const value of uiMessages(messages, "intents")) {
78
96
  result.intents.push(value);
79
- for await (const value of uiMessages(messages, "notifications"))
97
+ }
98
+ for await (const value of uiMessages(messages, "notifications")) {
80
99
  result.notifications.push(value);
100
+ }
81
101
  return result;
82
102
  }
@@ -8,9 +8,9 @@ interface ReadUiResourceOptions {
8
8
  onNeedsAuth?: SessionRecoveryDeps["onNeedsAuth"];
9
9
  }
10
10
  export declare class UiResourceHandler {
11
- private log;
12
- private manager;
13
- private config;
11
+ private readonly log;
12
+ private readonly manager;
13
+ private readonly config;
14
14
  constructor(manager: McpServerManager, config?: McpConfig);
15
15
  readUiResource(serverName: string, uri: string, options?: ReadUiResourceOptions): Promise<UiResourceContent>;
16
16
  private getListResourceMeta;
@@ -1,5 +1,5 @@
1
1
  import { logger } from "./logger.js";
2
- import { isServerDisabled } from "./types.js";
2
+ import { isServerDisabled, } from "./types.js";
3
3
  // MCP Apps resource media type; metadata reads must not import the app bridge.
4
4
  export const RESOURCE_MIME_TYPE = "text/html;profile=mcp-app";
5
5
  export class UiResourceHandler {
@@ -11,8 +11,10 @@ export class UiResourceHandler {
11
11
  this.config = config;
12
12
  }
13
13
  async readUiResource(serverName, uri, options = {}) {
14
- const { UrlElicitationRequiredError } = await import("@modelcontextprotocol/client");
15
- const { SessionRecoveryAuthRequiredError, withSessionRecovery } = await import("./session-recovery.js");
14
+ const [{ UrlElicitationRequiredError }, { SessionRecoveryAuthRequiredError, withSessionRecovery },] = await Promise.all([
15
+ import("@modelcontextprotocol/client"),
16
+ import("./session-recovery.js"),
17
+ ]);
16
18
  const log = this.log.child({ server: serverName, uri });
17
19
  if (!uri.startsWith("ui://")) {
18
20
  throw new Error(`Invalid UI resource "${uri}": URI must start with ui://`);
@@ -45,8 +47,10 @@ export class UiResourceHandler {
45
47
  }
46
48
  }
47
49
  catch (error) {
48
- if (error instanceof UrlElicitationRequiredError || error instanceof SessionRecoveryAuthRequiredError)
50
+ if (error instanceof UrlElicitationRequiredError ||
51
+ error instanceof SessionRecoveryAuthRequiredError) {
49
52
  throw error;
53
+ }
50
54
  const message = error instanceof Error ? error.message : String(error);
51
55
  log.error("Failed to read resource", error instanceof Error ? error : undefined);
52
56
  throw new Error(`Failed to fetch UI resource "${uri}": ${message}`, error instanceof Error ? { cause: error } : undefined);
@@ -66,18 +70,22 @@ export class UiResourceHandler {
66
70
  const listMeta = extractUiMeta(this.getListResourceMeta(serverName, uri));
67
71
  log.debug("Resource loaded successfully", {
68
72
  contentLength: html.length,
69
- hasCsp: !!contentMeta.csp || !!listMeta.csp,
73
+ hasCsp: Boolean(contentMeta.csp) || Boolean(listMeta.csp),
70
74
  });
71
75
  return {
72
76
  uri: content.uri ?? uri,
73
77
  html,
74
78
  mimeType: mimeType ?? RESOURCE_MIME_TYPE,
75
79
  meta: {
76
- ...((contentMeta.csp ?? listMeta.csp) !== undefined ? { csp: contentMeta.csp ?? listMeta.csp } : {}),
80
+ ...((contentMeta.csp ?? listMeta.csp) !== undefined
81
+ ? { csp: contentMeta.csp ?? listMeta.csp }
82
+ : {}),
77
83
  ...((contentMeta.permissions ?? listMeta.permissions) !== undefined
78
84
  ? { permissions: contentMeta.permissions ?? listMeta.permissions }
79
85
  : {}),
80
- ...((contentMeta.domain ?? listMeta.domain) !== undefined ? { domain: contentMeta.domain ?? listMeta.domain } : {}),
86
+ ...((contentMeta.domain ?? listMeta.domain) !== undefined
87
+ ? { domain: contentMeta.domain ?? listMeta.domain }
88
+ : {}),
81
89
  ...((contentMeta.prefersBorder ?? listMeta.prefersBorder) !== undefined
82
90
  ? { prefersBorder: contentMeta.prefersBorder ?? listMeta.prefersBorder }
83
91
  : {}),
@@ -86,11 +94,13 @@ export class UiResourceHandler {
86
94
  }
87
95
  getListResourceMeta(serverName, uri) {
88
96
  const connection = this.manager.getConnection(serverName);
89
- if (!connection?.resources?.length)
97
+ if (!connection?.resources?.length) {
90
98
  return undefined;
99
+ }
91
100
  const resource = connection.resources.find((entry) => entry.uri === uri);
92
- if (!resource || !resource._meta || typeof resource._meta !== "object")
101
+ if (!resource || !resource._meta || typeof resource._meta !== "object") {
93
102
  return undefined;
103
+ }
94
104
  return resource._meta;
95
105
  }
96
106
  }
@@ -100,11 +110,13 @@ function selectContent(result, preferredUri) {
100
110
  throw new Error(`No contents returned for UI resource: ${preferredUri}`);
101
111
  }
102
112
  const byUri = contents.find((content) => content.uri === preferredUri);
103
- if (byUri)
113
+ if (byUri) {
104
114
  return byUri;
115
+ }
105
116
  const byHtmlMime = contents.find((content) => content.mimeType && isHtmlMimeType(content.mimeType));
106
- if (byHtmlMime)
117
+ if (byHtmlMime) {
107
118
  return byHtmlMime;
119
+ }
108
120
  const firstContent = contents[0];
109
121
  if (!firstContent) {
110
122
  throw new Error(`No contents returned for UI resource: ${preferredUri}`);
@@ -136,28 +148,28 @@ const UI_CSP_DOMAIN_FIELDS = [
136
148
  "baseUriDomains",
137
149
  ];
138
150
  function extractUiMeta(meta) {
139
- if (!meta || typeof meta !== "object")
151
+ if (!meta || typeof meta !== "object") {
140
152
  return {};
153
+ }
141
154
  const ui = isRecord(meta.ui) ? meta.ui : undefined;
142
155
  const out = {};
143
156
  const openAiCsp = hasOwnProperty(meta, "openai/widgetCSP")
144
157
  ? normalizeOpenAiWidgetCsp(meta["openai/widgetCSP"])
145
158
  : undefined;
146
- const hasStandardCsp = !!ui && hasOwnProperty(ui, "csp");
159
+ const hasStandardCsp = ui !== undefined && hasOwnProperty(ui, "csp");
147
160
  const standardCspValue = hasStandardCsp ? ui.csp : undefined;
148
161
  if (hasStandardCsp && !isRecord(standardCspValue)) {
149
162
  // A declared canonical container takes precedence even when malformed.
150
163
  out.csp = {};
151
164
  }
152
165
  else {
153
- const standardCsp = hasStandardCsp
154
- ? normalizeUiResourceCsp(standardCspValue)
155
- : undefined;
166
+ const standardCsp = hasStandardCsp ? normalizeUiResourceCsp(standardCspValue) : undefined;
156
167
  if (openAiCsp || standardCsp) {
157
168
  out.csp = { ...openAiCsp, ...standardCsp };
158
169
  if (isRecord(standardCspValue)) {
159
170
  for (const [, standardField] of OPENAI_CSP_FIELD_MAPPINGS) {
160
- if (hasOwnProperty(standardCspValue, standardField) && !copyStringArray(standardCspValue[standardField])) {
171
+ if (hasOwnProperty(standardCspValue, standardField) &&
172
+ !copyStringArray(standardCspValue[standardField])) {
161
173
  delete out.csp[standardField];
162
174
  }
163
175
  }
@@ -176,24 +188,28 @@ function extractUiMeta(meta) {
176
188
  return out;
177
189
  }
178
190
  function normalizeUiResourceCsp(value) {
179
- if (!isRecord(value))
191
+ if (!isRecord(value)) {
180
192
  return {};
193
+ }
181
194
  const csp = {};
182
195
  for (const field of UI_CSP_DOMAIN_FIELDS) {
183
196
  const domains = copyStringArray(value[field]);
184
- if (domains)
197
+ if (domains) {
185
198
  csp[field] = domains;
199
+ }
186
200
  }
187
201
  return csp;
188
202
  }
189
203
  function normalizeOpenAiWidgetCsp(value) {
190
- if (!isRecord(value))
204
+ if (!isRecord(value)) {
191
205
  return {};
206
+ }
192
207
  const csp = {};
193
208
  for (const [sourceField, targetField] of OPENAI_CSP_FIELD_MAPPINGS) {
194
209
  const domains = copyStringArray(value[sourceField]);
195
- if (domains)
210
+ if (domains) {
196
211
  csp[targetField] = domains;
212
+ }
197
213
  }
198
214
  return csp;
199
215
  }