@fitchmultz/pi-mcp-adapter 7.1.1 → 8.0.1
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 +30 -7
- package/LICENSE +1 -0
- package/OAUTH.md +4 -4
- package/README.md +43 -860
- package/dist/app-bridge.bundle.js +13 -13
- package/dist/config.js +1 -1
- package/dist/direct-tools.js +6 -5
- package/dist/error-signal.d.ts +3 -5
- package/dist/error-signal.js +3 -5
- package/dist/index.js +0 -11
- package/dist/init.js +0 -1
- package/dist/lifecycle.d.ts +0 -4
- package/dist/lifecycle.js +1 -14
- package/dist/mcp-auth-flow.d.ts +3 -9
- package/dist/mcp-auth-flow.js +4 -43
- package/dist/mcp-code.js +0 -1
- package/dist/mcp-output-guard.d.ts +5 -0
- package/dist/mcp-output-guard.js +55 -1
- package/dist/mcp-setup-panel.js +1 -1
- package/dist/proxy-modes.d.ts +1 -1
- package/dist/proxy-modes.js +89 -9
- package/dist/runtime-owner.d.ts +0 -4
- package/dist/runtime-owner.js +5 -18
- package/dist/server-manager.d.ts +2 -9
- package/dist/server-manager.js +7 -99
- package/dist/tool-loader.js +35 -6
- package/dist/tool-registrar.d.ts +11 -0
- package/dist/tool-registrar.js +23 -0
- package/dist/types.d.ts +5 -0
- package/dist/ui-message-history.d.ts +11 -0
- package/dist/ui-message-history.js +81 -0
- package/dist/ui-server.js +17 -10
- package/dist/ui-session.js +3 -2
- package/package.json +10 -13
- package/dist/checkpoint.d.ts +0 -18
- package/dist/checkpoint.js +0 -53
package/dist/config.js
CHANGED
|
@@ -49,7 +49,7 @@ export const KNOWN_SERVER_PRESETS = [
|
|
|
49
49
|
id: "chrome-devtools",
|
|
50
50
|
name: "Chrome DevTools",
|
|
51
51
|
summary: "Inspect and automate a local Chrome browser.",
|
|
52
|
-
entry: { command: "npx", args: ["-y", "chrome-devtools-mcp
|
|
52
|
+
entry: { command: "npx", args: ["-y", "chrome-devtools-mcp"] },
|
|
53
53
|
},
|
|
54
54
|
];
|
|
55
55
|
const IMPORT_PATHS = {
|
package/dist/direct-tools.js
CHANGED
|
@@ -3,6 +3,7 @@ import { parseDirectToolSelectors } from "./metadata-cache.js";
|
|
|
3
3
|
export { getMissingConfiguredDirectToolServers } from "./metadata-cache.js";
|
|
4
4
|
import { executeCall } from "./proxy-modes.js";
|
|
5
5
|
import { isServerDisabled } from "./types.js";
|
|
6
|
+
import { nativeMcpResult } from "./tool-registrar.js";
|
|
6
7
|
export function directToolSelection(config, serverName, envOverride) {
|
|
7
8
|
if (envOverride) {
|
|
8
9
|
const selection = parseDirectToolSelectors(envOverride);
|
|
@@ -33,20 +34,20 @@ export function createDirectToolExecutor(getState, getInitPromise, spec, beforeE
|
|
|
33
34
|
catch (error) {
|
|
34
35
|
throwIfAborted(signal);
|
|
35
36
|
const message = error instanceof Error ? error.message : String(error);
|
|
36
|
-
return {
|
|
37
|
+
return nativeMcpResult({
|
|
37
38
|
content: [{ type: "text", text: `MCP initialization failed: ${message}` }],
|
|
38
39
|
details: { error: "init_failed", message },
|
|
39
|
-
};
|
|
40
|
+
}, !!spec.outputSchema);
|
|
40
41
|
}
|
|
41
42
|
}
|
|
42
43
|
if (!state) {
|
|
43
|
-
return {
|
|
44
|
+
return nativeMcpResult({
|
|
44
45
|
content: [{ type: "text", text: "MCP not initialized" }],
|
|
45
46
|
details: { error: "not_initialized" },
|
|
46
|
-
};
|
|
47
|
+
}, !!spec.outputSchema);
|
|
47
48
|
}
|
|
48
49
|
const result = await executeCall(state, spec.originalName, params, spec.serverName, undefined, signal, { toolCallId }, beforeExecute ? (callSignal, operation) => beforeExecute(toolCallId, { ...ctx, signal: callSignal }, operation) : undefined, { exactOriginalName: true, ...(spec.resourceUri ? { resourceUri: spec.resourceUri } : {}) });
|
|
49
50
|
const { mode: _mode, ...details } = result.details;
|
|
50
|
-
return { ...result, details };
|
|
51
|
+
return nativeMcpResult({ ...result, details }, !!spec.outputSchema);
|
|
51
52
|
};
|
|
52
53
|
}
|
package/dist/error-signal.d.ts
CHANGED
|
@@ -1,11 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Classify returned MCP execution failures for Pi's native result-level isError.
|
|
3
3
|
*
|
|
4
4
|
* A failed MCP tool call is *returned* (not thrown), tagged `details.error: "tool_error"` (the server
|
|
5
|
-
* returned an error result) or `"call_failed"` (the call itself threw and was caught).
|
|
6
|
-
*
|
|
7
|
-
* `{ isError: true }` (and nothing else) flips the flag; pi's field-by-field merge keeps the original
|
|
8
|
-
* `content` and `details` intact.
|
|
5
|
+
* returned an error result) or `"call_failed"` (the call itself threw and was caught).
|
|
6
|
+
* Spread this onto the returned result without replacing human or structured content.
|
|
9
7
|
*
|
|
10
8
|
* Limited to those two codes: the adapter's other `details.error` values (`auth_required`, connection
|
|
11
9
|
* states, search/validation feedback, ...) are not failed tool calls, so they get no override.
|
package/dist/error-signal.js
CHANGED
|
@@ -1,11 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Classify returned MCP execution failures for Pi's native result-level isError.
|
|
3
3
|
*
|
|
4
4
|
* A failed MCP tool call is *returned* (not thrown), tagged `details.error: "tool_error"` (the server
|
|
5
|
-
* returned an error result) or `"call_failed"` (the call itself threw and was caught).
|
|
6
|
-
*
|
|
7
|
-
* `{ isError: true }` (and nothing else) flips the flag; pi's field-by-field merge keeps the original
|
|
8
|
-
* `content` and `details` intact.
|
|
5
|
+
* returned an error result) or `"call_failed"` (the call itself threw and was caught).
|
|
6
|
+
* Spread this onto the returned result without replacing human or structured content.
|
|
9
7
|
*
|
|
10
8
|
* Limited to those two codes: the adapter's other `details.error` values (`auth_required`, connection
|
|
11
9
|
* states, search/validation feedback, ...) are not failed tool calls, so they get no override.
|
package/dist/index.js
CHANGED
|
@@ -10,7 +10,6 @@ import { executeAuthComplete, executeAuthStart, executeCall, executeConnect, exe
|
|
|
10
10
|
import { formatTerminalError } from "./utils.js";
|
|
11
11
|
import { createOAuthRuntime, shutdownOAuth } from "./mcp-auth-flow.js";
|
|
12
12
|
import { renderMcpProxyToolCall, renderMcpToolResult } from "./tool-result-renderer.js";
|
|
13
|
-
import { toolErrorOverride } from "./error-signal.js";
|
|
14
13
|
import { createMcpRuntimeOwner, createOwnedUi, isAbortError } from "./runtime-owner.js";
|
|
15
14
|
import { publishMcpStatusShutdown } from "./mcp-status.js";
|
|
16
15
|
import { DEFAULT_MCP_SCRIPT_TIMEOUT_MS, runMcpScript } from "./mcp-code.js";
|
|
@@ -20,7 +19,6 @@ import { gatewayParameters, prepareGatewayArguments } from "./gateway-arguments.
|
|
|
20
19
|
import { executeResourceList, executeResourceRead } from "./resource-tools.js";
|
|
21
20
|
import { guardMcpOutput, guardedMcpDetails, readMcpResult, resolveMcpOutputGuardOptions } from "./mcp-output-guard.js";
|
|
22
21
|
import { abortable, throwIfAborted } from "./abort.js";
|
|
23
|
-
import { onSessionCheckpoint, prepareMcpCheckpoint } from "./checkpoint.js";
|
|
24
22
|
export { MCP_STATUS_EVENT, MCP_STATUS_SNAPSHOT_VERSION, } from "./types.js";
|
|
25
23
|
const INIT_WAIT_TIMEOUT_MS = 30_000;
|
|
26
24
|
const INIT_WAIT_TIMED_OUT = Symbol("init-wait-timed-out");
|
|
@@ -269,13 +267,6 @@ function installMcpAdapter(pi, options) {
|
|
|
269
267
|
if (ctx.cwd !== runtimeCwd)
|
|
270
268
|
await startSession(ctx);
|
|
271
269
|
});
|
|
272
|
-
onSessionCheckpoint(pi, async (event) => {
|
|
273
|
-
if (initPromise)
|
|
274
|
-
return { sleepReady: false, reason: "MCP initialization is active" };
|
|
275
|
-
if (!state || !currentOwner?.isActive())
|
|
276
|
-
return { sleepReady: false, reason: "MCP runtime is not initialized" };
|
|
277
|
-
return prepareMcpCheckpoint(state, event);
|
|
278
|
-
});
|
|
279
270
|
pi.on("session_shutdown", async () => {
|
|
280
271
|
// Cached tool selections exist before background server initialization finishes.
|
|
281
272
|
toolLoader.persist();
|
|
@@ -300,8 +291,6 @@ function installMcpAdapter(pi, options) {
|
|
|
300
291
|
if (failures.length)
|
|
301
292
|
throw new AggregateError(failures, "MCP session shutdown persistence/cleanup failed");
|
|
302
293
|
});
|
|
303
|
-
// Re-flag returned MCP tool failures so pi registers them as errors (see toolErrorOverride).
|
|
304
|
-
pi.on("tool_result", (event) => toolErrorOverride(event.details));
|
|
305
294
|
function createCommandContext(ctx) {
|
|
306
295
|
const owner = currentOwner;
|
|
307
296
|
const hasUI = ctx.hasUI;
|
package/dist/init.js
CHANGED
|
@@ -43,7 +43,6 @@ export function recordFailure(state, serverName, message) {
|
|
|
43
43
|
getFailureExpiryTimers(state).delete(serverName);
|
|
44
44
|
return;
|
|
45
45
|
}
|
|
46
|
-
state.owner.beforeActivity();
|
|
47
46
|
if (state.failureTracker.get(serverName) === failedAt) {
|
|
48
47
|
state.failureTracker.delete(serverName);
|
|
49
48
|
state.failureMessages?.delete(serverName);
|
package/dist/lifecycle.d.ts
CHANGED
|
@@ -15,10 +15,6 @@ export declare class McpLifecycleManager {
|
|
|
15
15
|
private activeHealthCheck;
|
|
16
16
|
private shutdownPromise;
|
|
17
17
|
private stopped;
|
|
18
|
-
private checkpointPauses;
|
|
19
|
-
hasActiveHealthCheck(): boolean;
|
|
20
|
-
/** Skip ticks, without cancelling accepted work or permanently closing the manager. */
|
|
21
|
-
pauseForCheckpoint(): () => void;
|
|
22
18
|
private healthSignal;
|
|
23
19
|
private removeHealthAbortListener;
|
|
24
20
|
private readonly manager;
|
package/dist/lifecycle.js
CHANGED
|
@@ -14,19 +14,6 @@ export class McpLifecycleManager {
|
|
|
14
14
|
activeHealthCheck;
|
|
15
15
|
shutdownPromise;
|
|
16
16
|
stopped = false;
|
|
17
|
-
checkpointPauses = 0;
|
|
18
|
-
hasActiveHealthCheck() { return this.activeHealthCheck !== undefined; }
|
|
19
|
-
/** Skip ticks, without cancelling accepted work or permanently closing the manager. */
|
|
20
|
-
pauseForCheckpoint() {
|
|
21
|
-
this.checkpointPauses++;
|
|
22
|
-
let released = false;
|
|
23
|
-
return () => {
|
|
24
|
-
if (!released) {
|
|
25
|
-
released = true;
|
|
26
|
-
this.checkpointPauses--;
|
|
27
|
-
}
|
|
28
|
-
};
|
|
29
|
-
}
|
|
30
17
|
healthSignal;
|
|
31
18
|
removeHealthAbortListener;
|
|
32
19
|
manager;
|
|
@@ -81,7 +68,7 @@ export class McpLifecycleManager {
|
|
|
81
68
|
signal?.addEventListener("abort", stop, { once: true });
|
|
82
69
|
this.removeHealthAbortListener = () => signal?.removeEventListener("abort", stop);
|
|
83
70
|
this.healthCheckInterval = setInterval(() => {
|
|
84
|
-
if (this.stopped || signal?.aborted || this.activeHealthCheck
|
|
71
|
+
if (this.stopped || signal?.aborted || this.activeHealthCheck)
|
|
85
72
|
return;
|
|
86
73
|
const check = this.checkConnections(signal)
|
|
87
74
|
.catch(error => {
|
package/dist/mcp-auth-flow.d.ts
CHANGED
|
@@ -9,7 +9,6 @@ import { type AuthStorageOptions } from "./mcp-auth.ts";
|
|
|
9
9
|
import { type ServerEntry } from "./types.ts";
|
|
10
10
|
/** Auth status for a server */
|
|
11
11
|
export type AuthStatus = "authenticated" | "expired" | "not_authenticated";
|
|
12
|
-
import type { McpCheckpointEvent } from "./checkpoint.ts";
|
|
13
12
|
export interface McpOAuthRuntime {
|
|
14
13
|
readonly signal: AbortSignal;
|
|
15
14
|
}
|
|
@@ -20,8 +19,6 @@ export interface AuthenticateOptions {
|
|
|
20
19
|
runtime?: McpOAuthRuntime;
|
|
21
20
|
}
|
|
22
21
|
export declare function createOAuthRuntime(signal?: AbortSignal): McpOAuthRuntime;
|
|
23
|
-
export declare function holdOAuthCheckpoint(runtime: McpOAuthRuntime, event: McpCheckpointEvent): () => void;
|
|
24
|
-
export declare function getOAuthCheckpointBlocker(runtime: McpOAuthRuntime): string | undefined;
|
|
25
22
|
type OAuthRequest = {
|
|
26
23
|
serverName: string;
|
|
27
24
|
serverUrl: string;
|
|
@@ -46,8 +43,7 @@ export declare function extractOAuthConfig(definition: ServerEntry): McpOAuthCon
|
|
|
46
43
|
* Start OAuth authentication flow for a server.
|
|
47
44
|
* Returns the authorization URL when browser authorization is required.
|
|
48
45
|
*/
|
|
49
|
-
export declare
|
|
50
|
-
declare function startAuthOperation(serverName: string, serverUrl: string, definition?: ServerEntry, options?: AuthenticateOptions): Promise<{
|
|
46
|
+
export declare function startAuth(serverName: string, serverUrl: string, definition?: ServerEntry, options?: AuthenticateOptions): Promise<{
|
|
51
47
|
authorizationUrl: string;
|
|
52
48
|
}>;
|
|
53
49
|
/** Authorization code plus the optional RFC 9207 `iss` callback parameter. */
|
|
@@ -74,8 +70,7 @@ export declare function completeAuthFromInput(serverName: string, input?: string
|
|
|
74
70
|
/**
|
|
75
71
|
* Complete OAuth authentication with an explicit code or a captured callback.
|
|
76
72
|
*/
|
|
77
|
-
export declare
|
|
78
|
-
declare function completeAuthOperation(serverName: string, authorizationCode?: string | AuthorizationResponseInput, options?: AuthenticateOptions): Promise<AuthStatus>;
|
|
73
|
+
export declare function completeAuth(serverName: string, authorizationCode?: string | AuthorizationResponseInput, options?: AuthenticateOptions): Promise<AuthStatus>;
|
|
79
74
|
/**
|
|
80
75
|
* Perform the complete OAuth authentication flow for a server.
|
|
81
76
|
*
|
|
@@ -90,8 +85,7 @@ export declare function authenticate(serverName: string, serverUrl: string, defi
|
|
|
90
85
|
*
|
|
91
86
|
* @param serverName - The name of the MCP server
|
|
92
87
|
*/
|
|
93
|
-
export declare
|
|
94
|
-
declare function removeAuthOperation(serverName: string, options?: AuthenticateOptions): Promise<void>;
|
|
88
|
+
export declare function removeAuth(serverName: string, options?: AuthenticateOptions): Promise<void>;
|
|
95
89
|
/**
|
|
96
90
|
* Check if OAuth is supported for a server configuration.
|
|
97
91
|
* OAuth is supported for HTTP servers unless explicitly disabled.
|
package/dist/mcp-auth-flow.js
CHANGED
|
@@ -42,7 +42,6 @@ export function createOAuthRuntime(signal) {
|
|
|
42
42
|
runtimeStates.set(runtime, {
|
|
43
43
|
controller,
|
|
44
44
|
generation: 0,
|
|
45
|
-
activeOperations: 0,
|
|
46
45
|
pendingAuths: new Map(),
|
|
47
46
|
pendingAuthStates: new Map(),
|
|
48
47
|
pendingAuthCleanupTimers: new Map(),
|
|
@@ -71,36 +70,6 @@ function getRuntimeState(runtime) {
|
|
|
71
70
|
throw new Error("Unknown OAuth runtime");
|
|
72
71
|
return state;
|
|
73
72
|
}
|
|
74
|
-
export function holdOAuthCheckpoint(runtime, event) {
|
|
75
|
-
const state = getRuntimeState(runtime);
|
|
76
|
-
if (state.checkpoint)
|
|
77
|
-
throw new Error("MCP OAuth checkpoint is already held");
|
|
78
|
-
state.checkpoint = event;
|
|
79
|
-
return () => { if (state.checkpoint === event)
|
|
80
|
-
state.checkpoint = undefined; };
|
|
81
|
-
}
|
|
82
|
-
export function getOAuthCheckpointBlocker(runtime) {
|
|
83
|
-
const state = getRuntimeState(runtime);
|
|
84
|
-
if (state.activeOperations || state.pendingAuthentications.size)
|
|
85
|
-
return "MCP OAuth operation is active";
|
|
86
|
-
if (state.pendingAuths.size || state.pendingAuthStates.size || state.pendingAuthCleanupTimers.size)
|
|
87
|
-
return "MCP browser OAuth callback/flow is pending";
|
|
88
|
-
// requests caches server challenges/discovery, not a pending user callback.
|
|
89
|
-
// Cold connect re-challenges; granted scopes and issuer bindings live in the
|
|
90
|
-
// native credential store. Actual interactive/auth operations veto above.
|
|
91
|
-
return undefined;
|
|
92
|
-
}
|
|
93
|
-
async function withOAuthOperation(runtime, run) {
|
|
94
|
-
const state = getRuntimeState(runtime);
|
|
95
|
-
state.checkpoint?.invalidate();
|
|
96
|
-
state.activeOperations++;
|
|
97
|
-
try {
|
|
98
|
-
return await run();
|
|
99
|
-
}
|
|
100
|
-
finally {
|
|
101
|
-
state.activeOperations--;
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
73
|
function getPendingAuthKey(serverName, options) {
|
|
105
74
|
return JSON.stringify([serverName, getAuthBaseDir(options)]);
|
|
106
75
|
}
|
|
@@ -118,7 +87,6 @@ export function getOAuthRequest(serverName, serverUrl, storageBase, runtime = le
|
|
|
118
87
|
export function recordOAuthChallenge(serverName, serverUrl, storageBase, error, runtime = legacyRuntime) {
|
|
119
88
|
if (runtime.signal.aborted || !isOAuthChallenge(error))
|
|
120
89
|
return;
|
|
121
|
-
getRuntimeState(runtime).checkpoint?.invalidate();
|
|
122
90
|
const previous = getOAuthRequest(serverName, serverUrl, storageBase, runtime);
|
|
123
91
|
const request = { ...previous, serverName, serverUrl };
|
|
124
92
|
if (error instanceof InsufficientScopeError) {
|
|
@@ -315,8 +283,7 @@ function parseOAuthRedirectUri(redirectUri) {
|
|
|
315
283
|
* Start OAuth authentication flow for a server.
|
|
316
284
|
* Returns the authorization URL when browser authorization is required.
|
|
317
285
|
*/
|
|
318
|
-
export
|
|
319
|
-
async function startAuthOperation(serverName, serverUrl, definition, options = {}) {
|
|
286
|
+
export async function startAuth(serverName, serverUrl, definition, options = {}) {
|
|
320
287
|
if (isServerDisabled(definition))
|
|
321
288
|
throw new Error(`MCP server "${serverName}" is disabled`);
|
|
322
289
|
const runtime = getRuntime(options);
|
|
@@ -479,10 +446,7 @@ async function setPendingAuth(runtime, serverName, pendingAuth, oauthState, sign
|
|
|
479
446
|
cleanupTimer.unref?.();
|
|
480
447
|
state.pendingAuthCleanupTimers.set(key, cleanupTimer);
|
|
481
448
|
}
|
|
482
|
-
function clearPendingAuth(
|
|
483
|
-
return withOAuthOperation(args[0], () => clearPendingAuthOperation(...args));
|
|
484
|
-
}
|
|
485
|
-
async function clearPendingAuthOperation(runtime, serverName, oauthState, fallbackStorageOptions = {}) {
|
|
449
|
+
async function clearPendingAuth(runtime, serverName, oauthState, fallbackStorageOptions = {}) {
|
|
486
450
|
const state = getRuntimeState(runtime);
|
|
487
451
|
const key = getPendingAuthKey(serverName, fallbackStorageOptions);
|
|
488
452
|
const pendingAuth = state.pendingAuths.get(key);
|
|
@@ -576,8 +540,7 @@ export async function completeAuthFromInput(serverName, input, options = {}) {
|
|
|
576
540
|
/**
|
|
577
541
|
* Complete OAuth authentication with an explicit code or a captured callback.
|
|
578
542
|
*/
|
|
579
|
-
export
|
|
580
|
-
async function completeAuthOperation(serverName, authorizationCode, options = {}) {
|
|
543
|
+
export async function completeAuth(serverName, authorizationCode, options = {}) {
|
|
581
544
|
const runtime = getRuntime(options);
|
|
582
545
|
const runtimeState = getRuntimeState(runtime);
|
|
583
546
|
const fallbackAuthStorageOptions = options.authStorageOptions ?? {};
|
|
@@ -743,8 +706,7 @@ export async function authenticate(serverName, serverUrl, definition, options =
|
|
|
743
706
|
*
|
|
744
707
|
* @param serverName - The name of the MCP server
|
|
745
708
|
*/
|
|
746
|
-
export
|
|
747
|
-
async function removeAuthOperation(serverName, options = {}) {
|
|
709
|
+
export async function removeAuth(serverName, options = {}) {
|
|
748
710
|
const runtime = getRuntime(options);
|
|
749
711
|
const signal = combineAbortSignals(runtime.signal, options.signal);
|
|
750
712
|
throwIfAborted(signal);
|
|
@@ -811,7 +773,6 @@ export async function shutdownOAuth(runtime = legacyRuntime) {
|
|
|
811
773
|
const state = getRuntimeState(runtime);
|
|
812
774
|
if (state.controller.signal.aborted)
|
|
813
775
|
return;
|
|
814
|
-
state.checkpoint?.invalidate();
|
|
815
776
|
state.generation += 1;
|
|
816
777
|
state.controller.abort(new Error("OAuth runtime stopped"));
|
|
817
778
|
for (const callbackState of Array.from(state.pendingAuthStates.values()))
|
package/dist/mcp-code.js
CHANGED
|
@@ -98,7 +98,6 @@ class McpScriptCaptureError extends Error {
|
|
|
98
98
|
}
|
|
99
99
|
export const runMcpScript = async (...args) => {
|
|
100
100
|
const state = args[0];
|
|
101
|
-
state.owner?.beforeActivity();
|
|
102
101
|
state.activeScripts = (state.activeScripts ?? 0) + 1;
|
|
103
102
|
try {
|
|
104
103
|
return await runMcpScriptOperation(...args);
|
|
@@ -78,6 +78,11 @@ export declare function guardMcpOutput(content: ContentBlock[], options?: McpOut
|
|
|
78
78
|
export declare function retainMcpResult(result: unknown, options?: McpOutputGuardOptions, rawResult?: boolean): Promise<RetainedMcpResult>;
|
|
79
79
|
export declare function formatMcpPayloadFile(file: McpPayloadFile): string;
|
|
80
80
|
export declare function formatMcpResultReference(ref: string): string;
|
|
81
|
+
/** Stream histories through the same private output-artifact and paged-reader boundary. */
|
|
82
|
+
export declare function saveMcpOutputStream(chunks: AsyncIterable<string>, outputDirectory?: string): Promise<{
|
|
83
|
+
path?: string;
|
|
84
|
+
error?: string;
|
|
85
|
+
}>;
|
|
81
86
|
export interface ReadMcpResultInput {
|
|
82
87
|
ref: string;
|
|
83
88
|
/** RFC 6901 JSON Pointer. Omit to read the entire retained result. */
|
package/dist/mcp-output-guard.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { randomBytes } from "node:crypto";
|
|
2
|
-
import { mkdir, mkdtemp, readFile, realpath, writeFile } from "node:fs/promises";
|
|
2
|
+
import { mkdir, mkdtemp, open, readFile, realpath, writeFile } from "node:fs/promises";
|
|
3
|
+
import { createReadStream } from "node:fs";
|
|
3
4
|
import { tmpdir } from "node:os";
|
|
4
5
|
import { basename, dirname, join, resolve } from "node:path";
|
|
5
6
|
export const DEFAULT_MCP_OUTPUT_MAX_BYTES = 50 * 1024;
|
|
@@ -320,6 +321,26 @@ async function saveArtifact(kind, text, outputDirectory) {
|
|
|
320
321
|
return { error: error instanceof Error ? error.message : String(error) };
|
|
321
322
|
}
|
|
322
323
|
}
|
|
324
|
+
/** Stream histories through the same private output-artifact and paged-reader boundary. */
|
|
325
|
+
export async function saveMcpOutputStream(chunks, outputDirectory) {
|
|
326
|
+
const artifact = await saveArtifact("output", "", outputDirectory);
|
|
327
|
+
if (!artifact.path)
|
|
328
|
+
return artifact;
|
|
329
|
+
try {
|
|
330
|
+
const file = await open(artifact.path, "a");
|
|
331
|
+
try {
|
|
332
|
+
for await (const chunk of chunks)
|
|
333
|
+
await file.writeFile(chunk);
|
|
334
|
+
}
|
|
335
|
+
finally {
|
|
336
|
+
await file.close();
|
|
337
|
+
}
|
|
338
|
+
return artifact;
|
|
339
|
+
}
|
|
340
|
+
catch (error) {
|
|
341
|
+
return { error: error instanceof Error ? error.message : String(error) };
|
|
342
|
+
}
|
|
343
|
+
}
|
|
323
344
|
export async function readMcpResult(input, options = {}) {
|
|
324
345
|
try {
|
|
325
346
|
if (!input || typeof input.ref !== "string")
|
|
@@ -330,6 +351,39 @@ export async function readMcpResult(input, options = {}) {
|
|
|
330
351
|
if (dirname(dirname(ref)) !== root || !/^pi-mcp-output-/.test(basename(dirname(ref)))
|
|
331
352
|
|| !/^(mcp-result|output)-[a-f0-9]+\.txt$/.test(basename(ref)))
|
|
332
353
|
throw new Error("ref is not an MCP output artifact in this host's output directory");
|
|
354
|
+
// Plain output pages without flattening a complete UI history into memory.
|
|
355
|
+
if (basename(ref).startsWith("output-") && input.path === undefined && input.fields === undefined) {
|
|
356
|
+
const offset = input.offset ?? 0;
|
|
357
|
+
const limit = input.limit ?? 12_000;
|
|
358
|
+
if (!Number.isSafeInteger(offset) || offset < 0 || !Number.isSafeInteger(limit) || limit < 1)
|
|
359
|
+
throw new Error("offset must be a non-negative integer and limit a positive integer");
|
|
360
|
+
const maxBytes = Math.max(1, (options.maxBytes ?? DEFAULT_MCP_OUTPUT_MAX_BYTES) - 256);
|
|
361
|
+
const maxLines = Math.max(1, (options.maxLines ?? DEFAULT_MCP_OUTPUT_MAX_LINES) - 1);
|
|
362
|
+
let total = 0, bytes = 0, lines = 1;
|
|
363
|
+
let page = "";
|
|
364
|
+
let finished = false;
|
|
365
|
+
for await (const chunk of createReadStream(ref, { encoding: "utf8" })) {
|
|
366
|
+
for (const char of chunk) {
|
|
367
|
+
if (total < offset && total + char.length > offset)
|
|
368
|
+
throw new Error("offset must not split a Unicode surrogate pair");
|
|
369
|
+
if (total >= offset && !finished) {
|
|
370
|
+
const nextBytes = bytes + byteLength(char);
|
|
371
|
+
const nextLines = lines + (char === "\n" ? 1 : 0);
|
|
372
|
+
if (page && (nextBytes > maxBytes || nextLines > maxLines || page.length + char.length > limit))
|
|
373
|
+
finished = true;
|
|
374
|
+
else {
|
|
375
|
+
page += char;
|
|
376
|
+
bytes = nextBytes;
|
|
377
|
+
lines = nextLines;
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
total += char.length;
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
const end = offset + page.length;
|
|
384
|
+
const nextOffset = end < total ? end : null;
|
|
385
|
+
return { content: [{ type: "text", text: page }, ...(nextOffset !== null ? [{ type: "text", text: `[MCP result page: ${offset}–${end} of ${total} characters. Continue with offset: ${end}.]` }] : [])], details: { ref, offset, nextOffset, totalCharacters: total } };
|
|
386
|
+
}
|
|
333
387
|
let rendered = await readFile(ref, "utf8");
|
|
334
388
|
if (input.path !== undefined || input.fields !== undefined || basename(ref).startsWith("mcp-result-")) {
|
|
335
389
|
let value = JSON.parse(rendered);
|
package/dist/mcp-setup-panel.js
CHANGED
package/dist/proxy-modes.d.ts
CHANGED
|
@@ -4,7 +4,7 @@ import type { ServerConnection } from "./server-manager.ts";
|
|
|
4
4
|
import type { ToolMetadata, McpOperationContext, McpToolCallIdentity } from "./types.ts";
|
|
5
5
|
import { type SessionRecoveryDeps } from "./session-recovery.ts";
|
|
6
6
|
type ProxyToolResult = AgentToolResult<Record<string, unknown>>;
|
|
7
|
-
export declare function executeUiMessages(state: McpExtensionState): ProxyToolResult
|
|
7
|
+
export declare function executeUiMessages(state: McpExtensionState): Promise<ProxyToolResult>;
|
|
8
8
|
export declare function executeStatus(state: McpExtensionState): ProxyToolResult;
|
|
9
9
|
export declare function executeAuthStart(state: McpExtensionState, serverName: string, signal?: AbortSignal): Promise<ProxyToolResult>;
|
|
10
10
|
export declare function executeAuthComplete(state: McpExtensionState, serverName: string, input?: string, signal?: AbortSignal): Promise<ProxyToolResult>;
|
package/dist/proxy-modes.js
CHANGED
|
@@ -6,13 +6,14 @@ import { combineAbortSignals, isAbortError } from "./runtime-owner.js";
|
|
|
6
6
|
import { buildToolMetadata, catalogCoverage, findToolByName, formatSchema, toToolDescriptor } from "./tool-metadata.js";
|
|
7
7
|
import { reconstructPromptMetadata } from "./metadata-cache.js";
|
|
8
8
|
import { renderMcpResultContent } from "./tool-registrar.js";
|
|
9
|
-
import { guardMcpOutput, guardedMcpDetails, retainMcpResult, resolveMcpOutputGuardOptions, formatMcpResultReference } from "./mcp-output-guard.js";
|
|
9
|
+
import { guardMcpOutput, guardedMcpDetails, retainMcpResult, resolveMcpOutputGuardOptions, formatMcpResultReference, saveMcpOutputStream } from "./mcp-output-guard.js";
|
|
10
10
|
import { maybeStartUiSession, summarizeUiSessionResult } from "./ui-session.js";
|
|
11
11
|
import { formatAuthRequiredMessage, formatMcpStatus, resolveServerUrl, truncateAtWord } from "./utils.js";
|
|
12
12
|
import { authenticate, completeAuthFromInput, startAuth, supportsOAuth } from "./mcp-auth-flow.js";
|
|
13
13
|
import { isInterruptedToolCall, isToolTransportFailure, trackToolCallOutcome, SessionRecoveryAuthRequiredError, withSessionRecovery } from "./session-recovery.js";
|
|
14
14
|
import { paginate, rankSuggestions, rankToolMatches } from "./search-ranking.js";
|
|
15
15
|
import { ensureToolCallApproved, isToolCallApprovalRequired } from "./tool-approval.js";
|
|
16
|
+
import { materializeUiMessages, uiMessages } from "./ui-message-history.js";
|
|
16
17
|
const INSTRUCTIONS_PREVIEW_LENGTH = 300;
|
|
17
18
|
function disabledResult(mode, serverName) {
|
|
18
19
|
const message = `Server "${serverName}" is disabled. Run /mcp enable ${serverName} and /reload to enable it.`;
|
|
@@ -120,8 +121,80 @@ async function attemptAutoAuth(state, serverName, signal) {
|
|
|
120
121
|
};
|
|
121
122
|
}
|
|
122
123
|
}
|
|
123
|
-
|
|
124
|
-
|
|
124
|
+
async function* uiSessionText(session) {
|
|
125
|
+
yield `\n## ${session.serverName} / ${session.toolName} (${session.completedAt.toLocaleTimeString()}, ${session.reason})`;
|
|
126
|
+
let heading = false;
|
|
127
|
+
for await (const prompt of uiMessages(session.messages, "prompts")) {
|
|
128
|
+
if (parseUiPromptHandoff(prompt))
|
|
129
|
+
continue;
|
|
130
|
+
if (!heading) {
|
|
131
|
+
yield "\n\n### Prompts:";
|
|
132
|
+
heading = true;
|
|
133
|
+
}
|
|
134
|
+
yield `\n- ${prompt}`;
|
|
135
|
+
}
|
|
136
|
+
heading = false;
|
|
137
|
+
for await (const value of uiMessages(session.messages, "intents")) {
|
|
138
|
+
const intent = value;
|
|
139
|
+
if (!heading) {
|
|
140
|
+
yield "\n\n### Intents:";
|
|
141
|
+
heading = true;
|
|
142
|
+
}
|
|
143
|
+
yield `\n- ${intent.intent}${intent.params ? ` (${JSON.stringify(intent.params)})` : ""}`;
|
|
144
|
+
}
|
|
145
|
+
for await (const prompt of uiMessages(session.messages, "prompts")) {
|
|
146
|
+
const handoff = parseUiPromptHandoff(prompt);
|
|
147
|
+
if (!handoff)
|
|
148
|
+
continue;
|
|
149
|
+
if (!heading) {
|
|
150
|
+
yield "\n\n### Intents:";
|
|
151
|
+
heading = true;
|
|
152
|
+
}
|
|
153
|
+
yield `\n- ${handoff.intent} (${JSON.stringify(handoff.params)})`;
|
|
154
|
+
}
|
|
155
|
+
if (session.messages.contexts.length) {
|
|
156
|
+
yield "\n\n### Context updates:";
|
|
157
|
+
for (const context of session.messages.contexts)
|
|
158
|
+
yield `\n- ${context.summary}${context.truncated ? " (truncated)" : ""}`;
|
|
159
|
+
}
|
|
160
|
+
heading = false;
|
|
161
|
+
for await (const notification of uiMessages(session.messages, "notifications")) {
|
|
162
|
+
if (!heading) {
|
|
163
|
+
yield "\n\n### Notifications:";
|
|
164
|
+
heading = true;
|
|
165
|
+
}
|
|
166
|
+
yield `\n- ${notification}`;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
export async function executeUiMessages(state) {
|
|
170
|
+
const pending = [...state.completedUiSessions];
|
|
171
|
+
const options = resolveMcpOutputGuardOptions(state.config?.settings, state.outputDirectory);
|
|
172
|
+
// Bound both inline output and details. Large retrieval streams; it never flattens all events.
|
|
173
|
+
const size = pending.reduce((bytes, session) => bytes + (session.messages.historyBytes ?? 0) + Buffer.byteLength(JSON.stringify(session.messages)), 0);
|
|
174
|
+
const large = size > Math.min(options.maxBytes, options.detailsMaxBytes);
|
|
175
|
+
const warnings = pending.flatMap(session => session.messages.historyError ? [session.messages.historyError] : []);
|
|
176
|
+
const paged = async () => {
|
|
177
|
+
async function* output() {
|
|
178
|
+
yield `UI Session Messages (${pending.length} sessions):\n`;
|
|
179
|
+
for (const session of pending)
|
|
180
|
+
yield* uiSessionText(session);
|
|
181
|
+
}
|
|
182
|
+
const artifact = await saveMcpOutputStream(output(), state.outputDirectory);
|
|
183
|
+
if (!artifact.path)
|
|
184
|
+
return { content: [{ type: "text", text: `UI messages remain available; retrieval could not be saved: ${artifact.error}` }], isError: true, details: { error: "ui_history_delivery_failed", cleared: false } };
|
|
185
|
+
// Newly completed sessions during I/O are not part of this drain.
|
|
186
|
+
state.completedUiSessions = state.completedUiSessions.filter(session => !pending.includes(session));
|
|
187
|
+
return { content: [{ type: "text", text: `UI Session Messages (${pending.length} sessions). ${formatMcpResultReference(artifact.path)}${warnings.length ? `\nHistory persistence warning: ${warnings.join("; ")}` : ""}` }], details: { sessions: pending.length, resultRef: artifact.path, historyRefs: pending.flatMap(session => session.messages.historyRef ? [session.messages.historyRef] : []), cleared: true } };
|
|
188
|
+
};
|
|
189
|
+
if (large)
|
|
190
|
+
return paged();
|
|
191
|
+
let sessions;
|
|
192
|
+
try {
|
|
193
|
+
sessions = await Promise.all(pending.map(async (session) => ({ ...session, messages: await materializeUiMessages(session.messages) })));
|
|
194
|
+
}
|
|
195
|
+
catch (error) {
|
|
196
|
+
return { content: [{ type: "text", text: `UI messages remain available; history could not be read: ${String(error)}` }], isError: true, details: { error: "ui_history_delivery_failed", cleared: false } };
|
|
197
|
+
}
|
|
125
198
|
if (sessions.length === 0) {
|
|
126
199
|
return {
|
|
127
200
|
content: [{ type: "text", text: "No UI session messages available." }],
|
|
@@ -183,9 +256,8 @@ export function executeUiMessages(state) {
|
|
|
183
256
|
}
|
|
184
257
|
}
|
|
185
258
|
const count = sessions.length;
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
content: [{ type: "text", text: output.join("\n") }],
|
|
259
|
+
const inline = {
|
|
260
|
+
content: [{ type: "text", text: output.join("\n") + (warnings.length ? `\nHistory persistence warning: ${warnings.join("; ")}` : "") }],
|
|
189
261
|
details: {
|
|
190
262
|
sessions: count,
|
|
191
263
|
prompts: allPrompts,
|
|
@@ -195,6 +267,11 @@ export function executeUiMessages(state) {
|
|
|
195
267
|
cleared: true,
|
|
196
268
|
},
|
|
197
269
|
};
|
|
270
|
+
const text = inline.content.map(block => block.type === "text" ? block.text : "").join("\n");
|
|
271
|
+
if (Buffer.byteLength(JSON.stringify(inline.details)) > options.detailsMaxBytes || Buffer.byteLength(text) > options.maxBytes || text.split("\n").length > options.maxLines)
|
|
272
|
+
return paged();
|
|
273
|
+
state.completedUiSessions = state.completedUiSessions.filter(session => !pending.includes(session));
|
|
274
|
+
return inline;
|
|
198
275
|
}
|
|
199
276
|
export function executeStatus(state) {
|
|
200
277
|
const servers = [];
|
|
@@ -699,7 +776,7 @@ export async function runToolCall(state, serverName, target, args, options) {
|
|
|
699
776
|
beforeDispatchPending = false;
|
|
700
777
|
}
|
|
701
778
|
throwIfAborted(callerSignal);
|
|
702
|
-
//
|
|
779
|
+
// Approval and UI preparation do not consume the service deadline.
|
|
703
780
|
const ownedSignal = combineAbortSignals(callerSignal, AbortSignal.timeout(Math.ceil(configuredOptions?.timeout ?? DEFAULT_REQUEST_TIMEOUT_MSEC)));
|
|
704
781
|
const requestOptions = { ...configuredOptions, signal: ownedSignal };
|
|
705
782
|
const recovery = { manager: state.manager, config: state.config, signal: ownedSignal, onNeedsAuth: recoverAuthConnection };
|
|
@@ -735,7 +812,7 @@ export async function runToolCall(state, serverName, target, args, options) {
|
|
|
735
812
|
if (options.raw) {
|
|
736
813
|
const message = isError
|
|
737
814
|
? (Array.isArray(record.content) ? record.content.filter(block => block.type === "text").map(block => block.text).join("\n") : "") || "Tool execution failed" : undefined;
|
|
738
|
-
return { content: [], details: { ...details, ...retained, ...(message ? { message } : {}) } };
|
|
815
|
+
return { content: [], ...(isError ? { isError: true } : {}), details: { ...details, ...retained, ...(message ? { message } : {}) } };
|
|
739
816
|
}
|
|
740
817
|
const content = renderMcpResultContent(record, retained.payloadFiles);
|
|
741
818
|
const guarded = await guardMcpOutput(content.length ? content : [{ type: "text", text: target.resourceUri ? "(empty resource)" : "(empty result)" }], {
|
|
@@ -743,7 +820,7 @@ export async function runToolCall(state, serverName, target, args, options) {
|
|
|
743
820
|
...(isError ? { prefix: "Error: ", suffix: schemaSuffix(), emptyTextFallback: "Tool execution failed" }
|
|
744
821
|
: uiSummary ? { suffix: `\n\n${uiSummary.message}` } : {}),
|
|
745
822
|
});
|
|
746
|
-
return { content: guarded.content, details: { ...details, ...guardedMcpDetails(guarded) } };
|
|
823
|
+
return { content: guarded.content, ...(isError ? { isError: true } : {}), details: { ...details, ...guardedMcpDetails(guarded) } };
|
|
747
824
|
}
|
|
748
825
|
catch (error) {
|
|
749
826
|
// Direct/proxy host errors throw; scripts retain their failed-call envelope.
|
|
@@ -759,6 +836,7 @@ export async function runToolCall(state, serverName, target, args, options) {
|
|
|
759
836
|
? await retainMcpResult(captureFailure.result, { ...outputGuardOptions, enabled: true, detailsMaxBytes: 1 }) : {};
|
|
760
837
|
return {
|
|
761
838
|
content: [{ type: "text", text: message }, ...(retained.resultRef ? [{ type: "text", text: formatMcpResultReference(retained.resultRef) }] : [])],
|
|
839
|
+
isError: true,
|
|
762
840
|
details: { ...detailsBase, ...retained, error: isAbortError(error, callerSignal) ? "aborted" : "call_capture_failed", message, recovery: context },
|
|
763
841
|
};
|
|
764
842
|
}
|
|
@@ -767,6 +845,7 @@ export async function runToolCall(state, serverName, target, args, options) {
|
|
|
767
845
|
uiSession?.sendToolCancelled(message);
|
|
768
846
|
return {
|
|
769
847
|
content: [{ type: "text", text: message }],
|
|
848
|
+
isError: true,
|
|
770
849
|
details: { ...detailsBase, error: "ambiguous_outcome", message, recovery: { ...identity, action: "readback" } },
|
|
771
850
|
};
|
|
772
851
|
}
|
|
@@ -794,6 +873,7 @@ export async function runToolCall(state, serverName, target, args, options) {
|
|
|
794
873
|
const guarded = await guardMcpOutput([{ type: "text", text: message }], { ...outputGuardOptions, prefix: "Failed to call tool: ", suffix: schemaSuffix() });
|
|
795
874
|
return {
|
|
796
875
|
content: guarded.content,
|
|
876
|
+
isError: true,
|
|
797
877
|
details: {
|
|
798
878
|
...detailsBase,
|
|
799
879
|
error: isAbortError(error, callerSignal) ? "aborted" : "call_failed",
|
package/dist/runtime-owner.d.ts
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import type { McpCheckpointEvent } from "./checkpoint.ts";
|
|
2
1
|
import type { ExtensionUIContext } from "@earendil-works/pi-coding-agent";
|
|
3
2
|
export interface McpRuntimeOwner {
|
|
4
3
|
readonly signal: AbortSignal;
|
|
@@ -6,10 +5,7 @@ export interface McpRuntimeOwner {
|
|
|
6
5
|
addCleanup(cleanup: () => void | Promise<void>): void;
|
|
7
6
|
stop(reason?: string): Promise<void>;
|
|
8
7
|
throwIfInactive(): void;
|
|
9
|
-
beforeActivity(): void;
|
|
10
|
-
holdCheckpoint(event: McpCheckpointEvent): () => void;
|
|
11
8
|
runCallback<T>(kind: "beforeExecute" | "onToolCall", run: () => Promise<T>): Promise<T>;
|
|
12
|
-
getCheckpointBlocker(): string | undefined;
|
|
13
9
|
}
|
|
14
10
|
export declare function createMcpRuntimeOwner(): McpRuntimeOwner;
|
|
15
11
|
export declare function combineAbortSignals(...signals: Array<AbortSignal | undefined>): AbortSignal | undefined;
|