@fitchmultz/pi-mcp-adapter 8.0.5 → 8.0.7

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 (88) hide show
  1. package/CHANGELOG.md +140 -7
  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.d.ts +3 -3
  73. package/dist/tool-result-renderer.js +65 -44
  74. package/dist/types.d.ts +2 -2
  75. package/dist/types.js +39 -18
  76. package/dist/ui-message-history.js +33 -13
  77. package/dist/ui-resource-handler.d.ts +3 -3
  78. package/dist/ui-resource-handler.js +37 -21
  79. package/dist/ui-server.js +101 -43
  80. package/dist/ui-session.js +66 -29
  81. package/dist/ui-stream-types.js +14 -4
  82. package/dist/ui-tool-visibility.js +12 -6
  83. package/dist/unix-socket-transport.d.ts +1 -2
  84. package/dist/unix-socket-transport.js +19 -12
  85. package/dist/utils.d.ts +2 -0
  86. package/dist/utils.js +60 -35
  87. package/package.json +29 -5
  88. package/skills/mcp-scripting/SKILL.md +6 -6
@@ -1,4 +1,4 @@
1
- import { getKeybindings, Text } from "@earendil-works/pi-tui";
1
+ import { Text } from "@earendil-works/pi-tui";
2
2
  const plainTheme = {
3
3
  fg: (_name, text) => text,
4
4
  bold: (text) => text,
@@ -25,7 +25,7 @@ class CollapsibleText {
25
25
  this.expandHint = expandHint;
26
26
  this.preTruncated = preTruncated;
27
27
  this.fullText = new Text(text, 0, 0);
28
- this.footerText = new Text(`${ellipsis}\n${expandHint}`, 0, 0);
28
+ this.footerText = new Text(expandHint === undefined ? ellipsis : `${ellipsis}\n${expandHint}`, 0, 0);
29
29
  }
30
30
  render(width) {
31
31
  if (this.expanded) {
@@ -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,26 +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
- // Read the shared TUI keybinding registry without importing the entire host SDK.
57
- function expandHint(theme) {
58
- const keys = getKeybindings().getKeys("app.tools.expand").join("/")
59
- .split("/").map(key => key.split("+")
60
- .map(part => process.platform === "darwin" && part.toLowerCase() === "alt" ? "option" : part)
61
- .join("+")).join("/");
62
- return theme.fg("dim", keys) + theme.fg("muted", " to expand");
63
- }
64
54
  function truncateText(value, maxChars) {
65
- if (value.length <= maxChars)
55
+ if (value.length <= maxChars) {
66
56
  return value;
57
+ }
67
58
  return `${value.slice(0, Math.max(0, maxChars - 1))}…`;
68
59
  }
69
60
  function formatJsonish(value, maxChars) {
@@ -83,51 +74,65 @@ function formatJsonish(value, maxChars) {
83
74
  }
84
75
  }
85
76
  function hasUsefulObjectContent(value) {
86
- 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);
87
81
  }
88
82
  export function formatMcpProxyToolCallLines(args, maxInputChars = DEFAULT_MAX_CALL_INPUT_CHARS) {
89
- if (args.action === "ui-messages" || args.action === "status")
83
+ if (args.action === "ui-messages" || args.action === "status") {
90
84
  return [`mcp ${args.action}`];
85
+ }
91
86
  if (args.action) {
92
87
  const target = args.tool ?? args.query ?? args.uri ?? args.ref;
93
88
  let line = `mcp ${args.action}${target ? ` ${target}` : ""}${args.server ? `${target ? " @" : ""} ${args.server}` : ""}`;
94
- if (args.action === "search" && args.includeSchemas === false)
89
+ if (args.action === "search" && args.includeSchemas === false) {
95
90
  line += " (schemas hidden)";
96
- 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];
97
95
  }
98
96
  if (args.tool) {
99
97
  const target = args.server ? `${args.tool} @ ${args.server}` : args.tool;
100
98
  const lines = [`mcp call ${target}`];
101
- if (args.args)
99
+ if (args.args) {
102
100
  lines.push(formatJsonish(args.args, maxInputChars));
101
+ }
103
102
  return lines;
104
103
  }
105
- if (args.connect)
104
+ if (args.connect) {
106
105
  return [`mcp connect ${args.connect}`];
107
- if (args.describe)
106
+ }
107
+ if (args.describe) {
108
108
  return [`mcp describe ${args.describe}`];
109
+ }
109
110
  if (args.search) {
110
111
  let line = `mcp search ${args.search}`;
111
- if (args.server)
112
+ if (args.server) {
112
113
  line += ` @ ${args.server}`;
113
- if (args.includeSchemas === false)
114
+ }
115
+ if (args.includeSchemas === false) {
114
116
  line += " (schemas hidden)";
117
+ }
115
118
  return [line];
116
119
  }
117
- if (args.server)
120
+ if (args.server) {
118
121
  return [`mcp list ${args.server}`];
122
+ }
119
123
  return ["mcp status"];
120
124
  }
121
125
  export function formatMcpDirectToolCallLines(displayName, args, maxInputChars = DEFAULT_MAX_CALL_INPUT_CHARS) {
122
- if (!hasUsefulObjectContent(args))
126
+ if (!hasUsefulObjectContent(args)) {
123
127
  return [displayName];
128
+ }
124
129
  return [displayName, formatJsonish(args, maxInputChars)];
125
130
  }
126
131
  function renderToolCallLines(lines, theme) {
127
132
  const activeTheme = theme ?? plainTheme;
128
133
  const [title = "mcp", ...rest] = lines;
129
134
  const styledTitle = activeTheme.fg("toolTitle", activeTheme.bold(title));
130
- const styledRest = rest.map(line => activeTheme.fg("muted", line));
135
+ const styledRest = rest.map((line) => activeTheme.fg("muted", line));
131
136
  return new Text([styledTitle, ...styledRest].join("\n"), 0, 0);
132
137
  }
133
138
  export function renderMcpProxyToolCall(args, theme) {
@@ -145,8 +150,9 @@ function blockToLines(block) {
145
150
  return [`[image: ${block.mimeType}]`];
146
151
  }
147
152
  function collectCollapsedResultLines(content, maxLines, maxChars) {
148
- if (content.length === 0)
153
+ if (content.length === 0) {
149
154
  return { lines: ["(empty result)"], truncated: false };
155
+ }
150
156
  const lines = [];
151
157
  let remainingChars = maxChars;
152
158
  let truncated = false;
@@ -167,45 +173,56 @@ function collectCollapsedResultLines(content, maxLines, maxChars) {
167
173
  };
168
174
  for (const block of content) {
169
175
  if (block.type !== "text") {
170
- if (!appendLine(`[image: ${block.mimeType}]`))
176
+ if (!appendLine(`[image: ${block.mimeType}]`)) {
171
177
  break;
178
+ }
172
179
  continue;
173
180
  }
174
181
  let start = 0;
175
182
  while (start <= block.text.length) {
176
183
  const newline = block.text.indexOf("\n", start);
177
184
  const line = newline === -1 ? block.text.slice(start) : block.text.slice(start, newline);
178
- if (!appendLine(line))
185
+ if (!appendLine(line)) {
179
186
  break;
180
- if (newline === -1)
187
+ }
188
+ if (newline === -1) {
181
189
  break;
190
+ }
182
191
  start = newline + 1;
183
192
  }
184
- if (truncated)
193
+ if (truncated) {
185
194
  break;
195
+ }
186
196
  }
187
- if (lines.length === 0)
197
+ if (lines.length === 0) {
188
198
  lines.push("");
189
- if (truncated && lines.length >= maxLines)
199
+ }
200
+ if (truncated && lines.length >= maxLines) {
190
201
  lines.push("…");
202
+ }
191
203
  return { lines, truncated };
192
204
  }
193
205
  export function formatMcpToolResultIdentity(details) {
194
- if (details?.mode !== "call")
206
+ if (details?.mode !== "call") {
195
207
  return null;
208
+ }
196
209
  const server = typeof details.server === "string"
197
210
  ? details.server
198
211
  : typeof details.hintServer === "string"
199
212
  ? details.hintServer
200
213
  : null;
201
- if (!server)
214
+ if (!server) {
202
215
  return null;
203
- if (typeof details.tool === "string")
216
+ }
217
+ if (typeof details.tool === "string") {
204
218
  return `MCP ${server}/${details.tool}`;
205
- if (typeof details.resourceUri === "string")
219
+ }
220
+ if (typeof details.resourceUri === "string") {
206
221
  return `MCP ${server} resource ${details.resourceUri}`;
207
- if (typeof details.requestedTool === "string")
222
+ }
223
+ if (typeof details.requestedTool === "string") {
208
224
  return `MCP ${server}/${details.requestedTool}`;
225
+ }
209
226
  return null;
210
227
  }
211
228
  export function formatMcpToolResultLines(result, expanded, maxCollapsedLines = 3, maxCollapsedChars = DEFAULT_MAX_COLLAPSED_CHARS) {
@@ -232,5 +249,9 @@ export function renderMcpToolResult(result, options, theme, context) {
232
249
  ...(identity ? [activeTheme.fg("muted", identity)] : []),
233
250
  ...display.lines.map((line) => activeTheme.fg("toolOutput", line)),
234
251
  ].join("\n");
235
- return new CollapsibleText(output, expanded, DEFAULT_MAX_COLLAPSED_LINES + (identity ? 1 : 0), activeTheme.fg("muted", "…"), activeTheme.fg("muted", `(${expandHint(activeTheme)})`), display.truncated);
252
+ // Pi injects keyHint so the hint shows the user's configured keys.
253
+ const expandHint = context
254
+ ? `${activeTheme.fg("muted", "(")}${context.keyHint("app.tools.expand", "to expand")}${activeTheme.fg("muted", ")")}`
255
+ : undefined;
256
+ return new CollapsibleText(output, expanded, DEFAULT_MAX_COLLAPSED_LINES + (identity ? 1 : 0), activeTheme.fg("muted", "…"), expandHint, display.truncated);
236
257
  }
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
  }