@fitchmultz/pi-mcp-adapter 8.0.6 → 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.
- package/CHANGELOG.md +128 -8
- package/OAUTH.md +13 -16
- package/README.md +8 -8
- package/cli.js +67 -28
- package/dist/abort.js +18 -9
- package/dist/client-operations.js +4 -2
- package/dist/commands.js +77 -43
- package/dist/config.js +194 -105
- package/dist/connection-runtime.d.ts +2 -2
- package/dist/connection-runtime.js +29 -18
- package/dist/consent-manager.d.ts +2 -2
- package/dist/consent-manager.js +5 -2
- package/dist/direct-tools.d.ts +1 -1
- package/dist/direct-tools.js +13 -7
- package/dist/elicitation-handler.d.ts +2 -3
- package/dist/elicitation-handler.js +113 -58
- package/dist/gateway-arguments.d.ts +1 -1
- package/dist/gateway-arguments.js +90 -23
- package/dist/glimpse-ui.js +10 -5
- package/dist/host-html-template.d.ts +3 -1
- package/dist/host-html-template.js +9 -12
- package/dist/index.d.ts +1 -1
- package/dist/index.js +264 -119
- package/dist/init.d.ts +1 -3
- package/dist/init.js +91 -51
- package/dist/legacy-migration.d.ts +1 -1
- package/dist/legacy-migration.js +17 -12
- package/dist/lifecycle.d.ts +3 -3
- package/dist/lifecycle.js +33 -17
- package/dist/logger.d.ts +2 -2
- package/dist/logger.js +4 -2
- package/dist/mcp-auth-flow.js +150 -58
- package/dist/mcp-auth.d.ts +7 -7
- package/dist/mcp-auth.js +189 -138
- package/dist/mcp-callback-server.js +19 -8
- package/dist/mcp-code.js +168 -54
- package/dist/mcp-keyring-helper.cjs +57 -23
- package/dist/mcp-oauth-provider.d.ts +6 -7
- package/dist/mcp-oauth-provider.js +146 -72
- package/dist/mcp-oauth-runtime.js +29 -16
- package/dist/mcp-output-guard.js +193 -65
- package/dist/mcp-output-preview.js +41 -20
- package/dist/mcp-panel.d.ts +9 -9
- package/dist/mcp-panel.js +141 -70
- package/dist/mcp-script-worker.mjs +135 -43
- package/dist/mcp-setup-panel.d.ts +8 -8
- package/dist/mcp-setup-panel.js +130 -44
- package/dist/mcp-status.js +14 -7
- package/dist/mcp-trace.d.ts +10 -5
- package/dist/mcp-trace.js +127 -61
- package/dist/metadata-cache.d.ts +3 -4
- package/dist/metadata-cache.js +83 -43
- package/dist/npx-resolver.js +101 -60
- package/dist/onboarding-state.js +4 -2
- package/dist/plain-result-reader.js +78 -31
- package/dist/prompts.js +36 -20
- package/dist/proxy-modes.js +670 -193
- package/dist/resource-tools.js +30 -9
- package/dist/runtime-owner.js +36 -18
- package/dist/sampling-handler.d.ts +1 -2
- package/dist/sampling-handler.js +41 -21
- package/dist/search-ranking.d.ts +1 -1
- package/dist/search-ranking.js +67 -34
- package/dist/server-manager.d.ts +11 -11
- package/dist/server-manager.js +283 -166
- package/dist/session-recovery.js +113 -61
- package/dist/tool-approval.js +9 -3
- package/dist/tool-loader.js +154 -55
- package/dist/tool-metadata.d.ts +1 -1
- package/dist/tool-metadata.js +51 -23
- package/dist/tool-registrar.js +32 -10
- package/dist/tool-result-renderer.js +58 -33
- package/dist/types.d.ts +2 -2
- package/dist/types.js +39 -18
- package/dist/ui-message-history.js +33 -13
- package/dist/ui-resource-handler.d.ts +3 -3
- package/dist/ui-resource-handler.js +37 -21
- package/dist/ui-server.js +101 -43
- package/dist/ui-session.js +66 -29
- package/dist/ui-stream-types.js +14 -4
- package/dist/ui-tool-visibility.js +12 -6
- package/dist/unix-socket-transport.d.ts +1 -2
- package/dist/unix-socket-transport.js +19 -12
- package/dist/utils.d.ts +2 -0
- package/dist/utils.js +60 -35
- package/package.json +26 -2
- 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 &&
|
|
45
|
+
if (!this.preTruncated &&
|
|
46
|
+
this.collapsedText.fullyIncluded &&
|
|
47
|
+
lines.length <= this.maxCollapsedLines) {
|
|
48
48
|
return lines;
|
|
49
|
-
|
|
50
|
-
|
|
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" &&
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
216
|
+
}
|
|
217
|
+
if (typeof details.tool === "string") {
|
|
196
218
|
return `MCP ${server}/${details.tool}`;
|
|
197
|
-
|
|
219
|
+
}
|
|
220
|
+
if (typeof details.resourceUri === "string") {
|
|
198
221
|
return `MCP ${server} resource ${details.resourceUri}`;
|
|
199
|
-
|
|
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
|
|
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) =>
|
|
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
|
|
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 &&
|
|
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 &&
|
|
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 &&
|
|
77
|
-
|
|
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
|
|
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("?")) &&
|
|
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
|
-
|
|
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
|
-
|
|
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, {
|
|
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 => {
|
|
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 ||
|
|
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 = {
|
|
75
|
-
|
|
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
|
-
|
|
94
|
+
}
|
|
95
|
+
for await (const value of uiMessages(messages, "intents")) {
|
|
78
96
|
result.intents.push(value);
|
|
79
|
-
|
|
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
|
|
15
|
-
|
|
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 ||
|
|
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:
|
|
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
|
|
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
|
|
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 =
|
|
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) &&
|
|
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
|
}
|