@fitchmultz/pi-mcp-adapter 5.0.0 → 7.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 +50 -0
- package/OAUTH.md +3 -3
- package/README.md +160 -88
- package/dist/app-bridge.bundle.js +75 -33
- package/dist/commands.js +9 -6
- package/dist/config.js +41 -16
- package/dist/direct-tools.d.ts +2 -4
- package/dist/direct-tools.js +21 -319
- package/dist/gateway-arguments.d.ts +33 -0
- package/dist/gateway-arguments.js +63 -0
- package/dist/index.js +181 -276
- package/dist/init.js +6 -20
- package/dist/lifecycle.d.ts +2 -2
- package/dist/lifecycle.js +2 -2
- package/dist/mcp-auth-flow.js +2 -2
- package/dist/mcp-auth.d.ts +1 -1
- package/dist/mcp-auth.js +3 -3
- package/dist/mcp-code.d.ts +7 -6
- package/dist/mcp-code.js +98 -51
- package/dist/mcp-oauth-provider.d.ts +5 -5
- package/dist/mcp-oauth-provider.js +5 -5
- package/dist/mcp-output-guard.d.ts +57 -2
- package/dist/mcp-output-guard.js +129 -24
- package/dist/mcp-panel.d.ts +3 -3
- package/dist/mcp-panel.js +36 -99
- package/dist/mcp-script-worker.mjs +5 -2
- package/dist/mcp-setup-panel.d.ts +5 -4
- package/dist/mcp-setup-panel.js +15 -7
- package/dist/mcp-status.js +2 -1
- package/dist/mcp-trace.d.ts +1 -1
- package/dist/mcp-trace.js +1 -1
- package/dist/metadata-cache.d.ts +1 -1
- package/dist/metadata-cache.js +25 -88
- package/dist/npx-resolver.js +8 -4
- package/dist/prompts.d.ts +1 -1
- package/dist/prompts.js +1 -1
- package/dist/proxy-modes.d.ts +41 -7
- package/dist/proxy-modes.js +275 -490
- package/dist/resource-tools.d.ts +15 -0
- package/dist/resource-tools.js +26 -1
- package/dist/sampling-handler.js +5 -1
- package/dist/search-ranking.d.ts +0 -2
- package/dist/search-ranking.js +70 -137
- package/dist/server-manager.d.ts +2 -2
- package/dist/server-manager.js +44 -27
- package/dist/session-recovery.d.ts +7 -4
- package/dist/session-recovery.js +76 -10
- package/dist/state.d.ts +1 -1
- package/dist/tool-loader.d.ts +32 -0
- package/dist/tool-loader.js +152 -0
- package/dist/tool-metadata.d.ts +67 -0
- package/dist/tool-metadata.js +23 -17
- package/dist/tool-registrar.d.ts +4 -4
- package/dist/tool-registrar.js +18 -13
- package/dist/tool-result-renderer.d.ts +6 -3
- package/dist/tool-result-renderer.js +11 -6
- package/dist/types.d.ts +11 -36
- package/dist/ui-resource-handler.d.ts +2 -2
- package/dist/ui-resource-handler.js +2 -2
- package/dist/ui-server.js +4 -3
- package/dist/ui-stream-types.d.ts +8 -8
- package/dist/unix-socket-transport.d.ts +1 -1
- package/dist/unix-socket-transport.js +1 -1
- package/dist/utils.d.ts +1 -0
- package/dist/utils.js +16 -4
- package/package.json +37 -96
- package/skills/mcp-scripting/SKILL.md +67 -20
- package/abort.ts +0 -34
- package/agent-dir.ts +0 -24
- package/app-bridge.bundle.js +0 -67
- package/banner.png +0 -0
- package/checkpoint.ts +0 -55
- package/commands.ts +0 -619
- package/config.ts +0 -1204
- package/consent-manager.ts +0 -25
- package/direct-tools.ts +0 -408
- package/dist/ts-shape.d.ts +0 -2
- package/dist/ts-shape.js +0 -147
- package/elicitation-handler.ts +0 -349
- package/error-signal.ts +0 -21
- package/glimpse-ui.ts +0 -80
- package/host-html-template.ts +0 -456
- package/index.ts +0 -900
- package/init.ts +0 -627
- package/legacy-migration.ts +0 -47
- package/lifecycle.ts +0 -165
- package/logger.ts +0 -127
- package/mcp-auth-flow.ts +0 -975
- package/mcp-auth.ts +0 -793
- package/mcp-callback-server.ts +0 -450
- package/mcp-code.ts +0 -386
- package/mcp-keyring-helper.cjs +0 -89
- package/mcp-oauth-provider.ts +0 -774
- package/mcp-output-guard.ts +0 -412
- package/mcp-panel.ts +0 -978
- package/mcp-script-worker.mjs +0 -128
- package/mcp-setup-panel.ts +0 -666
- package/mcp-status.ts +0 -109
- package/mcp-trace.ts +0 -295
- package/metadata-cache.ts +0 -333
- package/npx-resolver.ts +0 -488
- package/onboarding-state.ts +0 -58
- package/panel-keys.ts +0 -37
- package/prompts.ts +0 -337
- package/proxy-modes.ts +0 -1361
- package/resource-tools.ts +0 -17
- package/runtime-owner.ts +0 -123
- package/sampling-handler.ts +0 -273
- package/scripts/build.mjs +0 -131
- package/scripts/prepare.mjs +0 -102
- package/search-ranking.ts +0 -196
- package/server-manager.ts +0 -1320
- package/session-recovery.ts +0 -316
- package/state.ts +0 -64
- package/tool-approval.ts +0 -78
- package/tool-metadata.ts +0 -235
- package/tool-registrar.ts +0 -69
- package/tool-result-renderer.ts +0 -286
- package/ts-shape.ts +0 -145
- package/tsconfig.build.json +0 -13
- package/tsconfig.json +0 -16
- package/types.ts +0 -810
- package/ui-resource-handler.ts +0 -249
- package/ui-server.ts +0 -808
- package/ui-session.ts +0 -568
- package/ui-stream-types.ts +0 -89
- package/ui-tool-visibility.ts +0 -25
- package/unix-socket-transport.ts +0 -85
- package/utils.ts +0 -309
package/dist/resource-tools.d.ts
CHANGED
|
@@ -1 +1,16 @@
|
|
|
1
|
+
import type { McpExtensionState } from "./state.ts";
|
|
2
|
+
import { type McpOperationContext, type McpToolCallIdentity } from "./types.ts";
|
|
3
|
+
export declare function executeResourceList(state: McpExtensionState, server: string, limit?: number, offset?: number, signal?: AbortSignal): Promise<import("@earendil-works/pi-agent-core").AgentToolResult<Record<string, unknown>> | {
|
|
4
|
+
content: {
|
|
5
|
+
type: "text";
|
|
6
|
+
text: string;
|
|
7
|
+
}[];
|
|
8
|
+
details: {
|
|
9
|
+
mode: string;
|
|
10
|
+
error: string;
|
|
11
|
+
server: string;
|
|
12
|
+
message: string;
|
|
13
|
+
};
|
|
14
|
+
}>;
|
|
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>>>;
|
|
1
16
|
export declare function resourceNameToToolName(name: string): string;
|
package/dist/resource-tools.js
CHANGED
|
@@ -1,4 +1,29 @@
|
|
|
1
|
-
|
|
1
|
+
import { isServerDisabled, isToolAllowed, resolveToolPrefix } from "./types.js";
|
|
2
|
+
import { executeCall, prepareMcpConnection } from "./proxy-modes.js";
|
|
3
|
+
import { paginate } from "./search-ranking.js";
|
|
4
|
+
import { throwIfAborted } from "./abort.js";
|
|
5
|
+
import { combineAbortSignals } from "./runtime-owner.js";
|
|
6
|
+
export async function executeResourceList(state, server, limit = 12, offset = 0, signal) {
|
|
7
|
+
throwIfAborted(combineAbortSignals(state.owner?.signal, signal));
|
|
8
|
+
const definition = state.config.mcpServers[server];
|
|
9
|
+
if (!definition || isServerDisabled(definition)) {
|
|
10
|
+
return { content: [{ type: "text", text: `Server "${server}" is ${definition ? "disabled" : "not configured"}.` }], details: { mode: "resources", server, error: definition ? "server_disabled" : "server_not_found" } };
|
|
11
|
+
}
|
|
12
|
+
if (!state.toolMetadata.has(server) || state.manager.getConnection(server)?.status === "connected") {
|
|
13
|
+
const prepared = await prepareMcpConnection(state, server, signal);
|
|
14
|
+
if (prepared.error)
|
|
15
|
+
return prepared.error;
|
|
16
|
+
}
|
|
17
|
+
const prefix = resolveToolPrefix(definition, state.config.settings?.toolPrefix);
|
|
18
|
+
const page = paginate((state.toolMetadata.get(server) ?? [])
|
|
19
|
+
.filter(tool => tool.resourceUri !== undefined && definition.exposeResources !== false
|
|
20
|
+
&& isToolAllowed(tool.originalName, server, prefix, definition.includeTools, definition.excludeTools))
|
|
21
|
+
.map(tool => tool.resourceDescriptor ?? { uri: tool.resourceUri, description: tool.description }), offset, limit);
|
|
22
|
+
return { content: [{ type: "text", text: JSON.stringify({ server, ...page }, null, 2) }], details: { mode: "resources", server, ...page } };
|
|
23
|
+
}
|
|
24
|
+
export async function executeResourceRead(state, server, uri, signal, identity = {}, beforeDispatch, raw = false) {
|
|
25
|
+
return executeCall(state, uri, {}, server, undefined, signal, identity, beforeDispatch, { resourceUri: uri, raw });
|
|
26
|
+
}
|
|
2
27
|
export function resourceNameToToolName(name) {
|
|
3
28
|
let result = name
|
|
4
29
|
.replace(/[^a-zA-Z0-9]/g, "_")
|
package/dist/sampling-handler.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { truncateAtWord } from "./utils.js";
|
|
2
2
|
import { throwIfAborted } from "./abort.js";
|
|
3
|
+
import {} from "@modelcontextprotocol/client";
|
|
3
4
|
export function registerSamplingHandler(client, options, own = run => run()) {
|
|
4
5
|
// Own the handler's real promise, not the SDK caller's cancellable response facade.
|
|
5
6
|
client.setRequestHandler("sampling/createMessage", request => own(() => handleSamplingRequest(options, request)));
|
|
@@ -28,12 +29,15 @@ export async function handleSamplingRequest(options, request) {
|
|
|
28
29
|
throwIfAborted(signal);
|
|
29
30
|
await confirmSampling(options, "Approve MCP sampling request", formatRequestApproval(options.serverName, `${model.provider}/${model.id}`, params.systemPrompt, messages));
|
|
30
31
|
throwIfAborted(signal);
|
|
32
|
+
const supportsTemperature = model.compat && "supportsTemperature" in model.compat
|
|
33
|
+
? model.compat.supportsTemperature !== false
|
|
34
|
+
: !(model.id === "gpt-6-astra" && (model.api === "openai-responses" || model.api === "openai-codex-responses"));
|
|
31
35
|
const result = await options.modelRegistry.complete(model, {
|
|
32
36
|
...(params.systemPrompt !== undefined ? { systemPrompt: params.systemPrompt } : {}),
|
|
33
37
|
messages,
|
|
34
38
|
}, {
|
|
35
39
|
maxTokens: params.maxTokens,
|
|
36
|
-
...(params.temperature !== undefined ? { temperature: params.temperature } : {}),
|
|
40
|
+
...(supportsTemperature && params.temperature !== undefined ? { temperature: params.temperature } : {}),
|
|
37
41
|
...(params.metadata !== undefined ? { metadata: params.metadata } : {}),
|
|
38
42
|
...(signal ? { signal } : {}),
|
|
39
43
|
});
|
package/dist/search-ranking.d.ts
CHANGED
|
@@ -8,8 +8,6 @@ export interface RankedToolMatch {
|
|
|
8
8
|
score: number;
|
|
9
9
|
}
|
|
10
10
|
export declare function normalizeSearchText(value: string): string;
|
|
11
|
-
export declare function tokenize(value: string): string[];
|
|
12
|
-
export declare function scoreToolMatch(tool: ToolMetadata, server: string, query: string): number | null;
|
|
13
11
|
export declare function rankToolMatches(state: McpExtensionState, query: string, server?: string): RankedToolMatch[];
|
|
14
12
|
export declare function paginate<T>(items: T[], offset: number, limit: number): {
|
|
15
13
|
items: T[];
|
package/dist/search-ranking.js
CHANGED
|
@@ -1,98 +1,79 @@
|
|
|
1
|
-
import
|
|
2
|
-
|
|
3
|
-
* Shortest field token allowed to stem-match a longer query token.
|
|
4
|
-
* Real descriptions tokenize possessives into single letters ("project's" -> ["project", "s"]),
|
|
5
|
-
* which would otherwise make every query starting with that letter a match.
|
|
6
|
-
*/
|
|
7
|
-
const MIN_STEM_LENGTH = 4;
|
|
1
|
+
import MiniSearch, {} from "minisearch";
|
|
2
|
+
import { getServerPrefix, isServerDisabled, isToolAllowed, resolveToolPrefix } from "./types.js";
|
|
8
3
|
export const MAX_PAGE_SIZE = 100;
|
|
9
4
|
export const MAX_TOOL_NAME_LENGTH = 512;
|
|
10
|
-
const FIELD_WEIGHTS = {
|
|
11
|
-
name: 12,
|
|
12
|
-
originalName: 10,
|
|
13
|
-
server: 8,
|
|
14
|
-
description: 5,
|
|
15
|
-
};
|
|
16
5
|
export function normalizeSearchText(value) {
|
|
17
6
|
return value
|
|
18
7
|
.replace(/([a-z0-9])([A-Z])/g, "$1 $2")
|
|
19
8
|
.replace(/[_./:-]+/g, " ")
|
|
20
9
|
.toLowerCase();
|
|
21
10
|
}
|
|
22
|
-
|
|
23
|
-
|
|
11
|
+
function schemaSearchText(schema) {
|
|
12
|
+
if (!schema || typeof schema !== "object")
|
|
13
|
+
return "";
|
|
14
|
+
if (Array.isArray(schema))
|
|
15
|
+
return schema.map(schemaSearchText).join(" ");
|
|
16
|
+
return Object.entries(schema).flatMap(([key, value]) => {
|
|
17
|
+
if ((key === "description" || key === "title" || key === "const") && typeof value === "string")
|
|
18
|
+
return value;
|
|
19
|
+
if (key === "enum" && Array.isArray(value))
|
|
20
|
+
return value.filter(item => typeof item === "string");
|
|
21
|
+
if (key === "properties" && value && typeof value === "object")
|
|
22
|
+
return [Object.keys(value).join(" "), schemaSearchText(value)];
|
|
23
|
+
return schemaSearchText(value);
|
|
24
|
+
}).join(" ");
|
|
24
25
|
}
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
const
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
let
|
|
39
|
-
const
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
phraseMatched = true;
|
|
51
|
-
}
|
|
52
|
-
else if (value.includes(normalizedQuery)) {
|
|
53
|
-
score += weight * 6;
|
|
54
|
-
phraseMatched = true;
|
|
55
|
-
}
|
|
56
|
-
for (const token of queryTokens) {
|
|
57
|
-
if (fieldTokens.includes(token)) {
|
|
58
|
-
score += weight * 4;
|
|
59
|
-
matchedTokens.add(token);
|
|
60
|
-
}
|
|
61
|
-
else if (fieldTokens.some(fieldToken => fieldToken.startsWith(token) || (fieldToken.length >= MIN_STEM_LENGTH && token.startsWith(fieldToken)))) {
|
|
62
|
-
score += weight * 2;
|
|
63
|
-
matchedTokens.add(token);
|
|
64
|
-
}
|
|
65
|
-
else if (value.includes(token)) {
|
|
66
|
-
score += weight;
|
|
67
|
-
matchedTokens.add(token);
|
|
68
|
-
}
|
|
26
|
+
const indexes = new WeakMap();
|
|
27
|
+
function getIndex(state) {
|
|
28
|
+
const cached = indexes.get(state);
|
|
29
|
+
// Catalog publishers replace each server's metadata array on discovery/reconnect.
|
|
30
|
+
if (cached && cached.metadata.size === state.toolMetadata.size
|
|
31
|
+
&& [...state.toolMetadata].every(([server, metadata]) => cached.metadata.get(server) === metadata)) {
|
|
32
|
+
return cached.index;
|
|
33
|
+
}
|
|
34
|
+
const index = new MiniSearch({
|
|
35
|
+
fields: ["name", "originalName", "description", "schema"],
|
|
36
|
+
storeFields: ["server", "tool"],
|
|
37
|
+
searchOptions: { fields: ["name", "description", "schema"], boost: { name: 2, description: 1, schema: 1 }, prefix: true },
|
|
38
|
+
});
|
|
39
|
+
let id = 0;
|
|
40
|
+
for (const [server, tools] of state.toolMetadata) {
|
|
41
|
+
for (const tool of tools) {
|
|
42
|
+
if (tool.resourceUri !== undefined)
|
|
43
|
+
continue;
|
|
44
|
+
index.add({
|
|
45
|
+
id: id++, server, tool,
|
|
46
|
+
name: normalizeSearchText(tool.name),
|
|
47
|
+
originalName: normalizeSearchText(tool.originalName),
|
|
48
|
+
description: normalizeSearchText([tool.title, tool.description].filter(Boolean).join(" ")),
|
|
49
|
+
schema: normalizeSearchText(schemaSearchText(tool.inputSchema)),
|
|
50
|
+
});
|
|
69
51
|
}
|
|
70
52
|
}
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
53
|
+
indexes.set(state, { metadata: new Map(state.toolMetadata), index });
|
|
54
|
+
return index;
|
|
55
|
+
}
|
|
56
|
+
function searchCatalog(state, query, server, options) {
|
|
57
|
+
return getIndex(state).search(normalizeSearchText(query), {
|
|
58
|
+
...options,
|
|
59
|
+
filter: result => {
|
|
60
|
+
if (server && result.server !== server)
|
|
61
|
+
return false;
|
|
62
|
+
const definition = state.config.mcpServers[result.server];
|
|
63
|
+
if (!definition || isServerDisabled(definition))
|
|
64
|
+
return false;
|
|
65
|
+
const prefix = resolveToolPrefix(definition, state.config.settings?.toolPrefix);
|
|
66
|
+
return isToolAllowed(result.tool.originalName, result.server, prefix, definition.includeTools, definition.excludeTools);
|
|
67
|
+
},
|
|
68
|
+
}).map(({ server, tool, score }) => ({ server, tool, score }))
|
|
69
|
+
.sort((a, b) => b.score - a.score || a.tool.name.localeCompare(b.tool.name));
|
|
81
70
|
}
|
|
82
71
|
export function rankToolMatches(state, query, server) {
|
|
83
|
-
const matches =
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
continue;
|
|
89
|
-
for (const tool of metadata) {
|
|
90
|
-
const score = scoreToolMatch(tool, serverName, query);
|
|
91
|
-
if (score !== null)
|
|
92
|
-
matches.push({ server: serverName, tool, score });
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
return matches.sort((a, b) => b.score - a.score || a.tool.name.localeCompare(b.tool.name));
|
|
72
|
+
const matches = searchCatalog(state, query, server);
|
|
73
|
+
const identity = query.trim();
|
|
74
|
+
const exact = ({ tool }) => tool.name === identity || (Boolean(server) && tool.originalName === identity);
|
|
75
|
+
// Stable partition retains MiniSearch order and scores within both groups.
|
|
76
|
+
return [...matches.filter(exact), ...matches.filter(match => !exact(match))];
|
|
96
77
|
}
|
|
97
78
|
export function paginate(items, offset, limit) {
|
|
98
79
|
const safeOffset = Number.isFinite(offset) ? Math.max(0, Math.trunc(offset)) : 0;
|
|
@@ -107,36 +88,6 @@ export function paginate(items, offset, limit) {
|
|
|
107
88
|
nextOffset: nextOffset < total ? nextOffset : null,
|
|
108
89
|
};
|
|
109
90
|
}
|
|
110
|
-
function editDistance(a, b, maxDistance) {
|
|
111
|
-
if (Math.abs(a.length - b.length) > maxDistance)
|
|
112
|
-
return maxDistance + 1;
|
|
113
|
-
let previous = Array.from({ length: b.length + 1 }, (_, index) => index);
|
|
114
|
-
for (let i = 1; i <= a.length; i++) {
|
|
115
|
-
const current = [i];
|
|
116
|
-
let rowMin = i;
|
|
117
|
-
for (let j = 1; j <= b.length; j++) {
|
|
118
|
-
current[j] = Math.min(current[j - 1] + 1, previous[j] + 1, previous[j - 1] + (a[i - 1] === b[j - 1] ? 0 : 1));
|
|
119
|
-
rowMin = Math.min(rowMin, current[j]);
|
|
120
|
-
}
|
|
121
|
-
if (rowMin > maxDistance)
|
|
122
|
-
return maxDistance + 1;
|
|
123
|
-
previous = current;
|
|
124
|
-
}
|
|
125
|
-
return previous[b.length];
|
|
126
|
-
}
|
|
127
|
-
function rankClosest(tools, query, limit, candidateName) {
|
|
128
|
-
const normalizedQuery = normalizeSearchText(query).replace(/\s+/g, "_");
|
|
129
|
-
const maxDistance = Math.max(2, Math.floor(normalizedQuery.length * 0.2));
|
|
130
|
-
return tools
|
|
131
|
-
.map(tool => ({
|
|
132
|
-
tool,
|
|
133
|
-
distance: editDistance(normalizedQuery, normalizeSearchText(candidateName(tool)).replace(/\s+/g, "_"), maxDistance),
|
|
134
|
-
}))
|
|
135
|
-
.filter(({ distance }) => distance <= maxDistance)
|
|
136
|
-
.sort((a, b) => a.distance - b.distance || a.tool.name.localeCompare(b.tool.name))
|
|
137
|
-
.slice(0, limit)
|
|
138
|
-
.map(({ tool }) => tool.name);
|
|
139
|
-
}
|
|
140
91
|
export function rankSuggestions(state, name, limit, server) {
|
|
141
92
|
if (name.length > MAX_TOOL_NAME_LENGTH)
|
|
142
93
|
return [];
|
|
@@ -150,31 +101,13 @@ export function rankSuggestions(state, name, limit, server) {
|
|
|
150
101
|
.filter((candidate) => Boolean(candidate.prefix) && name.startsWith(`${candidate.prefix}_`))
|
|
151
102
|
.sort((a, b) => b.prefix.length - a.prefix.length)[0];
|
|
152
103
|
const matchedServer = server ?? inferred?.server;
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
.filter(([candidateServer]) => !isServerDisabled(state.config.mcpServers[candidateServer]))
|
|
156
|
-
.flatMap(([, metadata]) => metadata);
|
|
157
|
-
const closest = rankClosest(tools, name, limit, tool => tool.name);
|
|
158
|
-
return closest.length > 0 ? closest : rankToolMatches(state, name).slice(0, limit).map(match => match.tool.name);
|
|
159
|
-
}
|
|
160
|
-
const definition = state.config.mcpServers[matchedServer];
|
|
161
|
-
if (!definition || isServerDisabled(definition))
|
|
104
|
+
const definition = matchedServer ? state.config.mcpServers[matchedServer] : undefined;
|
|
105
|
+
if (matchedServer && (!definition || isServerDisabled(definition)))
|
|
162
106
|
return [];
|
|
163
|
-
const prefix = inferred?.prefix ?? getServerPrefix(matchedServer, resolveToolPrefix(definition, globalPrefix));
|
|
107
|
+
const prefix = inferred?.prefix ?? (matchedServer ? getServerPrefix(matchedServer, resolveToolPrefix(definition, globalPrefix)) : "");
|
|
164
108
|
const query = prefix && name.startsWith(`${prefix}_`) ? name.slice(prefix.length + 1) : name;
|
|
165
|
-
const
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
const queryTokens = tokenize(query);
|
|
170
|
-
const tokenMatches = rankToolMatches(state, query, matchedServer)
|
|
171
|
-
.filter(({ tool }) => {
|
|
172
|
-
const candidateTokens = tokenize(tool.originalName);
|
|
173
|
-
return queryTokens.every(token => candidateTokens.some(candidate => candidate === token
|
|
174
|
-
|| candidate.startsWith(token)
|
|
175
|
-
|| (candidate.length >= MIN_STEM_LENGTH && token.startsWith(candidate))));
|
|
176
|
-
})
|
|
177
|
-
.slice(0, limit)
|
|
178
|
-
.map(match => match.tool.name);
|
|
179
|
-
return tokenMatches.length > 0 || !server ? tokenMatches : rankSuggestions(state, name, limit);
|
|
109
|
+
const suggestions = searchCatalog(state, query, matchedServer, {
|
|
110
|
+
fields: [matchedServer ? "originalName" : "name"], combineWith: "AND", fuzzy: 2,
|
|
111
|
+
}).slice(0, limit).map(match => match.tool.name);
|
|
112
|
+
return suggestions.length > 0 || !server ? suggestions : rankSuggestions(state, name, limit);
|
|
180
113
|
}
|
package/dist/server-manager.d.ts
CHANGED
|
@@ -24,7 +24,6 @@ export interface ServerConnection {
|
|
|
24
24
|
type UiStreamListener = (serverName: string, notification: ServerStreamResultPatchNotification["params"]) => void;
|
|
25
25
|
type MetadataListChangedListener = (serverName: string, reason: string) => void;
|
|
26
26
|
export declare class McpServerManager {
|
|
27
|
-
private readonly defaultCwd?;
|
|
28
27
|
private ownedClients;
|
|
29
28
|
private connections;
|
|
30
29
|
private connectPromises;
|
|
@@ -56,7 +55,8 @@ export declare class McpServerManager {
|
|
|
56
55
|
* Preserve existing property setters (including tracing) rather than replacing their behavior. */
|
|
57
56
|
private fenceTransport;
|
|
58
57
|
/** Default cwd for stdio servers without an explicit config `cwd`. */
|
|
59
|
-
|
|
58
|
+
private readonly defaultCwd;
|
|
59
|
+
constructor(defaultCwd?: string);
|
|
60
60
|
setSamplingConfig(config: ServerSamplingConfig | undefined): void;
|
|
61
61
|
setMetadataListChangedListener(listener: MetadataListChangedListener | undefined): void;
|
|
62
62
|
setElicitationConfig(config: ServerElicitationConfig | undefined): void;
|
package/dist/server-manager.js
CHANGED
|
@@ -2,7 +2,7 @@ import { AsyncLocalStorage } from "node:async_hooks";
|
|
|
2
2
|
import { Client, StreamableHTTPClientTransport, SSEClientTransport, SdkHttpError, auth, extractWWWAuthenticateParams, UnauthorizedError, } from "@modelcontextprotocol/client";
|
|
3
3
|
import { StdioClientTransport } from "@modelcontextprotocol/client/stdio";
|
|
4
4
|
import { UnixSocketClientTransport } from "./unix-socket-transport.js";
|
|
5
|
-
import {
|
|
5
|
+
import { trackToolTransport, trackToolTransportFailure } from "./session-recovery.js";
|
|
6
6
|
import { isServerDisabled, isNonInteractiveOAuth, validateServerProtocolConfig, serverStreamResultPatchNotificationSchema, } from "./types.js";
|
|
7
7
|
import { resolveNpxBinary } from "./npx-resolver.js";
|
|
8
8
|
import { logger } from "./logger.js";
|
|
@@ -11,10 +11,10 @@ import { extractOAuthConfig, supportsOAuth, isOAuthChallenge, recordOAuthChallen
|
|
|
11
11
|
import { getAuthBaseDir } from "./mcp-auth.js";
|
|
12
12
|
import { registerSamplingHandler } from "./sampling-handler.js";
|
|
13
13
|
import { handleUrlElicitation, registerElicitationHandler, } from "./elicitation-handler.js";
|
|
14
|
-
import { normalizeRequestTimeoutMs, resolveBearerToken, resolveCommandSecret, resolveCommandSecretsRecord, resolveConfigPath, resolveServerUrl, } from "./utils.js";
|
|
14
|
+
import { ADAPTER_VERSION, normalizeRequestTimeoutMs, resolveBearerToken, resolveCommandSecret, resolveCommandSecretsRecord, resolveConfigPath, resolveServerUrl, } from "./utils.js";
|
|
15
15
|
import { abortable, throwIfAborted } from "./abort.js";
|
|
16
16
|
import { combineAbortSignals } from "./runtime-owner.js";
|
|
17
|
-
import { createMcpTraceWriter, isMcpTraceEnabled, traceTransportKind, wrapTransportWithMcpTrace, } from "./mcp-trace.js";
|
|
17
|
+
import { createMcpTraceWriter, isMcpTraceEnabled, McpTraceWriter, traceTransportKind, wrapTransportWithMcpTrace, } from "./mcp-trace.js";
|
|
18
18
|
const MAX_CAPTURED_STDERR_BYTES = 8 * 1024;
|
|
19
19
|
const MAX_CAPTURED_STDERR_LINES = 3;
|
|
20
20
|
const abortCleanupPromises = new WeakMap();
|
|
@@ -161,7 +161,6 @@ function appendStderrTail(tail, chunk) {
|
|
|
161
161
|
: combined;
|
|
162
162
|
}
|
|
163
163
|
export class McpServerManager {
|
|
164
|
-
defaultCwd;
|
|
165
164
|
// Includes native clients whose public connect/close facade ended before their send/auth tail.
|
|
166
165
|
// Their operation promises remain in the same clientOperations authority until settlement.
|
|
167
166
|
ownedClients = new Set();
|
|
@@ -267,6 +266,7 @@ export class McpServerManager {
|
|
|
267
266
|
}
|
|
268
267
|
}
|
|
269
268
|
/** Default cwd for stdio servers without an explicit config `cwd`. */
|
|
269
|
+
defaultCwd;
|
|
270
270
|
constructor(defaultCwd) {
|
|
271
271
|
this.defaultCwd = defaultCwd;
|
|
272
272
|
}
|
|
@@ -329,9 +329,30 @@ export class McpServerManager {
|
|
|
329
329
|
throwIfAborted(ownedSignal);
|
|
330
330
|
if (this.stopped)
|
|
331
331
|
throw new Error("MCP server manager is closed");
|
|
332
|
+
const wait = (promise, attempt) => {
|
|
333
|
+
attempt.waiters++;
|
|
334
|
+
return abortable(promise, ownedSignal).finally(() => {
|
|
335
|
+
if (--attempt.waiters === 0 && ownedSignal?.aborted)
|
|
336
|
+
attempt.controller.abort(ownedSignal.reason);
|
|
337
|
+
});
|
|
338
|
+
};
|
|
332
339
|
// Dedupe concurrent connection attempts.
|
|
333
|
-
|
|
334
|
-
|
|
340
|
+
const pending = this.connectPromises.get(name);
|
|
341
|
+
if (pending) {
|
|
342
|
+
const attempt = this.connectAttempts.get(name);
|
|
343
|
+
if (attempt.controller.signal.aborted) {
|
|
344
|
+
// Finish the canceled attempt's cleanup before starting a fresh one.
|
|
345
|
+
try {
|
|
346
|
+
await abortable(pending, ownedSignal);
|
|
347
|
+
}
|
|
348
|
+
catch (error) {
|
|
349
|
+
throwIfAborted(ownedSignal);
|
|
350
|
+
if (this.containsCleanupFailure(error))
|
|
351
|
+
throw error;
|
|
352
|
+
}
|
|
353
|
+
return this.connect(name, definition, signal);
|
|
354
|
+
}
|
|
355
|
+
return wait(pending, attempt);
|
|
335
356
|
}
|
|
336
357
|
const existing = this.connections.get(name);
|
|
337
358
|
if (existing?.status === "connected") {
|
|
@@ -339,10 +360,10 @@ export class McpServerManager {
|
|
|
339
360
|
return existing;
|
|
340
361
|
}
|
|
341
362
|
const generation = this.closeGenerations.get(name) ?? 0;
|
|
342
|
-
const
|
|
343
|
-
const attemptSignal = combineAbortSignals(
|
|
344
|
-
const promise = this.createConnection(name, definition, attemptSignal, ownedSignal).then(async (connection) => {
|
|
345
|
-
if (
|
|
363
|
+
const attempt = { controller: new AbortController(), waiters: 0 };
|
|
364
|
+
const attemptSignal = combineAbortSignals(this.runtimeSignal, attempt.controller.signal);
|
|
365
|
+
const promise = this.createConnection(name, definition, attemptSignal, ownedSignal ? attemptSignal : undefined).then(async (connection) => {
|
|
366
|
+
if (connection.status === "closed" || attempt.controller.signal.aborted || (this.closeGenerations.get(name) ?? 0) !== generation) {
|
|
346
367
|
await this.disposeConnection(connection);
|
|
347
368
|
throwIfAborted(attemptSignal);
|
|
348
369
|
throw new Error(`MCP connection for ${name} was closed while connecting`);
|
|
@@ -350,18 +371,15 @@ export class McpServerManager {
|
|
|
350
371
|
connection.inFlight = this.connections.get(name)?.inFlight ?? 0;
|
|
351
372
|
this.connections.set(name, connection);
|
|
352
373
|
return connection;
|
|
353
|
-
})
|
|
354
|
-
this.connectPromises.set(name, promise);
|
|
355
|
-
this.connectAttempts.set(name, attemptController);
|
|
356
|
-
try {
|
|
357
|
-
return await promise;
|
|
358
|
-
}
|
|
359
|
-
finally {
|
|
374
|
+
}).finally(() => {
|
|
360
375
|
if (this.connectPromises.get(name) === promise)
|
|
361
376
|
this.connectPromises.delete(name);
|
|
362
|
-
if (this.connectAttempts.get(name) ===
|
|
377
|
+
if (this.connectAttempts.get(name) === attempt)
|
|
363
378
|
this.connectAttempts.delete(name);
|
|
364
|
-
}
|
|
379
|
+
});
|
|
380
|
+
this.connectPromises.set(name, promise);
|
|
381
|
+
this.connectAttempts.set(name, attempt);
|
|
382
|
+
return wait(promise, attempt);
|
|
365
383
|
}
|
|
366
384
|
/**
|
|
367
385
|
* Reconnect a server whose connection was proven stale (e.g. by a 404
|
|
@@ -493,6 +511,8 @@ export class McpServerManager {
|
|
|
493
511
|
else {
|
|
494
512
|
transport = new UnixSocketClientTransport(resolveConfigPath(definition.socket));
|
|
495
513
|
}
|
|
514
|
+
if (!definition.url)
|
|
515
|
+
trackToolTransport(transport);
|
|
496
516
|
if (traceObserver) {
|
|
497
517
|
const traceTransportKindValue = traceTransportKind(definition, transport);
|
|
498
518
|
transport = wrapTransportWithMcpTrace(transport, name, traceTransportKindValue, traceObserver);
|
|
@@ -536,11 +556,9 @@ export class McpServerManager {
|
|
|
536
556
|
status: "connected",
|
|
537
557
|
oauthProvider,
|
|
538
558
|
};
|
|
539
|
-
//
|
|
540
|
-
// client's late close must never change its replacement's status.
|
|
559
|
+
// Track closure during discovery too; this only changes this client's connection.
|
|
541
560
|
client.onclose = () => {
|
|
542
|
-
|
|
543
|
-
connection.status = "closed";
|
|
561
|
+
connection.status = "closed";
|
|
544
562
|
};
|
|
545
563
|
const discoveryOptions = this.buildRequestOptions(definition, signal);
|
|
546
564
|
const catalogs = [
|
|
@@ -671,7 +689,7 @@ export class McpServerManager {
|
|
|
671
689
|
createClient(serverName, definition) {
|
|
672
690
|
const capabilities = this.buildClientCapabilities(definition);
|
|
673
691
|
let client;
|
|
674
|
-
client = new ManagedClient({ name: `pi-mcp-${serverName}`, version:
|
|
692
|
+
client = new ManagedClient({ name: `pi-mcp-${serverName}`, version: ADAPTER_VERSION }, {
|
|
675
693
|
versionNegotiation: { mode: definition.protocolVersion ?? (definition.url ? "auto" : "legacy") },
|
|
676
694
|
listMaxPages: 0,
|
|
677
695
|
...(Object.keys(capabilities).length > 0 ? { capabilities } : {}),
|
|
@@ -936,8 +954,7 @@ export class McpServerManager {
|
|
|
936
954
|
onAuthChallenge?.(error);
|
|
937
955
|
};
|
|
938
956
|
}
|
|
939
|
-
|
|
940
|
-
trackToolHttpFailures(transport);
|
|
957
|
+
trackToolTransport(transport, true);
|
|
941
958
|
return { transport, oauthProvider: authProvider !== undefined };
|
|
942
959
|
}
|
|
943
960
|
async fetchAllTools(client, requestOptions) {
|
|
@@ -1021,7 +1038,7 @@ export class McpServerManager {
|
|
|
1021
1038
|
async close(name) {
|
|
1022
1039
|
this.beforeActivity();
|
|
1023
1040
|
this.closeGenerations.set(name, (this.closeGenerations.get(name) ?? 0) + 1);
|
|
1024
|
-
this.connectAttempts.get(name)?.abort(new Error(`MCP connection ${name} was closed`));
|
|
1041
|
+
this.connectAttempts.get(name)?.controller.abort(new Error(`MCP connection ${name} was closed`));
|
|
1025
1042
|
const pendingClose = this.closePromises.get(name);
|
|
1026
1043
|
if (pendingClose)
|
|
1027
1044
|
return pendingClose;
|
|
@@ -2,15 +2,18 @@ import { type FetchLike, type Transport } from "@modelcontextprotocol/client";
|
|
|
2
2
|
import { type McpConfig } from "./types.ts";
|
|
3
3
|
import type { McpServerManager, ServerConnection } from "./server-manager.ts";
|
|
4
4
|
export declare function isToolTransportFailure(error: unknown): boolean;
|
|
5
|
-
|
|
5
|
+
export declare function isInterruptedToolCall(error: unknown): boolean;
|
|
6
|
+
/** Interrupted outcome reporting is separate from the transport retry policy. */
|
|
7
|
+
export declare function trackToolCallOutcome<T>(fn: () => Promise<T>, signal?: AbortSignal): Promise<T>;
|
|
8
|
+
/** Retain fetch/body-error origin without parsing or buffering the response. */
|
|
6
9
|
export declare const trackToolTransportFailure: FetchLike;
|
|
7
10
|
/** Observe only the originating tool send, not OAuth or catalog requests. */
|
|
8
|
-
export declare function
|
|
11
|
+
export declare function trackToolTransport(transport: Transport, http?: boolean): void;
|
|
9
12
|
export declare function isTerminatedSession(err: unknown, hadSessionId: boolean): boolean;
|
|
10
13
|
export declare class SessionRecoveryAuthRequiredError extends Error {
|
|
11
14
|
readonly serverName: string;
|
|
12
|
-
readonly authMessage
|
|
13
|
-
constructor(serverName: string, authMessage?: string
|
|
15
|
+
readonly authMessage: string | undefined;
|
|
16
|
+
constructor(serverName: string, authMessage?: string);
|
|
14
17
|
}
|
|
15
18
|
export interface AuthChallengeContext {
|
|
16
19
|
connection: ServerConnection;
|