@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.
- package/CHANGELOG.md +140 -7
- 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.d.ts +3 -3
- package/dist/tool-result-renderer.js +65 -44
- 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 +29 -5
- package/skills/mcp-scripting/SKILL.md +6 -6
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
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 &&
|
|
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
|
-
// 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" &&
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
216
|
+
}
|
|
217
|
+
if (typeof details.tool === "string") {
|
|
204
218
|
return `MCP ${server}/${details.tool}`;
|
|
205
|
-
|
|
219
|
+
}
|
|
220
|
+
if (typeof details.resourceUri === "string") {
|
|
206
221
|
return `MCP ${server} resource ${details.resourceUri}`;
|
|
207
|
-
|
|
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
|
-
|
|
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
|
|
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
|
}
|