@fitchmultz/pi-mcp-adapter 8.0.6 → 8.0.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +128 -8
- package/OAUTH.md +13 -16
- package/README.md +8 -8
- package/cli.js +67 -28
- package/dist/abort.js +18 -9
- package/dist/client-operations.js +4 -2
- package/dist/commands.js +77 -43
- package/dist/config.js +194 -105
- package/dist/connection-runtime.d.ts +2 -2
- package/dist/connection-runtime.js +29 -18
- package/dist/consent-manager.d.ts +2 -2
- package/dist/consent-manager.js +5 -2
- package/dist/direct-tools.d.ts +1 -1
- package/dist/direct-tools.js +13 -7
- package/dist/elicitation-handler.d.ts +2 -3
- package/dist/elicitation-handler.js +113 -58
- package/dist/gateway-arguments.d.ts +1 -1
- package/dist/gateway-arguments.js +90 -23
- package/dist/glimpse-ui.js +10 -5
- package/dist/host-html-template.d.ts +3 -1
- package/dist/host-html-template.js +9 -12
- package/dist/index.d.ts +1 -1
- package/dist/index.js +264 -119
- package/dist/init.d.ts +1 -3
- package/dist/init.js +91 -51
- package/dist/legacy-migration.d.ts +1 -1
- package/dist/legacy-migration.js +17 -12
- package/dist/lifecycle.d.ts +3 -3
- package/dist/lifecycle.js +33 -17
- package/dist/logger.d.ts +2 -2
- package/dist/logger.js +4 -2
- package/dist/mcp-auth-flow.js +150 -58
- package/dist/mcp-auth.d.ts +7 -7
- package/dist/mcp-auth.js +189 -138
- package/dist/mcp-callback-server.js +19 -8
- package/dist/mcp-code.js +168 -54
- package/dist/mcp-keyring-helper.cjs +57 -23
- package/dist/mcp-oauth-provider.d.ts +6 -7
- package/dist/mcp-oauth-provider.js +146 -72
- package/dist/mcp-oauth-runtime.js +29 -16
- package/dist/mcp-output-guard.js +193 -65
- package/dist/mcp-output-preview.js +41 -20
- package/dist/mcp-panel.d.ts +9 -9
- package/dist/mcp-panel.js +141 -70
- package/dist/mcp-script-worker.mjs +135 -43
- package/dist/mcp-setup-panel.d.ts +8 -8
- package/dist/mcp-setup-panel.js +130 -44
- package/dist/mcp-status.js +14 -7
- package/dist/mcp-trace.d.ts +10 -5
- package/dist/mcp-trace.js +127 -61
- package/dist/metadata-cache.d.ts +3 -4
- package/dist/metadata-cache.js +83 -43
- package/dist/npx-resolver.js +101 -60
- package/dist/onboarding-state.js +4 -2
- package/dist/plain-result-reader.js +78 -31
- package/dist/prompts.js +36 -20
- package/dist/proxy-modes.js +670 -193
- package/dist/resource-tools.js +30 -9
- package/dist/runtime-owner.js +36 -18
- package/dist/sampling-handler.d.ts +1 -2
- package/dist/sampling-handler.js +41 -21
- package/dist/search-ranking.d.ts +1 -1
- package/dist/search-ranking.js +67 -34
- package/dist/server-manager.d.ts +11 -11
- package/dist/server-manager.js +283 -166
- package/dist/session-recovery.js +113 -61
- package/dist/tool-approval.js +9 -3
- package/dist/tool-loader.js +154 -55
- package/dist/tool-metadata.d.ts +1 -1
- package/dist/tool-metadata.js +51 -23
- package/dist/tool-registrar.js +32 -10
- package/dist/tool-result-renderer.js +58 -33
- package/dist/types.d.ts +2 -2
- package/dist/types.js +39 -18
- package/dist/ui-message-history.js +33 -13
- package/dist/ui-resource-handler.d.ts +3 -3
- package/dist/ui-resource-handler.js +37 -21
- package/dist/ui-server.js +101 -43
- package/dist/ui-session.js +66 -29
- package/dist/ui-stream-types.js +14 -4
- package/dist/ui-tool-visibility.js +12 -6
- package/dist/unix-socket-transport.d.ts +1 -2
- package/dist/unix-socket-transport.js +19 -12
- package/dist/utils.d.ts +2 -0
- package/dist/utils.js +60 -35
- package/package.json +26 -2
- package/skills/mcp-scripting/SKILL.md +6 -6
package/dist/mcp-trace.js
CHANGED
|
@@ -5,8 +5,9 @@ export const MCP_TRACE_SCHEMA_VERSION = 1;
|
|
|
5
5
|
export const DEFAULT_MCP_TRACE_MAX_BYTES = 256 * 1024;
|
|
6
6
|
export const DEFAULT_MCP_TRACE_MAX_EVENTS = 10_000;
|
|
7
7
|
function boundedPositiveInteger(value, fallback) {
|
|
8
|
-
if (!Number.isFinite(value) || value === undefined || value <= 0)
|
|
8
|
+
if (!Number.isFinite(value) || value === undefined || value <= 0) {
|
|
9
9
|
return fallback;
|
|
10
|
+
}
|
|
10
11
|
return Math.floor(value);
|
|
11
12
|
}
|
|
12
13
|
/** Redact strings before they can become durable trace metadata. */
|
|
@@ -18,22 +19,27 @@ export function redactTraceText(value, maxLength = 160) {
|
|
|
18
19
|
.replace(/\b[a-z][a-z\d+.-]*:\/\/[^\s"'<>]+/gi, "[REDACTED_URL]")
|
|
19
20
|
.replace(/\b(?:bearer|basic)\s+[A-Za-z0-9._~+/=-]+/gi, "[REDACTED_AUTH]")
|
|
20
21
|
.replace(/\b(?:token|secret|password|passwd|api[_-]?key|authorization|cookie)\s*[:=]\s*[^\s,;]+/gi, "$1=[REDACTED]");
|
|
21
|
-
if (redacted.length > maxLength)
|
|
22
|
+
if (redacted.length > maxLength) {
|
|
22
23
|
redacted = `${redacted.slice(0, maxLength - 1)}…`;
|
|
24
|
+
}
|
|
23
25
|
return redacted;
|
|
24
26
|
}
|
|
25
27
|
function messageKind(message) {
|
|
26
|
-
if ("method" in message)
|
|
28
|
+
if ("method" in message) {
|
|
27
29
|
return "id" in message ? "request" : "notification";
|
|
30
|
+
}
|
|
28
31
|
return "response";
|
|
29
32
|
}
|
|
30
33
|
function traceId(value) {
|
|
31
|
-
if (value === null)
|
|
34
|
+
if (value === null) {
|
|
32
35
|
return null;
|
|
33
|
-
|
|
36
|
+
}
|
|
37
|
+
if (typeof value === "number" && Number.isFinite(value)) {
|
|
34
38
|
return value;
|
|
35
|
-
|
|
39
|
+
}
|
|
40
|
+
if (typeof value === "string") {
|
|
36
41
|
return "[REDACTED_ID]";
|
|
42
|
+
}
|
|
37
43
|
return undefined;
|
|
38
44
|
}
|
|
39
45
|
function messageBytes(message) {
|
|
@@ -44,33 +50,49 @@ function messageBytes(message) {
|
|
|
44
50
|
return undefined;
|
|
45
51
|
}
|
|
46
52
|
}
|
|
47
|
-
export function createMcpTraceEvent(
|
|
48
|
-
const kind = messageKind(message);
|
|
49
|
-
const bytes = messageBytes(message);
|
|
50
|
-
|
|
53
|
+
export function createMcpTraceEvent(request) {
|
|
54
|
+
const kind = messageKind(request.message);
|
|
55
|
+
const bytes = messageBytes(request.message);
|
|
56
|
+
return {
|
|
51
57
|
version: MCP_TRACE_SCHEMA_VERSION,
|
|
52
58
|
timestamp: new Date().toISOString(),
|
|
53
|
-
direction,
|
|
54
|
-
server: redactTraceText(server, 120),
|
|
55
|
-
transport,
|
|
59
|
+
direction: request.direction,
|
|
60
|
+
server: redactTraceText(request.server, 120),
|
|
61
|
+
transport: request.transport,
|
|
56
62
|
kind,
|
|
57
|
-
status,
|
|
63
|
+
status: request.status,
|
|
58
64
|
...(bytes !== undefined ? { bytes } : {}),
|
|
65
|
+
...traceMessageMetadata(request.message),
|
|
66
|
+
...traceOutcomeMetadata(request),
|
|
59
67
|
};
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
}
|
|
73
|
-
|
|
68
|
+
}
|
|
69
|
+
function traceMessageMetadata(message) {
|
|
70
|
+
const fields = {};
|
|
71
|
+
if ("method" in message) {
|
|
72
|
+
fields.method = redactTraceText(message.method, 120);
|
|
73
|
+
}
|
|
74
|
+
if ("id" in message) {
|
|
75
|
+
fields.id = traceId(message.id) ?? null;
|
|
76
|
+
}
|
|
77
|
+
return fields;
|
|
78
|
+
}
|
|
79
|
+
function traceOutcomeMetadata(request) {
|
|
80
|
+
const fields = {};
|
|
81
|
+
const relatedRequestId = traceId(request.relatedRequestId);
|
|
82
|
+
if (relatedRequestId !== undefined && relatedRequestId !== null) {
|
|
83
|
+
fields.relatedRequestId = relatedRequestId;
|
|
84
|
+
}
|
|
85
|
+
const wireError = "error" in request.message ? request.message.error : undefined;
|
|
86
|
+
if (typeof wireError === "object" &&
|
|
87
|
+
wireError !== null &&
|
|
88
|
+
"code" in wireError &&
|
|
89
|
+
typeof wireError.code === "number") {
|
|
90
|
+
fields.errorCode = wireError.code;
|
|
91
|
+
}
|
|
92
|
+
if (request.durationMs !== undefined && Number.isFinite(request.durationMs)) {
|
|
93
|
+
fields.durationMs = Math.max(0, Math.round(request.durationMs * 100) / 100);
|
|
94
|
+
}
|
|
95
|
+
return fields;
|
|
74
96
|
}
|
|
75
97
|
export class McpTraceWriter {
|
|
76
98
|
maxBytes;
|
|
@@ -90,19 +112,25 @@ export class McpTraceWriter {
|
|
|
90
112
|
this.options = options;
|
|
91
113
|
this.maxBytes = boundedPositiveInteger(options.maxBytes, DEFAULT_MCP_TRACE_MAX_BYTES);
|
|
92
114
|
this.maxEvents = boundedPositiveInteger(options.maxEvents, DEFAULT_MCP_TRACE_MAX_EVENTS);
|
|
93
|
-
this.append =
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
115
|
+
this.append =
|
|
116
|
+
options.appendFile ??
|
|
117
|
+
(async (path, data, appendOptions) => {
|
|
118
|
+
await appendFile(path, data, appendOptions);
|
|
119
|
+
});
|
|
120
|
+
this.resetFile =
|
|
121
|
+
options.writeFile ??
|
|
122
|
+
(async (path, data, writeOptions) => {
|
|
123
|
+
await writeFile(path, data, writeOptions);
|
|
124
|
+
});
|
|
125
|
+
this.makeDirectory =
|
|
126
|
+
options.mkdir ??
|
|
127
|
+
(async (path) => {
|
|
128
|
+
await mkdir(path, { recursive: true });
|
|
129
|
+
return;
|
|
130
|
+
});
|
|
103
131
|
this.fileReady = this.makeDirectory(dirname(this.options.filePath), { recursive: true })
|
|
104
132
|
.then(() => this.resetFile(this.options.filePath, "", { encoding: "utf8" }))
|
|
105
|
-
.catch(error => {
|
|
133
|
+
.catch((error) => {
|
|
106
134
|
// Tracing must never change MCP request/response behavior.
|
|
107
135
|
this.persistenceError = error;
|
|
108
136
|
this.initializationFailed = true;
|
|
@@ -119,8 +147,9 @@ export class McpTraceWriter {
|
|
|
119
147
|
return { bytes: this.bytesWritten, events: this.eventsWritten };
|
|
120
148
|
}
|
|
121
149
|
write(event) {
|
|
122
|
-
if (this.disabled || this.eventsWritten >= this.maxEvents)
|
|
150
|
+
if (this.disabled || this.eventsWritten >= this.maxEvents) {
|
|
123
151
|
return;
|
|
152
|
+
}
|
|
124
153
|
let line;
|
|
125
154
|
try {
|
|
126
155
|
line = `${JSON.stringify(event)}\n`;
|
|
@@ -136,12 +165,16 @@ export class McpTraceWriter {
|
|
|
136
165
|
}
|
|
137
166
|
this.bytesWritten += bytes;
|
|
138
167
|
this.eventsWritten += 1;
|
|
139
|
-
this.queue = this.queue
|
|
168
|
+
this.queue = this.queue
|
|
169
|
+
.then(async () => {
|
|
140
170
|
await this.fileReady;
|
|
141
|
-
if (this.initializationFailed)
|
|
171
|
+
if (this.initializationFailed) {
|
|
142
172
|
return;
|
|
173
|
+
}
|
|
143
174
|
await this.append(this.options.filePath, line, { encoding: "utf8" });
|
|
144
|
-
|
|
175
|
+
return;
|
|
176
|
+
})
|
|
177
|
+
.catch((error) => {
|
|
145
178
|
// Tracing must never change MCP request/response behavior.
|
|
146
179
|
this.persistenceError ??= error;
|
|
147
180
|
this.disabled = true;
|
|
@@ -150,16 +183,24 @@ export class McpTraceWriter {
|
|
|
150
183
|
async flush(strict = false) {
|
|
151
184
|
await this.fileReady;
|
|
152
185
|
await this.queue;
|
|
153
|
-
if (strict && this.persistenceError !== undefined)
|
|
186
|
+
if (strict && this.persistenceError !== undefined) {
|
|
187
|
+
// oxlint-disable-next-line typescript/only-throw-error -- QX-009: preserve the recorded persistence failure.
|
|
154
188
|
throw this.persistenceError;
|
|
189
|
+
}
|
|
155
190
|
}
|
|
156
191
|
}
|
|
157
192
|
export function createMcpTraceWriter(sessionCwd, settings = {}, randomSuffix = Math.random().toString(36).slice(2, 10)) {
|
|
158
193
|
const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
|
|
159
194
|
const configuredPath = settings.file;
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
195
|
+
let filePath;
|
|
196
|
+
if (typeof configuredPath === "string" && configuredPath !== "") {
|
|
197
|
+
filePath = isAbsolute(configuredPath)
|
|
198
|
+
? configuredPath
|
|
199
|
+
: resolve(sessionCwd ?? process.cwd(), configuredPath);
|
|
200
|
+
}
|
|
201
|
+
else {
|
|
202
|
+
filePath = resolve(sessionCwd ?? process.cwd(), ".pi", "fitch-mcp-adapter", "traces", `mcp-${timestamp}-${randomSuffix}.jsonl`);
|
|
203
|
+
}
|
|
163
204
|
return new McpTraceWriter({
|
|
164
205
|
filePath,
|
|
165
206
|
...(settings.maxBytes !== undefined ? { maxBytes: settings.maxBytes } : {}),
|
|
@@ -184,23 +225,34 @@ export function wrapTransportWithMcpTrace(transport, server, transportKind, obse
|
|
|
184
225
|
// An observer failure must never alter SDK transport behavior.
|
|
185
226
|
}
|
|
186
227
|
};
|
|
187
|
-
//
|
|
188
|
-
// hasPerRequestStream
|
|
228
|
+
// The SDK requires the native stdio prototype for sibling probing and reads
|
|
229
|
+
// HTTP hasPerRequestStream for cancellation. Compose callbacks in place.
|
|
189
230
|
const send = transport.send.bind(transport);
|
|
231
|
+
// oxlint-disable-next-line eslint/no-param-reassign -- QX-001: SDK invokes this member on the instance it holds.
|
|
190
232
|
transport.send = async (message, options) => {
|
|
191
233
|
const started = performance.now();
|
|
192
234
|
const messages = Array.isArray(message) ? message : [message];
|
|
193
235
|
try {
|
|
194
236
|
await send(message, options);
|
|
195
237
|
for (const item of messages) {
|
|
196
|
-
record(createMcpTraceEvent(
|
|
238
|
+
record(createMcpTraceEvent({
|
|
239
|
+
direction: "outbound",
|
|
240
|
+
server,
|
|
241
|
+
transport: transportKind,
|
|
242
|
+
message: item,
|
|
243
|
+
status: "sent",
|
|
197
244
|
durationMs: performance.now() - started,
|
|
198
245
|
}));
|
|
199
246
|
}
|
|
200
247
|
}
|
|
201
248
|
catch (error) {
|
|
202
249
|
for (const item of messages) {
|
|
203
|
-
record(createMcpTraceEvent(
|
|
250
|
+
record(createMcpTraceEvent({
|
|
251
|
+
direction: "outbound",
|
|
252
|
+
server,
|
|
253
|
+
transport: transportKind,
|
|
254
|
+
message: item,
|
|
255
|
+
status: "error",
|
|
204
256
|
durationMs: performance.now() - started,
|
|
205
257
|
}));
|
|
206
258
|
}
|
|
@@ -211,23 +263,37 @@ export function wrapTransportWithMcpTrace(transport, server, transportKind, obse
|
|
|
211
263
|
configurable: true,
|
|
212
264
|
get: () => messageHandler,
|
|
213
265
|
set: (handler) => {
|
|
214
|
-
messageHandler = handler
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
266
|
+
messageHandler = handler
|
|
267
|
+
? (message, extra) => {
|
|
268
|
+
record(createMcpTraceEvent({
|
|
269
|
+
direction: "inbound",
|
|
270
|
+
server,
|
|
271
|
+
transport: transportKind,
|
|
272
|
+
message,
|
|
273
|
+
status: "received",
|
|
274
|
+
}));
|
|
275
|
+
handler(message, extra);
|
|
276
|
+
}
|
|
277
|
+
: undefined;
|
|
218
278
|
},
|
|
219
279
|
});
|
|
220
280
|
return transport;
|
|
221
281
|
}
|
|
222
282
|
export function traceTransportKind(definition, transport) {
|
|
223
|
-
if (definition.command)
|
|
283
|
+
if (typeof definition.command === "string" && definition.command !== "") {
|
|
224
284
|
return "stdio";
|
|
225
|
-
|
|
285
|
+
}
|
|
286
|
+
if (typeof definition.socket === "string" && definition.socket !== "") {
|
|
226
287
|
return "unix-socket";
|
|
227
|
-
|
|
228
|
-
|
|
288
|
+
}
|
|
289
|
+
const constructorName = transport.constructor.name.toLowerCase();
|
|
290
|
+
if (constructorName.includes("sse")) {
|
|
229
291
|
return "sse";
|
|
230
|
-
|
|
292
|
+
}
|
|
293
|
+
if (constructorName.includes("streamable")) {
|
|
231
294
|
return "streamable-http";
|
|
232
|
-
|
|
295
|
+
}
|
|
296
|
+
return typeof definition.url === "string" && definition.url !== ""
|
|
297
|
+
? "streamable-http"
|
|
298
|
+
: "unknown";
|
|
233
299
|
}
|
package/dist/metadata-cache.d.ts
CHANGED
|
@@ -1,11 +1,10 @@
|
|
|
1
|
-
import type
|
|
2
|
-
|
|
3
|
-
export type { CachedPrompt, CachedResource, CachedTool, MetadataCache, ServerCacheEntry } from "./types.ts";
|
|
1
|
+
import { type CachedPrompt, type CachedResource, type CachedTool, type McpConfig, type McpTool, type McpResource, type McpPrompt, type MetadataCache, type ServerCacheEntry, type ServerEntry, type ToolMetadata, type PromptMetadata, type ToolPrefix } from "./types.ts";
|
|
2
|
+
export type { CachedPrompt, CachedResource, CachedTool, MetadataCache, ServerCacheEntry, } from "./types.ts";
|
|
4
3
|
export declare function getMetadataCachePath(): string;
|
|
5
4
|
export declare function loadMetadataCache(enabled?: boolean): MetadataCache | null;
|
|
6
5
|
export declare function saveMetadataCache(cache: MetadataCache, enabled?: boolean, selectedServers?: Iterable<string>): void;
|
|
7
6
|
export declare function computeServerHash(definition: ServerEntry): string;
|
|
8
|
-
export declare function isServerCacheValid(entry:
|
|
7
|
+
export declare function isServerCacheValid(entry: unknown, definition: ServerEntry, maxAgeMs?: number): boolean;
|
|
9
8
|
export declare function parseDirectToolSelectors(selectors: string[]): {
|
|
10
9
|
servers: Set<string>;
|
|
11
10
|
tools: Map<string, Set<string>>;
|
package/dist/metadata-cache.js
CHANGED
|
@@ -3,7 +3,7 @@ import { existsSync, readFileSync, writeFileSync, renameSync, mkdirSync, statSyn
|
|
|
3
3
|
import { dirname } from "node:path";
|
|
4
4
|
import { getAdapterPath } from "./agent-dir.js";
|
|
5
5
|
import { createHash } from "node:crypto";
|
|
6
|
-
import { formatPromptCommandName, isServerDisabled, resolveToolPrefix } from "./types.js";
|
|
6
|
+
import { formatPromptCommandName, isServerDisabled, resolveToolPrefix, } from "./types.js";
|
|
7
7
|
import { buildToolMetadata } from "./tool-metadata.js";
|
|
8
8
|
import { interpolateEnvRecord, resolveBearerToken, resolveConfigPath, resolveServerUrl, } from "./utils.js";
|
|
9
9
|
// Version 2 stores raw descriptors; version 1 readers cannot enforce raw UI visibility.
|
|
@@ -17,8 +17,9 @@ function cacheRevision(path) {
|
|
|
17
17
|
}
|
|
18
18
|
function freezeMetadata(value) {
|
|
19
19
|
if (value && typeof value === "object" && !Object.isFrozen(value)) {
|
|
20
|
-
for (const child of Object.values(value))
|
|
20
|
+
for (const child of Object.values(value)) {
|
|
21
21
|
freezeMetadata(child);
|
|
22
|
+
}
|
|
22
23
|
Object.freeze(value);
|
|
23
24
|
}
|
|
24
25
|
return value;
|
|
@@ -27,20 +28,28 @@ export function getMetadataCachePath() {
|
|
|
27
28
|
return getAdapterPath("mcp-cache.json");
|
|
28
29
|
}
|
|
29
30
|
export function loadMetadataCache(enabled = true) {
|
|
30
|
-
if (!enabled)
|
|
31
|
+
if (!enabled) {
|
|
31
32
|
return null;
|
|
33
|
+
}
|
|
32
34
|
const cachePath = getMetadataCachePath();
|
|
33
35
|
try {
|
|
34
36
|
const revision = cacheRevision(cachePath);
|
|
35
|
-
if (snapshot?.path === cachePath && snapshot.revision === revision)
|
|
37
|
+
if (snapshot?.path === cachePath && snapshot.revision === revision) {
|
|
36
38
|
return snapshot.cache;
|
|
39
|
+
}
|
|
37
40
|
const raw = JSON.parse(readFileSync(cachePath, "utf-8"));
|
|
38
|
-
const cache = raw &&
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
41
|
+
const cache = raw &&
|
|
42
|
+
typeof raw === "object" &&
|
|
43
|
+
!Array.isArray(raw) &&
|
|
44
|
+
(raw.version === 1 || raw.version === CACHE_VERSION) &&
|
|
45
|
+
raw.servers &&
|
|
46
|
+
typeof raw.servers === "object" &&
|
|
47
|
+
!Array.isArray(raw.servers)
|
|
48
|
+
? freezeMetadata(raw)
|
|
49
|
+
: null;
|
|
42
50
|
// A concurrent rename must not associate the old bytes with the new file's revision.
|
|
43
|
-
snapshot =
|
|
51
|
+
snapshot =
|
|
52
|
+
cacheRevision(cachePath) === revision ? { path: cachePath, revision, cache } : undefined;
|
|
44
53
|
return cache;
|
|
45
54
|
}
|
|
46
55
|
catch {
|
|
@@ -49,16 +58,19 @@ export function loadMetadataCache(enabled = true) {
|
|
|
49
58
|
}
|
|
50
59
|
}
|
|
51
60
|
export function saveMetadataCache(cache, enabled = true, selectedServers = []) {
|
|
52
|
-
if (!enabled)
|
|
61
|
+
if (!enabled) {
|
|
53
62
|
return;
|
|
63
|
+
}
|
|
54
64
|
const cachePath = getMetadataCachePath();
|
|
55
65
|
const dir = dirname(cachePath);
|
|
56
66
|
mkdirSync(dir, { recursive: true });
|
|
57
|
-
|
|
67
|
+
const merged = { version: CACHE_VERSION, servers: {} };
|
|
58
68
|
try {
|
|
59
69
|
if (existsSync(cachePath)) {
|
|
60
70
|
const existing = JSON.parse(readFileSync(cachePath, "utf-8"));
|
|
61
|
-
if (existing &&
|
|
71
|
+
if (existing &&
|
|
72
|
+
(existing.version === 1 || existing.version === CACHE_VERSION) &&
|
|
73
|
+
existing.servers) {
|
|
62
74
|
merged.servers = { ...existing.servers };
|
|
63
75
|
}
|
|
64
76
|
}
|
|
@@ -71,8 +83,9 @@ export function saveMetadataCache(cache, enabled = true, selectedServers = []) {
|
|
|
71
83
|
const protectedServers = new Set([...selectedServers, ...Object.keys(cache.servers)]);
|
|
72
84
|
const now = Date.now();
|
|
73
85
|
for (const [name, entry] of Object.entries(merged.servers)) {
|
|
74
|
-
if (!protectedServers.has(name) &&
|
|
75
|
-
|
|
86
|
+
if (!protectedServers.has(name) &&
|
|
87
|
+
isCacheEntry(entry) &&
|
|
88
|
+
(entry.cachedAt > now || now - entry.cachedAt > CACHE_MAX_AGE_MS)) {
|
|
76
89
|
delete merged.servers[name];
|
|
77
90
|
}
|
|
78
91
|
}
|
|
@@ -110,23 +123,30 @@ export function isServerCacheValid(entry, definition, maxAgeMs = CACHE_MAX_AGE_M
|
|
|
110
123
|
catch {
|
|
111
124
|
return false;
|
|
112
125
|
}
|
|
113
|
-
if (!isCacheEntry(entry) || entry.configHash !== configHash)
|
|
126
|
+
if (!isCacheEntry(entry) || entry.configHash !== configHash) {
|
|
114
127
|
return false;
|
|
115
|
-
|
|
128
|
+
}
|
|
129
|
+
if (entry.cachedAt > Date.now()) {
|
|
116
130
|
return false;
|
|
117
|
-
|
|
131
|
+
}
|
|
132
|
+
if (maxAgeMs > 0 && Date.now() - entry.cachedAt > maxAgeMs) {
|
|
118
133
|
return false;
|
|
134
|
+
}
|
|
119
135
|
return true;
|
|
120
136
|
}
|
|
121
137
|
function isCacheEntry(entry) {
|
|
122
|
-
if (!entry || typeof entry !== "object" || Array.isArray(entry))
|
|
138
|
+
if (!entry || typeof entry !== "object" || Array.isArray(entry)) {
|
|
123
139
|
return false;
|
|
140
|
+
}
|
|
124
141
|
const value = entry;
|
|
125
|
-
return typeof value.configHash === "string"
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
142
|
+
return (typeof value.configHash === "string" &&
|
|
143
|
+
typeof value.cachedAt === "number" &&
|
|
144
|
+
Number.isFinite(value.cachedAt) &&
|
|
145
|
+
value.cachedAt > 0 &&
|
|
146
|
+
Array.isArray(value.tools) &&
|
|
147
|
+
Array.isArray(value.resources) &&
|
|
148
|
+
(value.prompts === undefined || Array.isArray(value.prompts)) &&
|
|
149
|
+
(value.instructions === undefined || typeof value.instructions === "string"));
|
|
130
150
|
}
|
|
131
151
|
export function parseDirectToolSelectors(selectors) {
|
|
132
152
|
const servers = new Set();
|
|
@@ -155,15 +175,17 @@ export function getMissingConfiguredDirectToolServers(config, cache, envOverride
|
|
|
155
175
|
const globalDirect = config.settings?.directTools;
|
|
156
176
|
const envSelection = envOverride ? parseDirectToolSelectors(envOverride) : null;
|
|
157
177
|
for (const [serverName, definition] of Object.entries(config.mcpServers)) {
|
|
158
|
-
if (isServerDisabled(definition))
|
|
178
|
+
if (isServerDisabled(definition)) {
|
|
159
179
|
continue;
|
|
180
|
+
}
|
|
160
181
|
const hasDirectTools = envSelection
|
|
161
182
|
? envSelection.servers.has(serverName) || envSelection.tools.has(serverName)
|
|
162
183
|
: definition.directTools !== undefined
|
|
163
|
-
?
|
|
164
|
-
:
|
|
165
|
-
if (!hasDirectTools)
|
|
184
|
+
? Boolean(definition.directTools)
|
|
185
|
+
: Boolean(globalDirect);
|
|
186
|
+
if (!hasDirectTools) {
|
|
166
187
|
continue;
|
|
188
|
+
}
|
|
167
189
|
const serverCache = cache?.servers?.[serverName];
|
|
168
190
|
if (!serverCache || !isServerCacheValid(serverCache, definition)) {
|
|
169
191
|
missing.push(serverName);
|
|
@@ -172,19 +194,29 @@ export function getMissingConfiguredDirectToolServers(config, cache, envOverride
|
|
|
172
194
|
return missing;
|
|
173
195
|
}
|
|
174
196
|
export function reconstructToolMetadata(serverName, entry, prefix, definition) {
|
|
175
|
-
const key = JSON.stringify([
|
|
176
|
-
|
|
197
|
+
const key = JSON.stringify([
|
|
198
|
+
serverName,
|
|
199
|
+
prefix,
|
|
200
|
+
definition.exposeResources,
|
|
201
|
+
definition.includeTools,
|
|
202
|
+
definition.excludeTools,
|
|
203
|
+
definition.toolPrefix,
|
|
204
|
+
definition.disabled,
|
|
205
|
+
]);
|
|
177
206
|
const previous = toolProjections.get(entry);
|
|
178
|
-
if (Object.isFrozen(entry) && previous?.key === key)
|
|
207
|
+
if (Object.isFrozen(entry) && previous?.key === key) {
|
|
179
208
|
return previous.metadata;
|
|
180
|
-
|
|
181
|
-
|
|
209
|
+
}
|
|
210
|
+
const metadata = buildToolMetadata((entry.tools ?? []).filter((tool) => tool?.name).map(restoreCachedTool), entry.resources ?? [], definition, serverName, prefix).metadata;
|
|
211
|
+
if (Object.isFrozen(entry)) {
|
|
182
212
|
toolProjections.set(entry, { key, metadata: freezeMetadata(metadata) });
|
|
213
|
+
}
|
|
183
214
|
return metadata;
|
|
184
215
|
}
|
|
185
216
|
function restoreCachedTool(tool) {
|
|
186
217
|
const { uiResourceUri, uiVisibility, uiStreamMode, ...descriptor } = tool;
|
|
187
|
-
if (descriptor._meta !== undefined ||
|
|
218
|
+
if (descriptor._meta !== undefined ||
|
|
219
|
+
(uiResourceUri === undefined && uiVisibility === undefined && uiStreamMode === undefined)) {
|
|
188
220
|
return descriptor;
|
|
189
221
|
}
|
|
190
222
|
return {
|
|
@@ -199,23 +231,27 @@ function restoreCachedTool(tool) {
|
|
|
199
231
|
};
|
|
200
232
|
}
|
|
201
233
|
export function serializeTools(tools) {
|
|
202
|
-
return tools.filter(tool => tool?.name);
|
|
234
|
+
return tools.filter((tool) => tool?.name);
|
|
203
235
|
}
|
|
204
236
|
export function serializeResources(resources) {
|
|
205
|
-
return resources.filter(resource => resource?.name && resource?.uri);
|
|
237
|
+
return resources.filter((resource) => resource?.name && resource?.uri);
|
|
206
238
|
}
|
|
207
239
|
export function serializePrompts(prompts) {
|
|
208
240
|
return (prompts ?? [])
|
|
209
|
-
.filter(prompt => prompt?.name)
|
|
210
|
-
.map(prompt => ({
|
|
241
|
+
.filter((prompt) => prompt?.name)
|
|
242
|
+
.map((prompt) => ({
|
|
211
243
|
name: prompt.name,
|
|
212
244
|
...(prompt.title !== undefined ? { title: prompt.title } : {}),
|
|
213
245
|
...(prompt.description !== undefined ? { description: prompt.description } : {}),
|
|
214
246
|
...(Array.isArray(prompt.arguments)
|
|
215
247
|
? {
|
|
216
|
-
arguments: prompt.arguments
|
|
248
|
+
arguments: prompt.arguments
|
|
249
|
+
.filter((argument) => argument?.name)
|
|
250
|
+
.map((argument) => ({
|
|
217
251
|
name: argument.name,
|
|
218
|
-
...(argument.description !== undefined
|
|
252
|
+
...(argument.description !== undefined
|
|
253
|
+
? { description: argument.description }
|
|
254
|
+
: {}),
|
|
219
255
|
...(argument.required !== undefined ? { required: argument.required } : {}),
|
|
220
256
|
})),
|
|
221
257
|
}
|
|
@@ -224,9 +260,13 @@ export function serializePrompts(prompts) {
|
|
|
224
260
|
}
|
|
225
261
|
export function reconstructPromptMetadata(serverName, prompts, prefix, definition) {
|
|
226
262
|
const effectivePrefix = resolveToolPrefix(definition, prefix);
|
|
227
|
-
return (prompts ?? [])
|
|
263
|
+
return (prompts ?? [])
|
|
264
|
+
.filter((prompt) => prompt?.name)
|
|
265
|
+
.map((prompt) => {
|
|
228
266
|
const args = Array.isArray(prompt.arguments)
|
|
229
|
-
? prompt.arguments
|
|
267
|
+
? prompt.arguments
|
|
268
|
+
.filter((argument) => argument?.name)
|
|
269
|
+
.map((argument) => ({
|
|
230
270
|
name: argument.name,
|
|
231
271
|
...(argument.description !== undefined ? { description: argument.description } : {}),
|
|
232
272
|
...(argument.required !== undefined ? { required: argument.required } : {}),
|
|
@@ -248,9 +288,9 @@ function stableStringify(value) {
|
|
|
248
288
|
return serialized === undefined ? "undefined" : serialized;
|
|
249
289
|
}
|
|
250
290
|
if (Array.isArray(value)) {
|
|
251
|
-
return `[${value.map(v => stableStringify(v)).join(",")}]`;
|
|
291
|
+
return `[${value.map((v) => stableStringify(v)).join(",")}]`;
|
|
252
292
|
}
|
|
253
293
|
const obj = value;
|
|
254
294
|
const keys = Object.keys(obj).sort();
|
|
255
|
-
return `{${keys.map(k => `${JSON.stringify(k)}:${stableStringify(obj[k])}`).join(",")}}`;
|
|
295
|
+
return `{${keys.map((k) => `${JSON.stringify(k)}:${stableStringify(obj[k])}`).join(",")}}`;
|
|
256
296
|
}
|