@fitchmultz/pi-mcp-adapter 7.1.0 → 8.0.0
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 +27 -0
- package/OAUTH.md +3 -3
- package/README.md +26 -17
- package/dist/app-bridge.bundle.js +13 -13
- 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 +6 -23
- 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/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 +9 -100
- package/dist/tool-loader.d.ts +6 -21
- package/dist/tool-loader.js +76 -61
- 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 +12 -12
- package/dist/checkpoint.d.ts +0 -18
- package/dist/checkpoint.js +0 -53
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;
|
package/dist/runtime-owner.js
CHANGED
|
@@ -3,12 +3,10 @@ export function createMcpRuntimeOwner() {
|
|
|
3
3
|
const controller = new AbortController();
|
|
4
4
|
const cleanups = [];
|
|
5
5
|
let stopPromise;
|
|
6
|
-
let checkpoint;
|
|
7
|
-
const beforeActivity = () => checkpoint?.invalidate();
|
|
8
6
|
// These two host hooks can outlive abortable() at their call sites. Count their underlying
|
|
9
7
|
// promises through settlement, without cancelling, serializing, or replaying callback work.
|
|
10
8
|
const callbacks = { beforeExecute: 0, onToolCall: 0 };
|
|
11
|
-
const
|
|
9
|
+
const pendingCallback = () => {
|
|
12
10
|
if (callbacks.beforeExecute)
|
|
13
11
|
return "MCP beforeExecute callback is pending";
|
|
14
12
|
if (callbacks.onToolCall)
|
|
@@ -20,10 +18,7 @@ export function createMcpRuntimeOwner() {
|
|
|
20
18
|
};
|
|
21
19
|
return {
|
|
22
20
|
signal: controller.signal,
|
|
23
|
-
beforeActivity,
|
|
24
|
-
getCheckpointBlocker,
|
|
25
21
|
runCallback: async (kind, run) => {
|
|
26
|
-
beforeActivity();
|
|
27
22
|
// Invocation/cancellation policy stays at the call site. In particular, an accepted
|
|
28
23
|
// tool's outcome callback may still need to persist its result during shutdown.
|
|
29
24
|
callbacks[kind]++;
|
|
@@ -34,13 +29,6 @@ export function createMcpRuntimeOwner() {
|
|
|
34
29
|
callbacks[kind]--;
|
|
35
30
|
}
|
|
36
31
|
},
|
|
37
|
-
holdCheckpoint: event => {
|
|
38
|
-
if (checkpoint)
|
|
39
|
-
throw new Error("MCP checkpoint is already held");
|
|
40
|
-
checkpoint = event;
|
|
41
|
-
return () => { if (checkpoint === event)
|
|
42
|
-
checkpoint = undefined; };
|
|
43
|
-
},
|
|
44
32
|
isActive: () => !controller.signal.aborted,
|
|
45
33
|
addCleanup: cleanup => {
|
|
46
34
|
if (controller.signal.aborted) {
|
|
@@ -52,14 +40,13 @@ export function createMcpRuntimeOwner() {
|
|
|
52
40
|
stop: (reason = "MCP extension runtime stopped") => {
|
|
53
41
|
if (stopPromise)
|
|
54
42
|
return stopPromise;
|
|
55
|
-
beforeActivity();
|
|
56
43
|
controller.abort(new Error(reason));
|
|
57
44
|
const pendingCleanups = cleanups.splice(0).reverse().map(cleanup => Promise.resolve().then(cleanup));
|
|
58
45
|
stopPromise = Promise.allSettled(pendingCleanups).then(results => {
|
|
59
46
|
const failures = results.flatMap(result => result.status === "rejected" ? [result.reason] : []);
|
|
60
|
-
const
|
|
61
|
-
if (
|
|
62
|
-
failures.push(new Error(
|
|
47
|
+
const callback = pendingCallback();
|
|
48
|
+
if (callback)
|
|
49
|
+
failures.push(new Error(callback));
|
|
63
50
|
if (failures.length > 0) {
|
|
64
51
|
const aggregate = new AggregateError(failures, "MCP runtime cleanup failed");
|
|
65
52
|
console.error(`MCP: runtime cleanup failed: ${formatTerminalError(aggregate)}`);
|
|
@@ -68,7 +55,7 @@ export function createMcpRuntimeOwner() {
|
|
|
68
55
|
});
|
|
69
56
|
return stopPromise;
|
|
70
57
|
},
|
|
71
|
-
throwIfInactive: () => {
|
|
58
|
+
throwIfInactive: () => { controller.signal.throwIfAborted(); },
|
|
72
59
|
};
|
|
73
60
|
}
|
|
74
61
|
export function combineAbortSignals(...signals) {
|
package/dist/server-manager.d.ts
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import type { McpCheckpointEvent } from "./checkpoint.ts";
|
|
2
1
|
import { Client, type RequestOptions, type GetPromptResult, type ReadResourceResult, type UrlElicitationRequiredError } from "@modelcontextprotocol/client";
|
|
3
2
|
import { type McpTool, type McpResource, type McpPrompt, type ServerDefinition, type ServerStreamResultPatchNotification, type Transport, type McpTraceSettings } from "./types.ts";
|
|
4
3
|
import { type McpOAuthRuntime } from "./mcp-auth-flow.ts";
|
|
@@ -45,15 +44,9 @@ export declare class McpServerManager {
|
|
|
45
44
|
private traceSettings;
|
|
46
45
|
private traceWriter;
|
|
47
46
|
private stopped;
|
|
48
|
-
private checkpoint;
|
|
49
|
-
private readonly beforeActivity;
|
|
50
|
-
holdCheckpoint(event: McpCheckpointEvent): () => void;
|
|
51
|
-
getCheckpointBlocker(): string | undefined;
|
|
52
|
-
flushForCheckpoint(): Promise<void>;
|
|
53
47
|
private releaseClientWhenSettled;
|
|
54
|
-
/**
|
|
55
|
-
|
|
56
|
-
private fenceTransport;
|
|
48
|
+
/** Own native send/auth tails even after a public request has been cancelled. */
|
|
49
|
+
private ownTransportSends;
|
|
57
50
|
/** Default cwd for stdio servers without an explicit config `cwd`. */
|
|
58
51
|
private readonly defaultCwd;
|
|
59
52
|
constructor(defaultCwd?: string);
|
package/dist/server-manager.js
CHANGED
|
@@ -41,8 +41,6 @@ async function drainClientOperations(client) {
|
|
|
41
41
|
class ManagedClient extends Client {
|
|
42
42
|
serverName = "";
|
|
43
43
|
oauthProvider;
|
|
44
|
-
beforeActivity = () => { };
|
|
45
|
-
resourceSubscriptions = new Set();
|
|
46
44
|
own(operation, kind) {
|
|
47
45
|
let pending = clientOperations.get(this);
|
|
48
46
|
if (!pending)
|
|
@@ -53,21 +51,17 @@ class ManagedClient extends Client {
|
|
|
53
51
|
return operation;
|
|
54
52
|
}
|
|
55
53
|
ownNative(run) {
|
|
56
|
-
this.beforeActivity();
|
|
57
54
|
return this.own(run(), "request/refresh");
|
|
58
55
|
}
|
|
59
56
|
ownSend(run) {
|
|
60
|
-
this.beforeActivity();
|
|
61
57
|
// SDK-generated callback replies also perform HTTP/auth work, without calling callTool().
|
|
62
58
|
return this.own(Promise.resolve().then(run), "request/refresh");
|
|
63
59
|
}
|
|
64
60
|
ownCallback(kind, run) {
|
|
65
|
-
this.beforeActivity();
|
|
66
61
|
// Insert before dispatch. Cancellation/close of the SDK response does not settle this promise.
|
|
67
62
|
return this.own(Promise.resolve().then(run), kind);
|
|
68
63
|
}
|
|
69
64
|
async track(start, signal) {
|
|
70
|
-
this.beforeActivity();
|
|
71
65
|
const controller = this.oauthProvider ? new AbortController() : undefined;
|
|
72
66
|
let removeAbortListener = () => { };
|
|
73
67
|
const operation = this.oauthProvider
|
|
@@ -119,19 +113,11 @@ class ManagedClient extends Client {
|
|
|
119
113
|
subscribeResource(...args) {
|
|
120
114
|
return this.track(async (signal) => {
|
|
121
115
|
throwIfAborted(signal);
|
|
122
|
-
// A cancelled/lost response does not prove the server rejected the subscription.
|
|
123
|
-
// Retain the attempted URI until an explicit unsubscribe succeeds or the connection is discarded.
|
|
124
|
-
if (this.getServerCapabilities()?.resources?.subscribe)
|
|
125
|
-
this.resourceSubscriptions.add(args[0].uri);
|
|
126
116
|
return super.subscribeResource(args[0], { ...args[1], ...(signal ? { signal } : {}) });
|
|
127
117
|
}, args[1]?.signal);
|
|
128
118
|
}
|
|
129
119
|
unsubscribeResource(...args) {
|
|
130
|
-
return this.track(
|
|
131
|
-
const result = await super.unsubscribeResource(args[0], { ...args[1], ...(signal ? { signal } : {}) });
|
|
132
|
-
this.resourceSubscriptions.delete(args[0].uri);
|
|
133
|
-
return result;
|
|
134
|
-
}, args[1]?.signal);
|
|
120
|
+
return this.track(signal => super.unsubscribeResource(args[0], { ...args[1], ...(signal ? { signal } : {}) }), args[1]?.signal);
|
|
135
121
|
}
|
|
136
122
|
}
|
|
137
123
|
function boundedStderrChunk(chunk) {
|
|
@@ -184,85 +170,15 @@ export class McpServerManager {
|
|
|
184
170
|
traceSettings;
|
|
185
171
|
traceWriter;
|
|
186
172
|
stopped = false;
|
|
187
|
-
checkpoint;
|
|
188
|
-
beforeActivity = () => this.checkpoint?.invalidate();
|
|
189
|
-
holdCheckpoint(event) {
|
|
190
|
-
if (this.checkpoint)
|
|
191
|
-
throw new Error("MCP manager checkpoint is already held");
|
|
192
|
-
this.checkpoint = event;
|
|
193
|
-
return () => { if (this.checkpoint === event)
|
|
194
|
-
this.checkpoint = undefined; };
|
|
195
|
-
}
|
|
196
|
-
getCheckpointBlocker() {
|
|
197
|
-
if (this.stopped)
|
|
198
|
-
return "MCP manager is stopped";
|
|
199
|
-
if (this.connectPromises.size || this.reconnectPromises.size || this.closePromises.size || this.retiredConnections.size)
|
|
200
|
-
return "MCP connection/discovery/retirement is active";
|
|
201
|
-
if (this.acceptedUrlElicitations.size || this.uiStreamListeners.size)
|
|
202
|
-
return "MCP browser elicitation/UI stream is pending";
|
|
203
|
-
for (const client of this.ownedClients) {
|
|
204
|
-
// Only the SDK's exact auto-opened catalog handle reconstructs during connect().
|
|
205
|
-
// An explicit consumer listen(), even with the same filter, still owns private state.
|
|
206
|
-
const kinds = [...(clientOperations.get(client) ?? [])]
|
|
207
|
-
.filter(([operation, kind]) => kind !== "subscription" || operation !== client.autoOpenedSubscription?.closed)
|
|
208
|
-
.map(([, kind]) => kind);
|
|
209
|
-
const callback = kinds.find(kind => kind === "sampling callback" || kind === "elicitation callback");
|
|
210
|
-
if (callback)
|
|
211
|
-
return `MCP ${client.serverName}: ${callback} is active`;
|
|
212
|
-
if (kinds.includes("request/refresh"))
|
|
213
|
-
return `MCP ${client.serverName}: request/refresh is active`;
|
|
214
|
-
if (kinds.includes("subscription"))
|
|
215
|
-
return `MCP ${client.serverName}: subscription is active`;
|
|
216
|
-
}
|
|
217
|
-
for (const [name, connection] of this.connections) {
|
|
218
|
-
if (connection.inFlight)
|
|
219
|
-
return `MCP ${name}: request/refresh is active`;
|
|
220
|
-
if (connection.client instanceof ManagedClient && connection.client.resourceSubscriptions.size)
|
|
221
|
-
return `MCP ${name}: resource subscription is active or unresolved`;
|
|
222
|
-
if (!(connection.transport instanceof StreamableHTTPClientTransport))
|
|
223
|
-
return `MCP ${name}: only stateless Streamable HTTP is checkpoint-supported`;
|
|
224
|
-
if (connection.transport.sessionId !== undefined)
|
|
225
|
-
return `MCP ${name}: HTTP session state is not reconstructible`;
|
|
226
|
-
const capabilities = connection.client.getServerCapabilities();
|
|
227
|
-
if (capabilities?.tasks)
|
|
228
|
-
return `MCP ${name}: negotiated remote task state is not checkpoint-supported`;
|
|
229
|
-
const opaque = Object.keys(capabilities ?? {}).find(key => !["tools", "resources", "prompts", "logging", "completions"].includes(key)
|
|
230
|
-
&& !(key === "experimental" && Object.keys(capabilities?.experimental ?? {}).length === 0));
|
|
231
|
-
if (opaque)
|
|
232
|
-
return `MCP ${name}: unsupported negotiated capability ${opaque}`;
|
|
233
|
-
}
|
|
234
|
-
return undefined;
|
|
235
|
-
}
|
|
236
|
-
// Diagnostics are best-effort, not session authority or a clean-exit prerequisite.
|
|
237
|
-
async flushForCheckpoint() { await this.traceWriter?.flush(); }
|
|
238
173
|
releaseClientWhenSettled(client) {
|
|
239
174
|
if (client instanceof ManagedClient)
|
|
240
175
|
void drainClientOperations(client).then(() => this.ownedClients.delete(client));
|
|
241
176
|
}
|
|
242
|
-
/**
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
for (const key of ["onmessage", "onerror", "onclose"]) {
|
|
246
|
-
const descriptor = Object.getOwnPropertyDescriptor(transport, key);
|
|
247
|
-
let value = transport[key];
|
|
248
|
-
Object.defineProperty(transport, key, {
|
|
249
|
-
configurable: true,
|
|
250
|
-
get: () => {
|
|
251
|
-
const handler = descriptor?.get ? descriptor.get.call(transport) : value;
|
|
252
|
-
return handler ? (...args) => { this.beforeActivity(); return handler(...args); } : undefined;
|
|
253
|
-
},
|
|
254
|
-
set: next => { if (descriptor?.set)
|
|
255
|
-
descriptor.set.call(transport, next);
|
|
256
|
-
else
|
|
257
|
-
value = next; },
|
|
258
|
-
});
|
|
259
|
-
}
|
|
260
|
-
if (typeof transport.send === "function") {
|
|
177
|
+
/** Own native send/auth tails even after a public request has been cancelled. */
|
|
178
|
+
ownTransportSends(transport, client) {
|
|
179
|
+
if (typeof transport.send === "function" && client instanceof ManagedClient) {
|
|
261
180
|
const send = transport.send.bind(transport);
|
|
262
|
-
transport.send = (...args) =>
|
|
263
|
-
this.beforeActivity();
|
|
264
|
-
return client instanceof ManagedClient ? client.ownSend(() => send(...args)) : send(...args);
|
|
265
|
-
};
|
|
181
|
+
transport.send = (...args) => client.ownSend(() => send(...args));
|
|
266
182
|
}
|
|
267
183
|
}
|
|
268
184
|
/** Default cwd for stdio servers without an explicit config `cwd`. */
|
|
@@ -316,7 +232,6 @@ export class McpServerManager {
|
|
|
316
232
|
};
|
|
317
233
|
}
|
|
318
234
|
async connect(name, definition, signal) {
|
|
319
|
-
this.beforeActivity();
|
|
320
235
|
if (isServerDisabled(definition))
|
|
321
236
|
throw new Error(`MCP server "${name}" is disabled`);
|
|
322
237
|
if (this.stopped)
|
|
@@ -425,7 +340,6 @@ export class McpServerManager {
|
|
|
425
340
|
}
|
|
426
341
|
/** Retire only the expected current client; accepted native work drains before close. */
|
|
427
342
|
retire(name, connection) {
|
|
428
|
-
this.beforeActivity();
|
|
429
343
|
if (this.connections.get(name) !== connection)
|
|
430
344
|
return;
|
|
431
345
|
connection.status = "closed";
|
|
@@ -644,7 +558,7 @@ export class McpServerManager {
|
|
|
644
558
|
}
|
|
645
559
|
async connectClientWithAbort(client, transport, requestOptions, signal) {
|
|
646
560
|
throwIfAborted(signal);
|
|
647
|
-
this.
|
|
561
|
+
this.ownTransportSends(transport, client);
|
|
648
562
|
let abortCleanup;
|
|
649
563
|
const closeTransport = () => {
|
|
650
564
|
abortCleanup = Promise.resolve().then(() => transport.close());
|
|
@@ -691,7 +605,8 @@ export class McpServerManager {
|
|
|
691
605
|
let client;
|
|
692
606
|
client = new ManagedClient({ name: `pi-mcp-${serverName}`, version: ADAPTER_VERSION }, {
|
|
693
607
|
versionNegotiation: { mode: definition.protocolVersion ?? (definition.url ? "auto" : "legacy") },
|
|
694
|
-
|
|
608
|
+
// ponytail: reject catalogs over 1024 pages; raise this native cap if a real catalog needs more.
|
|
609
|
+
listMaxPages: 1024,
|
|
695
610
|
...(Object.keys(capabilities).length > 0 ? { capabilities } : {}),
|
|
696
611
|
listChanged: {
|
|
697
612
|
tools: {
|
|
@@ -713,7 +628,6 @@ export class McpServerManager {
|
|
|
713
628
|
});
|
|
714
629
|
client.serverName = serverName;
|
|
715
630
|
this.ownedClients.add(client);
|
|
716
|
-
client.beforeActivity = this.beforeActivity;
|
|
717
631
|
if (this.samplingConfig) {
|
|
718
632
|
registerSamplingHandler(client, { ...this.samplingConfig, serverName }, run => client.ownCallback("sampling callback", run));
|
|
719
633
|
}
|
|
@@ -739,7 +653,6 @@ export class McpServerManager {
|
|
|
739
653
|
return client;
|
|
740
654
|
}
|
|
741
655
|
handleToolsListChanged(serverName, client, error, tools) {
|
|
742
|
-
this.beforeActivity();
|
|
743
656
|
if (error) {
|
|
744
657
|
logger.debug(`MCP: tools/list_changed refresh failed for ${serverName}: ${error.message}`);
|
|
745
658
|
return;
|
|
@@ -753,7 +666,6 @@ export class McpServerManager {
|
|
|
753
666
|
this.metadataListChangedListener?.(serverName, "tools-list-changed");
|
|
754
667
|
}
|
|
755
668
|
handlePromptsListChanged(serverName, client, error, prompts) {
|
|
756
|
-
this.beforeActivity();
|
|
757
669
|
if (error) {
|
|
758
670
|
logger.debug(`MCP: prompts/list_changed refresh failed for ${serverName}: ${error.message}`);
|
|
759
671
|
return;
|
|
@@ -768,7 +680,6 @@ export class McpServerManager {
|
|
|
768
680
|
this.metadataListChangedListener?.(serverName, "prompts-list-changed");
|
|
769
681
|
}
|
|
770
682
|
handleResourcesListChanged(serverName, client, error, resources) {
|
|
771
|
-
this.beforeActivity();
|
|
772
683
|
if (error) {
|
|
773
684
|
logger.debug(`MCP: resources/list_changed refresh failed for ${serverName}: ${error.message}`);
|
|
774
685
|
return;
|
|
@@ -919,7 +830,6 @@ export class McpServerManager {
|
|
|
919
830
|
const timer = setTimeout(() => {
|
|
920
831
|
if (transportClosed)
|
|
921
832
|
return;
|
|
922
|
-
this.beforeActivity();
|
|
923
833
|
reconnect();
|
|
924
834
|
}, delay);
|
|
925
835
|
return () => clearTimeout(timer);
|
|
@@ -1036,7 +946,6 @@ export class McpServerManager {
|
|
|
1036
946
|
}
|
|
1037
947
|
}
|
|
1038
948
|
async close(name) {
|
|
1039
|
-
this.beforeActivity();
|
|
1040
949
|
this.closeGenerations.set(name, (this.closeGenerations.get(name) ?? 0) + 1);
|
|
1041
950
|
this.connectAttempts.get(name)?.controller.abort(new Error(`MCP connection ${name} was closed`));
|
|
1042
951
|
const pendingClose = this.closePromises.get(name);
|
|
@@ -1115,7 +1024,7 @@ export class McpServerManager {
|
|
|
1115
1024
|
this.elicitationConfig = undefined;
|
|
1116
1025
|
await Promise.all([...this.ownedClients].map(client => drainClientOperations(client)));
|
|
1117
1026
|
try {
|
|
1118
|
-
await this.
|
|
1027
|
+
await this.traceWriter?.flush();
|
|
1119
1028
|
}
|
|
1120
1029
|
catch (error) {
|
|
1121
1030
|
failures.push(error);
|
package/dist/tool-loader.d.ts
CHANGED
|
@@ -1,37 +1,22 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { ExtensionAPI, ExtensionContext, ToolDefinition } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import { type TSchema } from "typebox";
|
|
3
3
|
import { type MetadataCache } from "./metadata-cache.ts";
|
|
4
4
|
import type { McpExtensionState } from "./state.ts";
|
|
5
5
|
import { type DirectToolSpec, type McpAdapterOptions, type McpConfig, type ToolMetadata } from "./types.ts";
|
|
6
|
-
type Reference = {
|
|
7
|
-
name: string;
|
|
8
|
-
namespace?: string;
|
|
9
|
-
};
|
|
10
6
|
export type Feature = "gateway" | "script";
|
|
11
7
|
export type FeaturePolicy = Partial<Record<Feature, {
|
|
12
8
|
eager?: boolean;
|
|
13
9
|
}>>;
|
|
14
|
-
|
|
15
|
-
execute: (...args: Parameters<ToolDefinition<TParams, TDetails>["execute"]>) => Promise<AgentToolResult<TDetails> & {
|
|
16
|
-
tools: Reference[];
|
|
17
|
-
}>;
|
|
18
|
-
};
|
|
19
|
-
export type SearchAPI = ExtensionAPI & {
|
|
20
|
-
registerToolSearch?: <TParams extends TSchema, TDetails>(definition: SearchDefinition<TParams, TDetails>) => void;
|
|
21
|
-
getActiveToolReferences?: () => Reference[];
|
|
22
|
-
setActiveToolReferences?: (tools: Reference[]) => void;
|
|
23
|
-
};
|
|
24
|
-
export declare function supportsNativeAsync(ctx: ExtensionContext): boolean;
|
|
25
|
-
export declare function createToolLoader(pi: SearchAPI, getState: () => McpExtensionState | null, getInitPromise: () => Promise<McpExtensionState> | null, beforeExecute?: McpAdapterOptions["beforeExecute"]): {
|
|
26
|
-
native: boolean;
|
|
10
|
+
export declare function createToolLoader(pi: ExtensionAPI, getState: () => McpExtensionState | null, getInitPromise: () => Promise<McpExtensionState> | null, beforeExecute?: McpAdapterOptions["beforeExecute"]): {
|
|
27
11
|
registerFeature: <TParams extends TSchema, TDetails>(feature: Feature, definition: ToolDefinition<TParams, TDetails>) => void;
|
|
28
12
|
restore: (ctx: ExtensionContext) => void;
|
|
29
|
-
sync: (config: McpConfig, cache: MetadataCache | null, env?: string[] | null,
|
|
13
|
+
sync: (config: McpConfig, cache: MetadataCache | null, env?: string[] | null, registerGateways?: (pins: DirectToolSpec[]) => FeaturePolicy) => DirectToolSpec[];
|
|
30
14
|
activate: (matches: Array<{
|
|
31
15
|
server: string;
|
|
32
16
|
tool: ToolMetadata;
|
|
33
|
-
}>, enable?: Feature[]) =>
|
|
17
|
+
}>, enable?: Feature[]) => {
|
|
18
|
+
name: string;
|
|
19
|
+
}[];
|
|
34
20
|
persist: () => void;
|
|
35
21
|
selectedServers: () => string[];
|
|
36
22
|
};
|
|
37
|
-
export {};
|