@fitchmultz/pi-mcp-adapter 8.0.1 → 8.0.2
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 +19 -1
- package/README.md +3 -3
- package/dist/app-bridge.bundle.js +13 -13
- package/dist/client-operations.d.ts +5 -0
- package/dist/client-operations.js +19 -0
- package/dist/commands.d.ts +1 -1
- package/dist/commands.js +3 -1
- package/dist/connection-runtime.d.ts +503 -0
- package/dist/connection-runtime.js +98 -0
- package/dist/direct-tools.js +1 -1
- package/dist/host-html-template.js +24 -12
- package/dist/index.js +36 -29
- package/dist/init.d.ts +1 -1
- package/dist/init.js +66 -38
- package/dist/lifecycle.d.ts +1 -1
- package/dist/lifecycle.js +1 -1
- package/dist/mcp-auth-flow.d.ts +2 -43
- package/dist/mcp-auth-flow.js +3 -138
- package/dist/mcp-callback-server.js +1 -1
- package/dist/mcp-code.d.ts +0 -1
- package/dist/mcp-code.js +20 -7
- package/dist/mcp-oauth-callback-config.d.ts +8 -0
- package/dist/mcp-oauth-callback-config.js +28 -0
- package/dist/mcp-oauth-provider.d.ts +1 -8
- package/dist/mcp-oauth-provider.js +2 -28
- package/dist/mcp-oauth-runtime.d.ts +83 -0
- package/dist/mcp-oauth-runtime.js +145 -0
- package/dist/mcp-output-guard.d.ts +1 -1
- package/dist/mcp-output-guard.js +5 -108
- package/dist/mcp-output-preview.d.ts +29 -0
- package/dist/mcp-output-preview.js +220 -0
- package/dist/mcp-panel.js +1 -2
- package/dist/mcp-script-worker.mjs +14 -2
- package/dist/metadata-cache.d.ts +1 -1
- package/dist/metadata-cache.js +56 -14
- package/dist/plain-result-reader.d.ts +7 -0
- package/dist/plain-result-reader.js +201 -0
- package/dist/prompts.js +1 -1
- package/dist/proxy-modes.d.ts +1 -1
- package/dist/proxy-modes.js +14 -5
- package/dist/public-types.d.ts +2 -0
- package/dist/public-types.js +2 -0
- package/dist/resource-tools.d.ts +0 -1
- package/dist/resource-tools.js +0 -13
- package/dist/server-manager.d.ts +4 -4
- package/dist/server-manager.js +25 -125
- package/dist/tool-loader.js +10 -4
- package/dist/tool-metadata.js +20 -3
- package/dist/tool-result-renderer.d.ts +1 -1
- package/dist/tool-result-renderer.js +10 -3
- package/dist/types.d.ts +1 -1
- package/dist/types.js +0 -3
- package/dist/ui-message-history.js +3 -2
- package/dist/ui-resource-handler.d.ts +2 -1
- package/dist/ui-resource-handler.js +4 -3
- package/dist/ui-server.js +2 -1
- package/dist/ui-session.js +2 -1
- package/dist/utils.d.ts +1 -0
- package/dist/utils.js +1 -0
- package/package.json +4 -4
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
import { appendFileSync, fstatSync, realpathSync, statSync } from "node:fs";
|
|
2
|
+
import { createHash } from "node:crypto";
|
|
3
|
+
import { StringDecoder } from "node:string_decoder";
|
|
4
|
+
import { open } from "node:fs/promises";
|
|
5
|
+
import { byteLength } from "./mcp-output-preview.js";
|
|
6
|
+
const indexes = new Map();
|
|
7
|
+
const readers = new Map();
|
|
8
|
+
const MAX_INDEXES = 8;
|
|
9
|
+
const MAX_POINTS = 1024;
|
|
10
|
+
function sameRevision(a, b) {
|
|
11
|
+
return a.dev === b.dev && a.ino === b.ino && a.size === b.size
|
|
12
|
+
&& a.mtimeNs === b.mtimeNs && a.ctimeNs === b.ctimeNs;
|
|
13
|
+
}
|
|
14
|
+
/** Raw full-content SHA-256. Returns "" on a short read so a truncated verify never matches. */
|
|
15
|
+
async function digestOf(file, size) {
|
|
16
|
+
const hash = createHash("sha256");
|
|
17
|
+
const buffer = Buffer.allocUnsafe(64 * 1024);
|
|
18
|
+
let position = 0;
|
|
19
|
+
while (position < size) {
|
|
20
|
+
const { bytesRead } = await file.read(buffer, 0, Math.min(buffer.length, size - position), position);
|
|
21
|
+
if (!bytesRead)
|
|
22
|
+
return "";
|
|
23
|
+
hash.update(buffer.subarray(0, bytesRead));
|
|
24
|
+
position += bytesRead;
|
|
25
|
+
}
|
|
26
|
+
return hash.digest("hex");
|
|
27
|
+
}
|
|
28
|
+
function checkpoint(index, point) {
|
|
29
|
+
if (point.bytes - index.points.at(-1).bytes < index.stride)
|
|
30
|
+
return;
|
|
31
|
+
index.points.push(point);
|
|
32
|
+
if (index.points.length > MAX_POINTS) {
|
|
33
|
+
index.points = index.points.filter((_, i) => i % 2 === 0);
|
|
34
|
+
index.stride *= 2;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
function remember(ref, index) {
|
|
38
|
+
indexes.delete(ref);
|
|
39
|
+
indexes.set(ref, index);
|
|
40
|
+
if (indexes.size > MAX_INDEXES)
|
|
41
|
+
indexes.delete(indexes.keys().next().value);
|
|
42
|
+
}
|
|
43
|
+
function extend(index, after, text) {
|
|
44
|
+
// Clone before updating so a shared cached hasher never advances.
|
|
45
|
+
index.hasher = index.hasher.copy();
|
|
46
|
+
index.hasher.update(text, "utf8");
|
|
47
|
+
let position = Number(index.revision.size);
|
|
48
|
+
checkpoint(index, { bytes: position, characters: index.total });
|
|
49
|
+
// Index only the accepted delta, keeping checkpoints on complete UTF-8 code points.
|
|
50
|
+
for (let start = 0; start < text.length;) {
|
|
51
|
+
let end = Math.min(text.length, start + 16 * 1024);
|
|
52
|
+
if (end < text.length && text.codePointAt(end - 1) > 0xffff)
|
|
53
|
+
end++;
|
|
54
|
+
position += byteLength(text.slice(start, end));
|
|
55
|
+
checkpoint(index, { bytes: position, characters: index.total + end });
|
|
56
|
+
start = end;
|
|
57
|
+
}
|
|
58
|
+
index.total += text.length;
|
|
59
|
+
index.revision = after;
|
|
60
|
+
index.digest = index.hasher.copy().digest("hex");
|
|
61
|
+
}
|
|
62
|
+
/** Only the native, synchronous history writer can certify an append. External changes invalidate. */
|
|
63
|
+
export function appendPlainOutput(ref, text) {
|
|
64
|
+
let key = ref, cached, before;
|
|
65
|
+
try {
|
|
66
|
+
key = indexes.size || readers.size ? realpathSync(ref) : ref;
|
|
67
|
+
cached = indexes.get(key);
|
|
68
|
+
indexes.delete(key);
|
|
69
|
+
if (cached || readers.has(key))
|
|
70
|
+
before = statSync(ref, { bigint: true });
|
|
71
|
+
}
|
|
72
|
+
catch { /* Cache bookkeeping must not change write acceptance. */ }
|
|
73
|
+
appendFileSync(ref, text, { flush: true });
|
|
74
|
+
if (!before)
|
|
75
|
+
return;
|
|
76
|
+
try {
|
|
77
|
+
const after = statSync(ref, { bigint: true });
|
|
78
|
+
if (after.dev !== before.dev || after.ino !== before.ino
|
|
79
|
+
|| after.size !== before.size + BigInt(byteLength(text)))
|
|
80
|
+
return;
|
|
81
|
+
if (cached && sameRevision(cached.revision, before) && !text.includes("\ufffd")) {
|
|
82
|
+
const next = { ...cached, points: [...cached.points] };
|
|
83
|
+
extend(next, after, text);
|
|
84
|
+
remember(key, next);
|
|
85
|
+
}
|
|
86
|
+
for (const progress of readers.get(key) ?? []) {
|
|
87
|
+
if (!sameRevision(progress.revision, before))
|
|
88
|
+
continue;
|
|
89
|
+
extend(progress, after, text);
|
|
90
|
+
if (text.includes("\ufffd"))
|
|
91
|
+
progress.cacheable = false;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
catch { /* The durable append already succeeded; leave its index invalidated. */ }
|
|
95
|
+
}
|
|
96
|
+
/** Sparse byte/UTF16 checkpoints, never retained file contents. Changed/uncacheable files scan afresh. */
|
|
97
|
+
export async function readPlainResult(ref, offset, limit, maxBytes, maxLines) {
|
|
98
|
+
for (let attempt = 0; attempt < 2; attempt++) {
|
|
99
|
+
const file = await open(ref, "r");
|
|
100
|
+
let progress;
|
|
101
|
+
try {
|
|
102
|
+
// Pair snapshot registration/validation with the synchronous writer without yielding between them.
|
|
103
|
+
const revision = fstatSync(file.fd, { bigint: true });
|
|
104
|
+
const cached = indexes.get(ref);
|
|
105
|
+
const matched = cached !== undefined && sameRevision(cached.revision, revision);
|
|
106
|
+
const fresh = { revision, total: 0, points: [{ bytes: 0, characters: 0 }],
|
|
107
|
+
stride: 64 * 1024, digest: "", hasher: createHash("sha256") };
|
|
108
|
+
let index = matched ? cached : fresh;
|
|
109
|
+
// Append characters stay relative until the cold scan knows the opened prefix's total.
|
|
110
|
+
progress = { revision, total: 0, points: [{ bytes: Number(revision.size), characters: 0 }],
|
|
111
|
+
stride: index.stride, cacheable: true, digest: "", hasher: index.hasher.copy() };
|
|
112
|
+
const active = readers.get(ref) ?? new Set();
|
|
113
|
+
active.add(progress);
|
|
114
|
+
readers.set(ref, active);
|
|
115
|
+
// Matching stat fields are not an immutability proof: verify the cached digest against current
|
|
116
|
+
// bytes. Registering the snapshot first keeps live appends paired during the verification read.
|
|
117
|
+
let valid = matched ? cached : undefined;
|
|
118
|
+
if (valid && valid.digest !== (await digestOf(file, Number(revision.size)))) {
|
|
119
|
+
indexes.delete(ref);
|
|
120
|
+
valid = undefined;
|
|
121
|
+
index = fresh;
|
|
122
|
+
progress.hasher = createHash("sha256");
|
|
123
|
+
progress.stride = fresh.stride;
|
|
124
|
+
}
|
|
125
|
+
let point = index.points[0];
|
|
126
|
+
if (valid) {
|
|
127
|
+
for (const candidate of index.points) {
|
|
128
|
+
if (candidate.characters > offset)
|
|
129
|
+
break;
|
|
130
|
+
point = candidate;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
let total = point.characters, position = point.bytes;
|
|
134
|
+
let bytes = 0, lines = 1, page = "", finished = false, cacheable = true;
|
|
135
|
+
// Bound each attempt to its opened file snapshot, including while native history appends.
|
|
136
|
+
if (revision.size > 0n) {
|
|
137
|
+
const decoder = new StringDecoder("utf8");
|
|
138
|
+
const buffer = Buffer.allocUnsafe(64 * 1024);
|
|
139
|
+
let readPosition = point.bytes;
|
|
140
|
+
while (readPosition < Number(revision.size)) {
|
|
141
|
+
const { bytesRead } = await file.read(buffer, 0, Math.min(buffer.length, Number(revision.size) - readPosition), readPosition);
|
|
142
|
+
if (!bytesRead)
|
|
143
|
+
break;
|
|
144
|
+
readPosition += bytesRead;
|
|
145
|
+
const chunk = decoder.write(buffer.subarray(0, bytesRead)) + (readPosition === Number(revision.size) ? decoder.end() : "");
|
|
146
|
+
if (!valid) {
|
|
147
|
+
progress.hasher.update(buffer.subarray(0, bytesRead));
|
|
148
|
+
checkpoint(index, { bytes: position, characters: total });
|
|
149
|
+
// Invalid/incomplete UTF8 can re-decode differently after append. Keep the full-scan fallback.
|
|
150
|
+
if (chunk.includes("\ufffd"))
|
|
151
|
+
cacheable = false;
|
|
152
|
+
position += byteLength(chunk);
|
|
153
|
+
}
|
|
154
|
+
const start = Math.max(0, offset - total);
|
|
155
|
+
if (!finished && start < chunk.length) {
|
|
156
|
+
if (start > 0 && chunk.codePointAt(start - 1) > 0xffff)
|
|
157
|
+
throw new Error("offset must not split a Unicode surrogate pair");
|
|
158
|
+
for (const char of chunk.slice(start)) {
|
|
159
|
+
const nextBytes = bytes + byteLength(char);
|
|
160
|
+
const nextLines = lines + (char === "\n" ? 1 : 0);
|
|
161
|
+
if (page && (nextBytes > maxBytes || nextLines > maxLines || page.length + char.length > limit)) {
|
|
162
|
+
finished = true;
|
|
163
|
+
break;
|
|
164
|
+
}
|
|
165
|
+
page += char;
|
|
166
|
+
bytes = nextBytes;
|
|
167
|
+
lines = nextLines;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
total += chunk.length;
|
|
171
|
+
if (valid && finished)
|
|
172
|
+
break;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
// Only verified native appends may advance this snapshot. External edits still invalidate.
|
|
176
|
+
if (!sameRevision(progress.revision, statSync(ref, { bigint: true })))
|
|
177
|
+
continue;
|
|
178
|
+
if (!valid)
|
|
179
|
+
index.total = total;
|
|
180
|
+
// Serve the opened prefix; retain verified append checkpoints for the next read.
|
|
181
|
+
if (cacheable && progress.cacheable) {
|
|
182
|
+
const next = { ...index, revision: progress.revision, total: index.total + progress.total,
|
|
183
|
+
points: [...index.points], digest: progress.hasher.copy().digest("hex"), hasher: progress.hasher };
|
|
184
|
+
for (const point of progress.points)
|
|
185
|
+
checkpoint(next, { bytes: point.bytes, characters: index.total + point.characters });
|
|
186
|
+
remember(ref, next);
|
|
187
|
+
}
|
|
188
|
+
return { page, total: index.total };
|
|
189
|
+
}
|
|
190
|
+
finally {
|
|
191
|
+
if (progress) {
|
|
192
|
+
const active = readers.get(ref);
|
|
193
|
+
active.delete(progress);
|
|
194
|
+
if (!active.size)
|
|
195
|
+
readers.delete(ref);
|
|
196
|
+
}
|
|
197
|
+
await file.close();
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
throw new Error("MCP output artifact changed while reading; retry the page");
|
|
201
|
+
}
|
package/dist/prompts.js
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { isServerDisabled } from "./types.js";
|
|
2
|
-
import { lazyConnect } from "./init.js";
|
|
3
2
|
import { isServerCacheValid, loadMetadataCache, reconstructPromptMetadata } from "./metadata-cache.js";
|
|
4
3
|
import { logger } from "./logger.js";
|
|
5
4
|
import { truncateAtWord } from "./utils.js";
|
|
@@ -233,6 +232,7 @@ export function createPromptCommand(pi, getState, metadata) {
|
|
|
233
232
|
}
|
|
234
233
|
return;
|
|
235
234
|
}
|
|
235
|
+
const { lazyConnect } = await import("./init.js");
|
|
236
236
|
const connected = await lazyConnect(state, metadata.serverName, ctx.signal);
|
|
237
237
|
if (!connected) {
|
|
238
238
|
if (ctx.hasUI) {
|
package/dist/proxy-modes.d.ts
CHANGED
|
@@ -2,7 +2,7 @@ import type { AgentToolResult, ToolInfo } from "@earendil-works/pi-coding-agent"
|
|
|
2
2
|
import type { McpExtensionState } from "./state.ts";
|
|
3
3
|
import type { ServerConnection } from "./server-manager.ts";
|
|
4
4
|
import type { ToolMetadata, McpOperationContext, McpToolCallIdentity } from "./types.ts";
|
|
5
|
-
import {
|
|
5
|
+
import type { SessionRecoveryDeps } from "./session-recovery.ts";
|
|
6
6
|
type ProxyToolResult = AgentToolResult<Record<string, unknown>>;
|
|
7
7
|
export declare function executeUiMessages(state: McpExtensionState): Promise<ProxyToolResult>;
|
|
8
8
|
export declare function executeStatus(state: McpExtensionState): ProxyToolResult;
|
package/dist/proxy-modes.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { isDeepStrictEqual } from "node:util";
|
|
2
2
|
import { getServerPrefix, isServerDisabled, isNonInteractiveOAuth, parseUiPromptHandoff, resolveToolPrefix } from "./types.js";
|
|
3
3
|
import { lazyConnect, markKeepAliveAfterConnect, notifyToolMetadataUpdated, updateServerMetadata, updateMetadataCache, getFailureAgeSeconds, updateStatusBar, clearFailure, recordFailure } from "./init.js";
|
|
4
4
|
import { abortable, throwIfAborted } from "./abort.js";
|
|
@@ -7,10 +7,8 @@ import { buildToolMetadata, catalogCoverage, findToolByName, formatSchema, toToo
|
|
|
7
7
|
import { reconstructPromptMetadata } from "./metadata-cache.js";
|
|
8
8
|
import { renderMcpResultContent } from "./tool-registrar.js";
|
|
9
9
|
import { guardMcpOutput, guardedMcpDetails, retainMcpResult, resolveMcpOutputGuardOptions, formatMcpResultReference, saveMcpOutputStream } from "./mcp-output-guard.js";
|
|
10
|
-
import { maybeStartUiSession, summarizeUiSessionResult } from "./ui-session.js";
|
|
11
10
|
import { formatAuthRequiredMessage, formatMcpStatus, resolveServerUrl, truncateAtWord } from "./utils.js";
|
|
12
|
-
import {
|
|
13
|
-
import { isInterruptedToolCall, isToolTransportFailure, trackToolCallOutcome, SessionRecoveryAuthRequiredError, withSessionRecovery } from "./session-recovery.js";
|
|
11
|
+
import { supportsOAuth } from "./mcp-oauth-runtime.js";
|
|
14
12
|
import { paginate, rankSuggestions, rankToolMatches } from "./search-ranking.js";
|
|
15
13
|
import { ensureToolCallApproved, isToolCallApprovalRequired } from "./tool-approval.js";
|
|
16
14
|
import { materializeUiMessages, uiMessages } from "./ui-message-history.js";
|
|
@@ -96,6 +94,7 @@ async function attemptAutoAuth(state, serverName, signal) {
|
|
|
96
94
|
};
|
|
97
95
|
}
|
|
98
96
|
try {
|
|
97
|
+
const { authenticate } = await import("./mcp-auth-flow.js");
|
|
99
98
|
if (state.authStorageOptions) {
|
|
100
99
|
await authenticate(serverName, serverUrl, definition, signal
|
|
101
100
|
? { authStorageOptions: state.authStorageOptions, signal, runtime: state.oauthRuntime }
|
|
@@ -340,6 +339,7 @@ export function executeStatus(state) {
|
|
|
340
339
|
};
|
|
341
340
|
}
|
|
342
341
|
export async function executeAuthStart(state, serverName, signal) {
|
|
342
|
+
const { startAuth } = await import("./mcp-auth-flow.js");
|
|
343
343
|
const ownedSignal = combineAbortSignals(state.owner?.signal, signal);
|
|
344
344
|
throwIfAborted(ownedSignal);
|
|
345
345
|
const definition = state.config.mcpServers[serverName];
|
|
@@ -386,6 +386,7 @@ export async function executeAuthStart(state, serverName, signal) {
|
|
|
386
386
|
}
|
|
387
387
|
}
|
|
388
388
|
export async function executeAuthComplete(state, serverName, input, signal) {
|
|
389
|
+
const { completeAuthFromInput } = await import("./mcp-auth-flow.js");
|
|
389
390
|
const ownedSignal = combineAbortSignals(state.owner?.signal, signal);
|
|
390
391
|
throwIfAborted(ownedSignal);
|
|
391
392
|
const definition = state.config.mcpServers[serverName];
|
|
@@ -715,6 +716,9 @@ export async function executeConnect(state, serverName, signal, limit, offset) {
|
|
|
715
716
|
* Raw scripts skip content rendering but retain protocol results and artifacts.
|
|
716
717
|
*/
|
|
717
718
|
export async function runToolCall(state, serverName, target, args, options) {
|
|
719
|
+
const { DEFAULT_REQUEST_TIMEOUT_MSEC, UrlElicitationRequiredError } = await import("@modelcontextprotocol/client");
|
|
720
|
+
const { isInterruptedToolCall, isToolTransportFailure, trackToolCallOutcome, SessionRecoveryAuthRequiredError, withSessionRecovery } = await import("./session-recovery.js");
|
|
721
|
+
const { maybeStartUiSession, summarizeUiSessionResult } = await import("./ui-session.js");
|
|
718
722
|
const { detailsBase, signal, recoverAuthConnection, authRequiredMessage, autoAuthAttempted } = options;
|
|
719
723
|
const configuredOptions = state.manager.getRequestOptions?.(serverName, options.ownedSignal);
|
|
720
724
|
const callerSignal = combineAbortSignals(configuredOptions?.signal, options.ownedSignal);
|
|
@@ -746,6 +750,8 @@ export async function runToolCall(state, serverName, target, args, options) {
|
|
|
746
750
|
try {
|
|
747
751
|
state.manager.touch(serverName);
|
|
748
752
|
state.manager.incrementInFlight(serverName);
|
|
753
|
+
// Loading the connection-only modules can yield to cancellation or shutdown.
|
|
754
|
+
throwIfAborted(callerSignal);
|
|
749
755
|
uiSession = !target.resourceUri && target.uiResourceUri
|
|
750
756
|
? await abortable(maybeStartUiSession(state, {
|
|
751
757
|
serverName,
|
|
@@ -892,6 +898,7 @@ export async function runToolCall(state, serverName, target, args, options) {
|
|
|
892
898
|
}
|
|
893
899
|
/** Connect/auth once for calls and resource discovery. Recovery stays bounded by withSessionRecovery. */
|
|
894
900
|
export async function prepareMcpConnection(state, serverName, signal) {
|
|
901
|
+
const { SessionRecoveryAuthRequiredError } = await import("./session-recovery.js");
|
|
895
902
|
const ownedSignal = combineAbortSignals(state.owner?.signal, signal);
|
|
896
903
|
throwIfAborted(ownedSignal);
|
|
897
904
|
let autoAuthAttempted = false;
|
|
@@ -951,7 +958,9 @@ export async function prepareMcpConnection(state, serverName, signal) {
|
|
|
951
958
|
// Use the live descriptor for visibility, filtering, approval, schema and UI hints.
|
|
952
959
|
if (connection.tools) {
|
|
953
960
|
const { metadata } = buildToolMetadata(connection.tools, connection.resources ?? [], definition, serverName, state.config.settings?.toolPrefix ?? "server");
|
|
954
|
-
|
|
961
|
+
// Search indexes by metadata-array identity, so retain it for unchanged projections.
|
|
962
|
+
if (!isDeepStrictEqual(state.toolMetadata.get(serverName), metadata))
|
|
963
|
+
state.toolMetadata.set(serverName, metadata);
|
|
955
964
|
}
|
|
956
965
|
return { connection, recoverAuthConnection, autoAuthAttempted: () => autoAuthAttempted };
|
|
957
966
|
}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
export * from "./types.ts";
|
|
2
|
+
export { UI_STREAM_HOST_CONTEXT_KEY, UI_STREAM_REQUEST_META_KEY, UI_STREAM_RESULT_PATCH_METHOD, SERVER_STREAM_RESULT_PATCH_METHOD, UI_STREAM_STRUCTURED_CONTENT_KEY, uiStreamModeSchema, visualizationStreamPhaseSchema, visualizationStreamFrameTypeSchema, visualizationStreamStatusSchema, uiStreamHostContextSchema, visualizationStreamEnvelopeSchema, uiStreamCallToolResultSchema, uiStreamResultPatchNotificationSchema, serverStreamResultPatchNotificationSchema, getUiStreamHostContext, getVisualizationStreamEnvelope, } from "./ui-stream-types.ts";
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
export * from "./types.js";
|
|
2
|
+
export { UI_STREAM_HOST_CONTEXT_KEY, UI_STREAM_REQUEST_META_KEY, UI_STREAM_RESULT_PATCH_METHOD, SERVER_STREAM_RESULT_PATCH_METHOD, UI_STREAM_STRUCTURED_CONTENT_KEY, uiStreamModeSchema, visualizationStreamPhaseSchema, visualizationStreamFrameTypeSchema, visualizationStreamStatusSchema, uiStreamHostContextSchema, visualizationStreamEnvelopeSchema, uiStreamCallToolResultSchema, uiStreamResultPatchNotificationSchema, serverStreamResultPatchNotificationSchema, getUiStreamHostContext, getVisualizationStreamEnvelope, } from "./ui-stream-types.js";
|
package/dist/resource-tools.d.ts
CHANGED
|
@@ -13,4 +13,3 @@ export declare function executeResourceList(state: McpExtensionState, server: st
|
|
|
13
13
|
};
|
|
14
14
|
}>;
|
|
15
15
|
export declare function executeResourceRead(state: McpExtensionState, server: string, uri: string, signal?: AbortSignal, identity?: McpToolCallIdentity, beforeDispatch?: (signal: AbortSignal | undefined, operation: McpOperationContext) => Promise<void>, raw?: boolean): Promise<import("@earendil-works/pi-agent-core").AgentToolResult<Record<string, unknown>>>;
|
|
16
|
-
export declare function resourceNameToToolName(name: string): string;
|
package/dist/resource-tools.js
CHANGED
|
@@ -24,16 +24,3 @@ export async function executeResourceList(state, server, limit = 12, offset = 0,
|
|
|
24
24
|
export async function executeResourceRead(state, server, uri, signal, identity = {}, beforeDispatch, raw = false) {
|
|
25
25
|
return executeCall(state, uri, {}, server, undefined, signal, identity, beforeDispatch, { resourceUri: uri, raw });
|
|
26
26
|
}
|
|
27
|
-
export function resourceNameToToolName(name) {
|
|
28
|
-
let result = name
|
|
29
|
-
.replace(/[^a-zA-Z0-9]/g, "_")
|
|
30
|
-
.replace(/_+/g, "_")
|
|
31
|
-
.replace(/^_+/, "") // Remove leading underscores
|
|
32
|
-
.replace(/_+$/, "") // Remove trailing underscores
|
|
33
|
-
.toLowerCase();
|
|
34
|
-
// Ensure we have a valid name
|
|
35
|
-
if (!result || /^\d/.test(result)) {
|
|
36
|
-
result = "resource" + (result ? "_" + result : "");
|
|
37
|
-
}
|
|
38
|
-
return result;
|
|
39
|
-
}
|
package/dist/server-manager.d.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import { Client,
|
|
1
|
+
import type { Client, RequestOptions, GetPromptResult, ReadResourceResult, UrlElicitationRequiredError } from "@modelcontextprotocol/client";
|
|
2
2
|
import { type McpTool, type McpResource, type McpPrompt, type ServerDefinition, type ServerStreamResultPatchNotification, type Transport, type McpTraceSettings } from "./types.ts";
|
|
3
|
-
import { type McpOAuthRuntime } from "./mcp-
|
|
3
|
+
import { type McpOAuthRuntime } from "./mcp-oauth-runtime.ts";
|
|
4
4
|
import { type AuthStorageOptions } from "./mcp-auth.ts";
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
5
|
+
import type { ServerSamplingConfig } from "./sampling-handler.ts";
|
|
6
|
+
import type { ServerElicitationConfig } from "./elicitation-handler.ts";
|
|
7
7
|
export interface ServerConnection {
|
|
8
8
|
client: Client;
|
|
9
9
|
transport: Transport;
|
package/dist/server-manager.js
CHANGED
|
@@ -1,16 +1,9 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import { StdioClientTransport } from "@modelcontextprotocol/client/stdio";
|
|
4
|
-
import { UnixSocketClientTransport } from "./unix-socket-transport.js";
|
|
5
|
-
import { trackToolTransport, trackToolTransportFailure } from "./session-recovery.js";
|
|
6
|
-
import { isServerDisabled, isNonInteractiveOAuth, validateServerProtocolConfig, serverStreamResultPatchNotificationSchema, } from "./types.js";
|
|
1
|
+
import { clientOperations, drainClientCallbacks, drainClientOperations } from "./client-operations.js";
|
|
2
|
+
import { isServerDisabled, isNonInteractiveOAuth, validateServerProtocolConfig, } from "./types.js";
|
|
7
3
|
import { resolveNpxBinary } from "./npx-resolver.js";
|
|
8
4
|
import { logger } from "./logger.js";
|
|
9
|
-
import {
|
|
10
|
-
import { extractOAuthConfig, supportsOAuth, isOAuthChallenge, recordOAuthChallenge, getOAuthRequest, bindOAuthRequestIssuer } from "./mcp-auth-flow.js";
|
|
5
|
+
import { supportsOAuth } from "./mcp-oauth-runtime.js";
|
|
11
6
|
import { getAuthBaseDir } from "./mcp-auth.js";
|
|
12
|
-
import { registerSamplingHandler } from "./sampling-handler.js";
|
|
13
|
-
import { handleUrlElicitation, registerElicitationHandler, } from "./elicitation-handler.js";
|
|
14
7
|
import { ADAPTER_VERSION, normalizeRequestTimeoutMs, resolveBearerToken, resolveCommandSecret, resolveCommandSecretsRecord, resolveConfigPath, resolveServerUrl, } from "./utils.js";
|
|
15
8
|
import { abortable, throwIfAborted } from "./abort.js";
|
|
16
9
|
import { combineAbortSignals } from "./runtime-owner.js";
|
|
@@ -18,108 +11,6 @@ import { createMcpTraceWriter, isMcpTraceEnabled, McpTraceWriter, traceTransport
|
|
|
18
11
|
const MAX_CAPTURED_STDERR_BYTES = 8 * 1024;
|
|
19
12
|
const MAX_CAPTURED_STDERR_LINES = 3;
|
|
20
13
|
const abortCleanupPromises = new WeakMap();
|
|
21
|
-
const clientOperations = new WeakMap();
|
|
22
|
-
async function drainClientCallbacks(client) {
|
|
23
|
-
for (;;) {
|
|
24
|
-
const pending = [...(clientOperations.get(client) ?? [])]
|
|
25
|
-
.filter(([, kind]) => kind === "sampling callback" || kind === "elicitation callback")
|
|
26
|
-
.map(([operation]) => operation);
|
|
27
|
-
if (!pending.length)
|
|
28
|
-
return;
|
|
29
|
-
await Promise.allSettled(pending);
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
async function drainClientOperations(client) {
|
|
33
|
-
for (;;) {
|
|
34
|
-
const pending = [...(clientOperations.get(client)?.keys() ?? [])];
|
|
35
|
-
if (!pending.length)
|
|
36
|
-
return;
|
|
37
|
-
await Promise.allSettled(pending);
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
// Track complete SDK operations, including pagination and pre-request cache awaits.
|
|
41
|
-
class ManagedClient extends Client {
|
|
42
|
-
serverName = "";
|
|
43
|
-
oauthProvider;
|
|
44
|
-
own(operation, kind) {
|
|
45
|
-
let pending = clientOperations.get(this);
|
|
46
|
-
if (!pending)
|
|
47
|
-
clientOperations.set(this, pending = new Map());
|
|
48
|
-
pending.set(operation, kind);
|
|
49
|
-
const settled = () => { pending.delete(operation); };
|
|
50
|
-
void operation.then(settled, settled);
|
|
51
|
-
return operation;
|
|
52
|
-
}
|
|
53
|
-
ownNative(run) {
|
|
54
|
-
return this.own(run(), "request/refresh");
|
|
55
|
-
}
|
|
56
|
-
ownSend(run) {
|
|
57
|
-
// SDK-generated callback replies also perform HTTP/auth work, without calling callTool().
|
|
58
|
-
return this.own(Promise.resolve().then(run), "request/refresh");
|
|
59
|
-
}
|
|
60
|
-
ownCallback(kind, run) {
|
|
61
|
-
// Insert before dispatch. Cancellation/close of the SDK response does not settle this promise.
|
|
62
|
-
return this.own(Promise.resolve().then(run), kind);
|
|
63
|
-
}
|
|
64
|
-
async track(start, signal) {
|
|
65
|
-
const controller = this.oauthProvider ? new AbortController() : undefined;
|
|
66
|
-
let removeAbortListener = () => { };
|
|
67
|
-
const operation = this.oauthProvider
|
|
68
|
-
? this.oauthProvider.runWithSignal(controller.signal, () => {
|
|
69
|
-
// Native legacy cancellation notifications must retain their operation's auth context.
|
|
70
|
-
const abort = AsyncLocalStorage.bind(() => controller.abort(signal?.reason));
|
|
71
|
-
if (signal?.aborted)
|
|
72
|
-
abort();
|
|
73
|
-
else
|
|
74
|
-
signal?.addEventListener("abort", abort, { once: true });
|
|
75
|
-
removeAbortListener = () => signal?.removeEventListener("abort", abort);
|
|
76
|
-
return start(controller.signal);
|
|
77
|
-
})
|
|
78
|
-
: start(signal);
|
|
79
|
-
void operation.then(removeAbortListener, () => { controller?.abort(); removeAbortListener(); });
|
|
80
|
-
return this.own(operation, "request/refresh");
|
|
81
|
-
}
|
|
82
|
-
callTool(...args) {
|
|
83
|
-
return this.track(signal => super.callTool(args[0], { ...args[1], ...(signal ? { signal } : {}) }), args[1]?.signal);
|
|
84
|
-
}
|
|
85
|
-
readResource(...args) {
|
|
86
|
-
return this.track(signal => super.readResource(args[0], { ...args[1], ...(signal ? { signal } : {}) }), args[1]?.signal);
|
|
87
|
-
}
|
|
88
|
-
getPrompt(...args) {
|
|
89
|
-
return this.track(signal => super.getPrompt(args[0], { ...args[1], ...(signal ? { signal } : {}) }), args[1]?.signal);
|
|
90
|
-
}
|
|
91
|
-
listTools(...args) {
|
|
92
|
-
return this.track(signal => super.listTools(args[0], { ...args[1], ...(signal ? { signal } : {}) }), args[1]?.signal);
|
|
93
|
-
}
|
|
94
|
-
listResources(...args) {
|
|
95
|
-
return this.track(signal => super.listResources(args[0], { ...args[1], ...(signal ? { signal } : {}) }), args[1]?.signal);
|
|
96
|
-
}
|
|
97
|
-
listPrompts(...args) {
|
|
98
|
-
return this.track(signal => super.listPrompts(args[0], { ...args[1], ...(signal ? { signal } : {}) }), args[1]?.signal);
|
|
99
|
-
}
|
|
100
|
-
complete(...args) {
|
|
101
|
-
return this.track(signal => super.complete(args[0], { ...args[1], ...(signal ? { signal } : {}) }), args[1]?.signal);
|
|
102
|
-
}
|
|
103
|
-
setLoggingLevel(...args) {
|
|
104
|
-
return this.track(signal => super.setLoggingLevel(args[0], { ...args[1], ...(signal ? { signal } : {}) }), args[1]?.signal);
|
|
105
|
-
}
|
|
106
|
-
listen(...args) {
|
|
107
|
-
return this.track(async (signal) => {
|
|
108
|
-
const subscription = await super.listen(args[0], { ...args[1], ...(signal ? { signal } : {}) });
|
|
109
|
-
this.own(subscription.closed, "subscription");
|
|
110
|
-
return subscription;
|
|
111
|
-
}, args[1]?.signal);
|
|
112
|
-
}
|
|
113
|
-
subscribeResource(...args) {
|
|
114
|
-
return this.track(async (signal) => {
|
|
115
|
-
throwIfAborted(signal);
|
|
116
|
-
return super.subscribeResource(args[0], { ...args[1], ...(signal ? { signal } : {}) });
|
|
117
|
-
}, args[1]?.signal);
|
|
118
|
-
}
|
|
119
|
-
unsubscribeResource(...args) {
|
|
120
|
-
return this.track(signal => super.unsubscribeResource(args[0], { ...args[1], ...(signal ? { signal } : {}) }), args[1]?.signal);
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
14
|
function boundedStderrChunk(chunk) {
|
|
124
15
|
if (Buffer.isBuffer(chunk)) {
|
|
125
16
|
const start = Math.max(0, chunk.byteLength - MAX_CAPTURED_STDERR_BYTES);
|
|
@@ -171,12 +62,12 @@ export class McpServerManager {
|
|
|
171
62
|
traceWriter;
|
|
172
63
|
stopped = false;
|
|
173
64
|
releaseClientWhenSettled(client) {
|
|
174
|
-
if (client
|
|
65
|
+
if (this.ownedClients.has(client))
|
|
175
66
|
void drainClientOperations(client).then(() => this.ownedClients.delete(client));
|
|
176
67
|
}
|
|
177
68
|
/** Own native send/auth tails even after a public request has been cancelled. */
|
|
178
69
|
ownTransportSends(transport, client) {
|
|
179
|
-
if (typeof transport.send === "function"
|
|
70
|
+
if (typeof transport.send === "function") {
|
|
180
71
|
const send = transport.send.bind(transport);
|
|
181
72
|
transport.send = (...args) => client.ownSend(() => send(...args));
|
|
182
73
|
}
|
|
@@ -369,7 +260,10 @@ export class McpServerManager {
|
|
|
369
260
|
async createConnection(name, definition, signal, requestSignal) {
|
|
370
261
|
throwIfAborted(signal);
|
|
371
262
|
validateServerProtocolConfig(definition);
|
|
372
|
-
|
|
263
|
+
const runtime = await import("./connection-runtime.js");
|
|
264
|
+
const { StdioClientTransport, SdkHttpError, UnixSocketClientTransport, trackToolTransport, getOAuthRequest, isOAuthChallenge } = runtime;
|
|
265
|
+
throwIfAborted(signal);
|
|
266
|
+
let client = this.createClient(name, definition, runtime);
|
|
373
267
|
const tracingEnabled = isMcpTraceEnabled(definition, this.traceSettings);
|
|
374
268
|
const traceWriter = tracingEnabled
|
|
375
269
|
? (this.traceWriter ??= createMcpTraceWriter(this.defaultCwd, this.traceSettings ?? {}))
|
|
@@ -420,7 +314,7 @@ export class McpServerManager {
|
|
|
420
314
|
}
|
|
421
315
|
else if (definition.url) {
|
|
422
316
|
// HTTP transport with fallback
|
|
423
|
-
({ transport, oauthProvider } = this.createHttpTransport(definition, name, client, false, implicitOAuth, onAuthChallenge));
|
|
317
|
+
({ transport, oauthProvider } = this.createHttpTransport(runtime, definition, name, client, false, implicitOAuth, onAuthChallenge));
|
|
424
318
|
}
|
|
425
319
|
else {
|
|
426
320
|
transport = new UnixSocketClientTransport(resolveConfigPath(definition.socket));
|
|
@@ -449,13 +343,13 @@ export class McpServerManager {
|
|
|
449
343
|
legacySse = true;
|
|
450
344
|
await client.close();
|
|
451
345
|
this.releaseClientWhenSettled(client);
|
|
452
|
-
client = this.createClient(name, { ...definition, protocolVersion: "legacy" });
|
|
453
|
-
({ transport, oauthProvider } = this.createHttpTransport(definition, name, client, legacySse, implicitOAuth, onAuthChallenge));
|
|
346
|
+
client = this.createClient(name, { ...definition, protocolVersion: "legacy" }, runtime);
|
|
347
|
+
({ transport, oauthProvider } = this.createHttpTransport(runtime, definition, name, client, legacySse, implicitOAuth, onAuthChallenge));
|
|
454
348
|
if (traceObserver)
|
|
455
349
|
transport = wrapTransportWithMcpTrace(transport, name, traceTransportKind(definition, transport), traceObserver);
|
|
456
350
|
continue;
|
|
457
351
|
}
|
|
458
|
-
this.attachAdapterNotificationHandlers(name, client);
|
|
352
|
+
this.attachAdapterNotificationHandlers(name, client, runtime.serverStreamResultPatchNotificationSchema);
|
|
459
353
|
const instructions = client.getInstructions?.();
|
|
460
354
|
const connection = {
|
|
461
355
|
client,
|
|
@@ -501,8 +395,8 @@ export class McpServerManager {
|
|
|
501
395
|
implicitOAuth = true;
|
|
502
396
|
await client.close();
|
|
503
397
|
this.releaseClientWhenSettled(client);
|
|
504
|
-
client = this.createClient(name, legacySse ? { ...definition, protocolVersion: "legacy" } : definition);
|
|
505
|
-
({ transport, oauthProvider } = this.createHttpTransport(definition, name, client, legacySse, implicitOAuth, onAuthChallenge));
|
|
398
|
+
client = this.createClient(name, legacySse ? { ...definition, protocolVersion: "legacy" } : definition, runtime);
|
|
399
|
+
({ transport, oauthProvider } = this.createHttpTransport(runtime, definition, name, client, legacySse, implicitOAuth, onAuthChallenge));
|
|
506
400
|
if (traceObserver)
|
|
507
401
|
transport = wrapTransportWithMcpTrace(transport, name, traceTransportKind(definition, transport), traceObserver);
|
|
508
402
|
}
|
|
@@ -600,7 +494,8 @@ export class McpServerManager {
|
|
|
600
494
|
: {}),
|
|
601
495
|
};
|
|
602
496
|
}
|
|
603
|
-
createClient(serverName, definition) {
|
|
497
|
+
createClient(serverName, definition, runtime) {
|
|
498
|
+
const { ManagedClient, registerSamplingHandler, registerElicitationHandler } = runtime;
|
|
604
499
|
const capabilities = this.buildClientCapabilities(definition);
|
|
605
500
|
let client;
|
|
606
501
|
client = new ManagedClient({ name: `pi-mcp-${serverName}`, version: ADAPTER_VERSION }, {
|
|
@@ -693,6 +588,10 @@ export class McpServerManager {
|
|
|
693
588
|
this.metadataListChangedListener?.(serverName, "resources-list-changed");
|
|
694
589
|
}
|
|
695
590
|
async handleUrlElicitationRequired(serverName, error) {
|
|
591
|
+
if (this.runtimeSignal?.aborted || !this.elicitationConfig?.allowUrl)
|
|
592
|
+
return "cancel";
|
|
593
|
+
const { handleUrlElicitation } = await import("./elicitation-handler.js");
|
|
594
|
+
// The import yields; recheck cancellation and URL support before prompting.
|
|
696
595
|
if (this.runtimeSignal?.aborted || !this.elicitationConfig?.allowUrl)
|
|
697
596
|
return "cancel";
|
|
698
597
|
for (const params of error.elicitations) {
|
|
@@ -716,7 +615,8 @@ export class McpServerManager {
|
|
|
716
615
|
}
|
|
717
616
|
accepted.add(elicitationId);
|
|
718
617
|
}
|
|
719
|
-
createHttpTransport(definition, serverName, client, legacySse = false, implicitOAuth = false, onAuthChallenge) {
|
|
618
|
+
createHttpTransport(connectionRuntime, definition, serverName, client, legacySse = false, implicitOAuth = false, onAuthChallenge) {
|
|
619
|
+
const { StreamableHTTPClientTransport, SSEClientTransport, auth, extractWWWAuthenticateParams, UnauthorizedError, McpOAuthProvider, extractOAuthConfig, isOAuthChallenge, recordOAuthChallenge, getOAuthRequest, bindOAuthRequestIssuer, trackToolTransport, trackToolTransportFailure, } = connectionRuntime;
|
|
720
620
|
const serverUrl = resolveServerUrl(definition);
|
|
721
621
|
const url = new URL(serverUrl);
|
|
722
622
|
const storageBase = getAuthBaseDir(this.authStorageOptions);
|
|
@@ -901,8 +801,8 @@ export class McpServerManager {
|
|
|
901
801
|
return [];
|
|
902
802
|
}
|
|
903
803
|
}
|
|
904
|
-
attachAdapterNotificationHandlers(serverName, client) {
|
|
905
|
-
client.setNotificationHandler(
|
|
804
|
+
attachAdapterNotificationHandlers(serverName, client, schema) {
|
|
805
|
+
client.setNotificationHandler(schema.shape.method.value, { params: schema.shape.params }, params => {
|
|
906
806
|
this.uiStreamListeners.get(params.streamToken)?.(serverName, params);
|
|
907
807
|
});
|
|
908
808
|
}
|
package/dist/tool-loader.js
CHANGED
|
@@ -13,6 +13,7 @@ const reserved = new Set(["read", "bash", "powershell", "edit", "write", "grep",
|
|
|
13
13
|
export function createToolLoader(pi, getState, getInitPromise, beforeExecute) {
|
|
14
14
|
pi.registerEntryRenderer(ENTRY, () => undefined);
|
|
15
15
|
const registered = new Map();
|
|
16
|
+
const fingerprints = new WeakMap();
|
|
16
17
|
let available = new Set();
|
|
17
18
|
let selected = new Map();
|
|
18
19
|
let inactive = new Map();
|
|
@@ -118,11 +119,12 @@ export function createToolLoader(pi, getState, getInitPromise, beforeExecute) {
|
|
|
118
119
|
const metadata = getState()?.toolMetadata.get(server) ?? (entry && isServerCacheValid(entry, definition)
|
|
119
120
|
? reconstructToolMetadata(server, entry, config.settings?.toolPrefix ?? "server", definition) : []);
|
|
120
121
|
const selection = directToolSelection(config, server, env ?? undefined);
|
|
121
|
-
for (const
|
|
122
|
+
for (const source of metadata) {
|
|
123
|
+
const { name: prefixedName, ...tool } = source;
|
|
122
124
|
if (tool.resourceUri)
|
|
123
125
|
continue;
|
|
124
126
|
const pinned = selection === true || (Array.isArray(selection) && selection.includes(tool.originalName));
|
|
125
|
-
specs.push({ spec: { ...tool, prefixedName, serverName: server }, selection: { server, tool: tool.originalName }, name: prefixedName, pinned });
|
|
127
|
+
specs.push({ spec: { ...tool, prefixedName, serverName: server }, source, selection: { server, tool: tool.originalName }, name: prefixedName, pinned });
|
|
126
128
|
}
|
|
127
129
|
}
|
|
128
130
|
const counts = new Map();
|
|
@@ -130,13 +132,17 @@ export function createToolLoader(pi, getState, getInitPromise, beforeExecute) {
|
|
|
130
132
|
counts.set(name, (counts.get(name) ?? 0) + 1);
|
|
131
133
|
available = new Set();
|
|
132
134
|
const enabled = [];
|
|
133
|
-
for (const { spec, selection, name, pinned } of specs) {
|
|
135
|
+
for (const { spec, source, selection, name, pinned } of specs) {
|
|
134
136
|
const id = key(selection);
|
|
135
137
|
if (reserved.has(name) || counts.get(name) > 1 || (existing.has(name) && !previousNames.has(name)))
|
|
136
138
|
continue;
|
|
137
139
|
available.add(id);
|
|
138
140
|
const instructions = getState()?.serverInstructions.get(spec.serverName) ?? cache?.servers[spec.serverName]?.instructions;
|
|
139
|
-
const
|
|
141
|
+
const previous = Object.isFrozen(source) ? fingerprints.get(source) : undefined;
|
|
142
|
+
const fingerprint = previous?.server === spec.serverName && previous.instructions === instructions
|
|
143
|
+
? previous.value : JSON.stringify({ spec, name, instructions });
|
|
144
|
+
if (Object.isFrozen(source))
|
|
145
|
+
fingerprints.set(source, { server: spec.serverName, instructions, value: fingerprint });
|
|
140
146
|
if (registered.get(id)?.fingerprint !== fingerprint) {
|
|
141
147
|
pi.registerTool({
|
|
142
148
|
name,
|