@code-yeongyu/senpi 2026.10.10-5 → 2026.10.10-6
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 +22 -0
- package/dist/bundle/chunks/{anthropic-messages-OE3QBX4P.js → anthropic-messages-DUULMQBF.js} +1 -1
- package/dist/bundle/chunks/{app-server-command-6FTH3TK6.js → app-server-command-GJ5NXQSY.js} +1 -1
- package/dist/bundle/chunks/{chunk-2UUTN6QT.js → chunk-C7LEZMWS.js} +97 -97
- package/dist/bundle/chunks/{chunk-ZSYRWQWG.js → chunk-JMI5OCIJ.js} +1 -1
- package/dist/bundle/chunks/{chunk-ODYQK5XJ.js → chunk-TALS4X7S.js} +1 -1
- package/dist/bundle/chunks/{chunk-2PUAD5TD.js → chunk-VGYIPYOK.js} +1 -1
- package/dist/bundle/chunks/{chunk-4TK7VCZZ.js → chunk-WKMUAG23.js} +1 -1
- package/dist/bundle/chunks/{chunk-TIMLYJMK.js → chunk-YV2XQFXG.js} +1 -1
- package/dist/bundle/chunks/{chunk-TZVDXVHB.js → chunk-ZYHIRFGT.js} +1 -1
- package/dist/bundle/chunks/{cli-main-UYTUBG64.js → cli-main-SZ54SQLA.js} +1 -1
- package/dist/bundle/chunks/{help-fast-path-EHRZEBRO.js → help-fast-path-W5W3FYYM.js} +1 -1
- package/dist/bundle/chunks/{host-command-TOVYSHYH.js → host-command-YG2FIDPN.js} +1 -1
- package/dist/bundle/chunks/{interactive-mode-KJIOCRS7.js → interactive-mode-SCNDQ4R6.js} +1 -1
- package/dist/bundle/chunks/{multi-session-host-TEFLJTTA.js → multi-session-host-ZDZMPNP5.js} +4 -3
- package/dist/bundle/chunks/{package-manager-cli-QSJSIHUR.js → package-manager-cli-4LMYHHKH.js} +1 -1
- package/dist/bundle/chunks/{rotation-stream-C5BM7TMM.js → rotation-stream-E3XKSST7.js} +1 -1
- package/dist/bundle/chunks/{rpc-mode-RUKK7VBF.js → rpc-mode-ZFNFREN5.js} +1 -1
- package/dist/bundle/chunks/{session-control-endpoint-GZDV2FOZ.js → session-control-endpoint-MVSYBGM3.js} +1 -1
- package/dist/bundle/chunks/{session-picker-6D4LC3KR.js → session-picker-TNXWY5QY.js} +1 -1
- package/dist/bundle/chunks/session-worker.js +158 -157
- package/dist/bundle/cli.js +1 -1
- package/dist/bundle/index.js +1 -1
- package/dist/bundle/rpc-entry.js +1 -1
- package/dist/bundle/runtime-manifest.json +1 -1
- package/dist/core/extensions/builtin/anthropic-subscription/auth-lane.js +3 -2
- package/dist/core/extensions/builtin/anthropic-subscription/options.js +11 -2
- package/dist/core/extensions/builtin/anthropic-subscription/session-reattach.js +4 -1
- package/dist/core/extensions/builtin/anthropic-subscription/session-sync.js +3 -0
- package/dist/core/extensions/builtin/anthropic-subscription/settings.d.ts +5 -0
- package/dist/core/extensions/builtin/anthropic-subscription/settings.js +11 -0
- package/dist/core/extensions/builtin/anthropic-subscription/stream.d.ts +1 -0
- package/dist/core/extensions/builtin/anthropic-subscription/stream.js +17 -1
- package/dist/core/extensions/builtin/compaction/lane-policy.d.ts +20 -12
- package/dist/core/extensions/builtin/compaction/lane-policy.js +39 -34
- package/dist/core/extensions/builtin/mcp/auth/catalog-identity.d.ts +4 -0
- package/dist/core/extensions/builtin/mcp/auth/catalog-identity.js +41 -0
- package/dist/core/extensions/builtin/mcp/auth/legacy-lock.d.ts +3 -0
- package/dist/core/extensions/builtin/mcp/auth/legacy-lock.js +39 -0
- package/dist/core/extensions/builtin/mcp/auth/legacy-migration.d.ts +14 -0
- package/dist/core/extensions/builtin/mcp/auth/legacy-migration.js +91 -0
- package/dist/core/extensions/builtin/mcp/auth/oauth-refresh.js +1 -1
- package/dist/core/extensions/builtin/mcp/auth/token-store.d.ts +1 -0
- package/dist/core/extensions/builtin/mcp/auth/token-store.js +19 -90
- package/dist/core/extensions/builtin/mcp/catalog-cache.d.ts +7 -3
- package/dist/core/extensions/builtin/mcp/catalog-cache.js +61 -12
- package/dist/core/extensions/builtin/mcp/catalog.d.ts +3 -1
- package/dist/core/extensions/builtin/mcp/catalog.js +2 -0
- package/dist/core/extensions/builtin/mcp/connection-types.d.ts +2 -0
- package/dist/core/extensions/builtin/mcp/expose/call.d.ts +12 -0
- package/dist/core/extensions/builtin/mcp/expose/call.js +60 -0
- package/dist/core/extensions/builtin/mcp/expose/proxy.js +16 -3
- package/dist/core/extensions/builtin/mcp/expose/register.d.ts +3 -1
- package/dist/core/extensions/builtin/mcp/expose/register.js +41 -50
- package/dist/core/extensions/builtin/mcp/expose/session.d.ts +4 -0
- package/dist/core/extensions/builtin/mcp/expose/session.js +5 -1
- package/dist/core/extensions/builtin/mcp/expose/tier-b.js +15 -4
- package/dist/core/extensions/builtin/mcp/index.js +5 -26
- package/dist/core/extensions/builtin/mcp/invocation.d.ts +42 -0
- package/dist/core/extensions/builtin/mcp/invocation.js +118 -0
- package/dist/core/extensions/builtin/mcp/service-connection.d.ts +3 -0
- package/dist/core/extensions/builtin/mcp/service-connection.js +6 -0
- package/dist/core/extensions/builtin/mcp/service-register.d.ts +4 -0
- package/dist/core/extensions/builtin/mcp/service-register.js +45 -1
- package/dist/core/extensions/builtin/mcp/service-tools-changed.js +26 -3
- package/dist/core/extensions/builtin/mcp/service-types.d.ts +6 -0
- package/dist/core/extensions/builtin/mcp/service.js +83 -49
- package/dist/core/extensions/builtin/mcp/shared-connection.js +17 -4
- package/dist/core/extensions/builtin/mcp/sharing-policy.js +1 -0
- package/dist/core/extensions/builtin/mcp/startup-race.js +47 -9
- package/dist/core/extensions/builtin/permission-system/dispatch-metadata.d.ts +8 -0
- package/dist/core/extensions/builtin/permission-system/dispatch-metadata.js +23 -0
- package/dist/core/extensions/builtin/permission-system/dispatch-policy.d.ts +14 -0
- package/dist/core/extensions/builtin/permission-system/dispatch-policy.js +53 -0
- package/dist/core/extensions/builtin/permission-system/dispatch.d.ts +33 -0
- package/dist/core/extensions/builtin/permission-system/dispatch.js +106 -0
- package/dist/core/extensions/builtin/permission-system/index.js +25 -4
- package/dist/core/extensions/builtin/permission-system/prompt.d.ts +1 -1
- package/dist/core/extensions/builtin/permission-system/prompt.js +3 -1
- package/dist/core/extensions/builtin/permission-system/service.d.ts +11 -1
- package/dist/core/extensions/builtin/permission-system/service.js +14 -4
- package/dist/core/extensions/builtin/ttsr/detectors/collapse-near-duplicates.d.ts +1 -0
- package/dist/core/extensions/builtin/ttsr/detectors/collapse-near-duplicates.js +24 -5
- package/dist/modes/rpc/rpc-client.js +14 -2
- package/dist/modes/rpc/rpc-request-deadline.d.ts +8 -0
- package/dist/modes/rpc/rpc-request-deadline.js +10 -0
- package/dist/modes/rpc/rpc-types.d.ts +7 -4
- package/dist/modes/rpc/session-binding.js +15 -1
- package/dist/modes/rpc/session-command-router.d.ts +1 -0
- package/dist/modes/rpc/session-command-router.js +16 -0
- package/docs/providers.md +3 -2
- package/docs/rpc.md +12 -6
- package/package.json +6 -6
|
@@ -3,11 +3,13 @@ import type { Progress } from "@modelcontextprotocol/sdk/types.js";
|
|
|
3
3
|
import type { TSchema } from "typebox";
|
|
4
4
|
import type { ToolDefinition } from "../../../types.ts";
|
|
5
5
|
import type { McpToolCatalogEntry } from "../catalog.ts";
|
|
6
|
+
import type { McpInvocation, McpInvocationError } from "../invocation.ts";
|
|
6
7
|
export interface McpToolDetails {
|
|
7
8
|
server: string;
|
|
8
9
|
tool: string;
|
|
9
10
|
preview?: string;
|
|
10
11
|
progress?: Progress;
|
|
12
|
+
error?: McpInvocationError;
|
|
11
13
|
}
|
|
12
14
|
export type McpToolDefinition = ToolDefinition<TSchema, McpToolDetails | undefined, unknown>;
|
|
13
15
|
type WarnFn = (message: string) => void;
|
|
@@ -25,6 +27,6 @@ export declare function mapMcpCatalogNames(entries: readonly McpToolCatalogEntry
|
|
|
25
27
|
export declare function buildMcpToolDefinitions(entries: readonly McpToolCatalogEntry[], warn?: WarnFn): McpToolDefinition[];
|
|
26
28
|
/** Full guarded call path for one catalog entry (lifecycle + retry + output
|
|
27
29
|
* guard). Shared by direct/search tool definitions and the Tier-C proxy. */
|
|
28
|
-
export declare function executeMcpCatalogEntry(entry: McpToolCatalogEntry, args: Record<string, unknown>, signal: AbortSignal | undefined, onUpdate: Parameters<McpToolDefinition["execute"]>[3]): Promise<AgentToolResult<McpToolDetails | undefined>>;
|
|
30
|
+
export declare function executeMcpCatalogEntry(entry: McpToolCatalogEntry, args: Record<string, unknown>, signal: AbortSignal | undefined, onUpdate: Parameters<McpToolDefinition["execute"]>[3], invocation?: McpInvocation): Promise<AgentToolResult<McpToolDetails | undefined>>;
|
|
29
31
|
export {};
|
|
30
32
|
//# sourceMappingURL=register.d.ts.map
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { Text } from "@earendil-works/pi-tui";
|
|
2
|
+
import { forgetDispatchApproval } from "../../permission-system/dispatch.js";
|
|
3
|
+
import { registerDispatchIdentity } from "../../permission-system/dispatch-metadata.js";
|
|
2
4
|
import { ToolExecError } from "../errors.js";
|
|
3
5
|
import { applyMcpOutputGuard } from "../guard/output-guard.js";
|
|
4
|
-
import {
|
|
5
|
-
import { runMcpConnectionLifecycleCall } from "../idle.js";
|
|
6
|
+
import { callMcpTool } from "./call.js";
|
|
6
7
|
import { buildMcpToolNames, convertJsonSchemaToTypeBox, mapMcpToolResult, } from "./schema-compat.js";
|
|
7
8
|
/** Pair each catalog entry with its stable, collision-resolved mcp tool name.
|
|
8
9
|
* Shared by the full-tool builder and the Tier-B search catalog so names never
|
|
@@ -18,15 +19,21 @@ export function buildMcpToolDefinitions(entries, warn) {
|
|
|
18
19
|
function createMcpToolDefinition(entry, name) {
|
|
19
20
|
const converted = convertJsonSchemaToTypeBox(entry.schema);
|
|
20
21
|
const label = `${entry.server}/${entry.tool}`;
|
|
22
|
+
registerDispatchIdentity(converted.schema, () => entry.invocation?.identity(entry));
|
|
21
23
|
return {
|
|
22
24
|
name,
|
|
23
25
|
label,
|
|
24
26
|
description: entry.description ?? `MCP tool ${label}`,
|
|
25
27
|
parameters: converted.schema,
|
|
26
28
|
executionMode: "parallel",
|
|
27
|
-
async execute(
|
|
29
|
+
async execute(toolCallId, params, signal, onUpdate, context) {
|
|
28
30
|
const args = isRecord(params) ? params : {};
|
|
29
|
-
return await executeMcpCatalogEntry(entry, args, signal, onUpdate
|
|
31
|
+
return await executeMcpCatalogEntry(entry, args, signal, onUpdate, {
|
|
32
|
+
toolCallId,
|
|
33
|
+
toolName: name,
|
|
34
|
+
input: args,
|
|
35
|
+
context,
|
|
36
|
+
});
|
|
30
37
|
},
|
|
31
38
|
renderCall(args, theme) {
|
|
32
39
|
return new Text(theme.fg("toolTitle", theme.bold(`${name} ${previewArgs(args)}`.trim())), 0, 0);
|
|
@@ -41,47 +48,39 @@ function createMcpToolDefinition(entry, name) {
|
|
|
41
48
|
}
|
|
42
49
|
/** Full guarded call path for one catalog entry (lifecycle + retry + output
|
|
43
50
|
* guard). Shared by direct/search tool definitions and the Tier-C proxy. */
|
|
44
|
-
export async function executeMcpCatalogEntry(entry, args, signal, onUpdate) {
|
|
45
|
-
const label = `${entry.server}/${entry.tool}`;
|
|
46
|
-
const result = await callMcpTool(entry, args, signal, onUpdate, label);
|
|
47
|
-
const mapped = mapMcpToolResult(normalizeCallToolResult(result));
|
|
48
|
-
if (!mapped.ok) {
|
|
49
|
-
throw new ToolExecError(mapped.error.message, { phase: "call", serverName: entry.server });
|
|
50
|
-
}
|
|
51
|
-
const guarded = await applyMcpOutputGuard(mapped.content, {
|
|
52
|
-
agentDir: entry.agentDir,
|
|
53
|
-
artifacts: entry.artifacts,
|
|
54
|
-
outputGuard: entry.outputGuard,
|
|
55
|
-
server: entry.server,
|
|
56
|
-
});
|
|
57
|
-
const content = toAgentContent(guarded);
|
|
58
|
-
return { content, details: { preview: previewContent(content), server: entry.server, tool: entry.tool } };
|
|
59
|
-
}
|
|
60
|
-
async function callMcpTool(entry, args, signal, onUpdate, label) {
|
|
51
|
+
export async function executeMcpCatalogEntry(entry, args, signal, onUpdate, invocation) {
|
|
61
52
|
try {
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
details: { progress, server: entry.server, tool: entry.tool },
|
|
71
|
-
});
|
|
72
|
-
},
|
|
73
|
-
signal,
|
|
74
|
-
timeout: entry.requestTimeoutMs,
|
|
75
|
-
});
|
|
53
|
+
const label = `${entry.server}/${entry.tool}`;
|
|
54
|
+
const outcome = await callMcpTool(entry, args, signal, onUpdate, label, invocation);
|
|
55
|
+
if (outcome.kind === "refused") {
|
|
56
|
+
const guarded = await applyMcpOutputGuard([{ type: "text", text: JSON.stringify({ error: outcome.error }) }], {
|
|
57
|
+
agentDir: entry.agentDir,
|
|
58
|
+
artifacts: entry.artifacts,
|
|
59
|
+
outputGuard: entry.outputGuard,
|
|
60
|
+
server: entry.server,
|
|
76
61
|
});
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
62
|
+
return {
|
|
63
|
+
content: toAgentContent(guarded),
|
|
64
|
+
details: { error: outcome.error, preview: outcome.error.kind, server: entry.server, tool: entry.tool },
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
const current = outcome.entry;
|
|
68
|
+
const mapped = mapMcpToolResult(normalizeCallToolResult(outcome.result));
|
|
69
|
+
if (!mapped.ok) {
|
|
70
|
+
throw new ToolExecError(mapped.error.message, { phase: "call", serverName: entry.server });
|
|
71
|
+
}
|
|
72
|
+
const guarded = await applyMcpOutputGuard(mapped.content, {
|
|
73
|
+
agentDir: current.agentDir,
|
|
74
|
+
artifacts: current.artifacts,
|
|
75
|
+
outputGuard: current.outputGuard,
|
|
76
|
+
server: current.server,
|
|
84
77
|
});
|
|
78
|
+
const content = toAgentContent(guarded);
|
|
79
|
+
return { content, details: { preview: previewContent(content), server: entry.server, tool: entry.tool } };
|
|
80
|
+
}
|
|
81
|
+
finally {
|
|
82
|
+
if (invocation !== undefined)
|
|
83
|
+
forgetDispatchApproval(invocation.input);
|
|
85
84
|
}
|
|
86
85
|
}
|
|
87
86
|
function normalizeCallToolResult(result) {
|
|
@@ -121,14 +120,6 @@ function previewArgs(args) {
|
|
|
121
120
|
function isRecord(value) {
|
|
122
121
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
123
122
|
}
|
|
124
|
-
function errorLabel(error) {
|
|
125
|
-
return error instanceof Error ? `${error.name}: ${error.message}` : String(error);
|
|
126
|
-
}
|
|
127
|
-
function formatProgress(label, progress) {
|
|
128
|
-
const total = progress.total === undefined ? "" : `/${progress.total}`;
|
|
129
|
-
const message = progress.message === undefined ? "" : ` ${progress.message}`;
|
|
130
|
-
return `${label} progress ${progress.progress}${total}${message}`.trim();
|
|
131
|
-
}
|
|
132
123
|
function truncatePreview(value) {
|
|
133
124
|
return value.length <= 120 ? value : `${value.slice(0, 117)}...`;
|
|
134
125
|
}
|
|
@@ -4,6 +4,7 @@ import type { McpCachedServerCatalog } from "../catalog-cache.ts";
|
|
|
4
4
|
import type { ResolvedMcpConfig } from "../config-schema.ts";
|
|
5
5
|
import type { ServerConnection } from "../connection.ts";
|
|
6
6
|
import type { McpOutputArtifacts } from "../guard/output-guard.ts";
|
|
7
|
+
import type { McpInvocationResolver } from "../invocation.ts";
|
|
7
8
|
import { type McpLogger } from "../log.ts";
|
|
8
9
|
import type { McpPromptServer } from "../prompts.ts";
|
|
9
10
|
import { type McpResourceServer } from "../resources.ts";
|
|
@@ -22,8 +23,11 @@ export interface McpDirectRegistrationEntry {
|
|
|
22
23
|
readonly cachedCatalog?: McpCachedServerCatalog;
|
|
23
24
|
readonly ensureFresh?: () => Promise<void>;
|
|
24
25
|
readonly ensureCachedToolConnected?: () => Promise<void>;
|
|
26
|
+
readonly invocation?: McpInvocationResolver;
|
|
25
27
|
/** A startup connect still owns this server's catalog; its refresh registers it. */
|
|
26
28
|
readonly startupCatalogPending?: boolean;
|
|
29
|
+
/** Service admission and registry updates never enumerate a remote server. */
|
|
30
|
+
readonly localCatalogOnly?: boolean;
|
|
27
31
|
readonly onRegistered?: (identity: string) => void;
|
|
28
32
|
}
|
|
29
33
|
export declare function registerDirectMcpTools(pi: Pick<ExtensionAPI, "getActiveTools" | "setActiveTools" | "registerTool">, config: ResolvedMcpConfig, entries: Iterable<McpDirectRegistrationEntry>, toolSearchService: ToolSearchService, options?: McpCatalogRegistrationOptions): Promise<McpSessionRegistration | undefined>;
|
|
@@ -16,11 +16,14 @@ export async function registerDirectMcpTools(pi, config, entries, toolSearchServ
|
|
|
16
16
|
if (server?.config === undefined)
|
|
17
17
|
continue;
|
|
18
18
|
const catalog = entry.cachedCatalog === undefined
|
|
19
|
-
? entry.connection.state === "connected" &&
|
|
19
|
+
? entry.connection.state === "connected" &&
|
|
20
|
+
entry.startupCatalogPending !== true &&
|
|
21
|
+
entry.localCatalogOnly !== true
|
|
20
22
|
? await collectToolCatalog(entry.name, entry.connection, server.config, {
|
|
21
23
|
agentDir: entry.agentDir,
|
|
22
24
|
artifacts: entry.artifacts,
|
|
23
25
|
ensureFresh: entry.ensureFresh,
|
|
26
|
+
invocation: entry.invocation,
|
|
24
27
|
outputGuard: config.settings.outputGuard,
|
|
25
28
|
})
|
|
26
29
|
: []
|
|
@@ -28,6 +31,7 @@ export async function registerDirectMcpTools(pi, config, entries, toolSearchServ
|
|
|
28
31
|
agentDir: entry.agentDir,
|
|
29
32
|
artifacts: entry.artifacts,
|
|
30
33
|
ensureFresh: entry.ensureFresh,
|
|
34
|
+
invocation: entry.invocation,
|
|
31
35
|
outputGuard: config.settings.outputGuard,
|
|
32
36
|
});
|
|
33
37
|
const identity = mcpRegistrationIdentity(catalog, entry.cachedCatalog);
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
// MCP feeds its searchable documents and activation hook into that service.
|
|
7
7
|
import { Text } from "@earendil-works/pi-tui";
|
|
8
8
|
import { Type } from "typebox";
|
|
9
|
+
import { getDispatchIdentity, registerDispatchIdentity } from "../../permission-system/dispatch-metadata.js";
|
|
9
10
|
import { deriveMcpRegistrationId } from "../../tool-search/engine/marker.js";
|
|
10
11
|
import { registerToolsPreservingActiveSet } from "../active-set.js";
|
|
11
12
|
import { createMcpProxyTool } from "./proxy.js";
|
|
@@ -37,6 +38,8 @@ export function registerMcpTierBTools(pi, input, toolSearchService, warn) {
|
|
|
37
38
|
}));
|
|
38
39
|
const fullDefs = buildMcpToolDefinitions(input.registeredEntries, warn);
|
|
39
40
|
const fullByName = new Map(fullDefs.map((def) => [def.name, def]));
|
|
41
|
+
const promoted = promotedNamesByRegistrar.get(pi) ?? new Set();
|
|
42
|
+
promotedNamesByRegistrar.set(pi, promoted);
|
|
40
43
|
const gatewayNames = [];
|
|
41
44
|
for (const gateway of input.proxyGateways ?? []) {
|
|
42
45
|
const tool = createMcpProxyTool(gateway.server, gateway.entries);
|
|
@@ -47,7 +50,11 @@ export function registerMcpTierBTools(pi, input, toolSearchService, warn) {
|
|
|
47
50
|
pi.registerTool(tool);
|
|
48
51
|
gatewayNames.push(tool.name);
|
|
49
52
|
}
|
|
50
|
-
const activeMcpNames = [
|
|
53
|
+
const activeMcpNames = [
|
|
54
|
+
...mapMcpCatalogNames(input.activeEntries).map(({ name }) => name),
|
|
55
|
+
...fullDefs.filter((def) => input.searchMode && promoted.has(def.name)).map((def) => def.name),
|
|
56
|
+
...gatewayNames,
|
|
57
|
+
];
|
|
51
58
|
const managedNames = new Set([...fullDefs.map((def) => def.name), ...gatewayNames]);
|
|
52
59
|
const previousManagedNames = managedNamesByRegistrar.get(pi) ?? new Set();
|
|
53
60
|
managedNamesByRegistrar.set(pi, managedNames);
|
|
@@ -59,6 +66,8 @@ export function registerMcpTierBTools(pi, input, toolSearchService, warn) {
|
|
|
59
66
|
const known = [...new Set(names.filter((name) => registeredNames.has(name)))];
|
|
60
67
|
if (known.length === 0)
|
|
61
68
|
return;
|
|
69
|
+
for (const name of known)
|
|
70
|
+
promoted.add(name);
|
|
62
71
|
if (stubSwap)
|
|
63
72
|
swapStubsToFull(pi, known, stubbed, fullByName);
|
|
64
73
|
const current = pi.getActiveTools();
|
|
@@ -86,8 +95,6 @@ export function registerMcpTierBTools(pi, input, toolSearchService, warn) {
|
|
|
86
95
|
// stubSwap: every search-mode tool is registered as a tiny stub and kept
|
|
87
96
|
// active so the tools array is length-stable; direct tools stay full.
|
|
88
97
|
const directActive = new Set(activeMcpNames);
|
|
89
|
-
const promoted = promotedNamesByRegistrar.get(pi) ?? new Set();
|
|
90
|
-
promotedNamesByRegistrar.set(pi, promoted);
|
|
91
98
|
const toRegister = fullDefs.map((def) => {
|
|
92
99
|
if (directActive.has(def.name) || promoted.has(def.name))
|
|
93
100
|
return def;
|
|
@@ -135,13 +142,17 @@ function isLegacyMcpRegistrationName(name) {
|
|
|
135
142
|
* definition and runs the call in the same turn, so the model never needs a
|
|
136
143
|
* tool_search round trip; without a promotion hook it only points at tool_search. */
|
|
137
144
|
export function buildMcpStubDefinition(name, promotion) {
|
|
145
|
+
const parameters = Type.Object({}, { additionalProperties: true });
|
|
146
|
+
if (promotion !== undefined) {
|
|
147
|
+
registerDispatchIdentity(parameters, (input) => getDispatchIdentity(promotion.full.parameters, input));
|
|
148
|
+
}
|
|
138
149
|
return {
|
|
139
150
|
name,
|
|
140
151
|
label: name,
|
|
141
152
|
description: promotion === undefined
|
|
142
153
|
? `Inactive MCP tool. Run tool_search to activate ${name}, then call it on your next turn.`
|
|
143
154
|
: `Deferred MCP tool. Call ${name} with its real arguments; it activates and runs on this first call (tool_search lists its schema).`,
|
|
144
|
-
parameters
|
|
155
|
+
parameters,
|
|
145
156
|
executionMode: "parallel",
|
|
146
157
|
async execute(toolCallId, params, signal, onUpdate, ctx) {
|
|
147
158
|
if (promotion === undefined) {
|
|
@@ -8,7 +8,6 @@ import { registerMcpPromptCommands } from "./prompts.js";
|
|
|
8
8
|
import { expandMcpResourceMentions } from "./resources.js";
|
|
9
9
|
import { getMcpService, McpService } from "./service.js";
|
|
10
10
|
import { parseSkillMcpDeclarations, skillActivationTargets, } from "./skills.js";
|
|
11
|
-
import { MCP_ATTACH_SETTLE_TIMEOUT_MS } from "./startup-race.js";
|
|
12
11
|
import { reportMcpAsyncError, safeEventBusOn, wrapAsync } from "./wrap.js";
|
|
13
12
|
const MCP_BUILTIN_EXTENSION_PATH = "<builtin:mcp>";
|
|
14
13
|
export function createMcpExtension(service, sessionOwned = true) {
|
|
@@ -91,16 +90,8 @@ export function createMcpExtension(service, sessionOwned = true) {
|
|
|
91
90
|
}
|
|
92
91
|
return undefined;
|
|
93
92
|
});
|
|
94
|
-
// Attach is
|
|
95
|
-
//
|
|
96
|
-
// collection is awaited inside attachSession) can still be in flight when
|
|
97
|
-
// before_agent_start fires. The old `attached` boolean was only set on
|
|
98
|
-
// completion, so before_agent_start would start a SECOND concurrent attach —
|
|
99
|
-
// which found the connection entries already created (still "connecting"),
|
|
100
|
-
// collected an empty catalog, and registered no MCP tools for turn 1; the
|
|
101
|
-
// first attach then landed the real registration turns later. Memoizing the
|
|
102
|
-
// in-flight promise makes before_agent_start await the ORIGINAL attach, so
|
|
103
|
-
// the first turn's payload deterministically carries the MCP tool set.
|
|
93
|
+
// Attach is single-flight: the prompt uses the original attach's bindings
|
|
94
|
+
// and known catalog, without waiting for deferred remote discovery.
|
|
104
95
|
// session_start always starts a fresh attach (reloads must re-sync config).
|
|
105
96
|
const attach = (event, ctx) => {
|
|
106
97
|
attachedSessionId = ctx.sessionManager?.getSessionId?.();
|
|
@@ -117,11 +108,7 @@ export function createMcpExtension(service, sessionOwned = true) {
|
|
|
117
108
|
const onSessionStart = wrapAsync("mcp.session_start", (event, ctx) => attach(event, ctx), sink);
|
|
118
109
|
pi.on("session_start", (event, ctx) => {
|
|
119
110
|
const work = onSessionStart(event, ctx);
|
|
120
|
-
//
|
|
121
|
-
// puts a cold server's boot and catalog handshake in front of the first frame: measured at
|
|
122
|
-
// 254ms median of a 292ms dispatch on a real config, against 0.2ms with no servers. Attach
|
|
123
|
-
// is single-flight via attachPromise + service.#attachQueue and before_agent_start awaits
|
|
124
|
-
// it, so the first turn still carries the full tool set; only the first paint stops waiting.
|
|
111
|
+
// First paint does not wait for attach; prompt preparation reuses its promise.
|
|
125
112
|
void work;
|
|
126
113
|
});
|
|
127
114
|
const onBeforeAgentStart = async (event, ctx) => {
|
|
@@ -148,16 +135,8 @@ export function createMcpExtension(service, sessionOwned = true) {
|
|
|
148
135
|
for (const warning of warnings)
|
|
149
136
|
createMcpLogger("skills").warn(warning);
|
|
150
137
|
}
|
|
151
|
-
//
|
|
152
|
-
//
|
|
153
|
-
// server's stale instructions - or none at all - for the whole session.
|
|
154
|
-
// Await the attach's own completion signal instead, bounded; on timeout the
|
|
155
|
-
// turn still goes out and the server's catalog lands on a later turn.
|
|
156
|
-
if ((await service.whenAttachSettled()) === "timeout") {
|
|
157
|
-
createMcpLogger("service").warn("MCP attach still settling at prompt build", {
|
|
158
|
-
timeoutMs: MCP_ATTACH_SETTLE_TIMEOUT_MS,
|
|
159
|
-
});
|
|
160
|
-
}
|
|
138
|
+
// Use the known instructions now. Deferred catalog registration refreshes
|
|
139
|
+
// the service for subsequent turns instead of gating this provider request.
|
|
161
140
|
const systemPrompt = injectMcpInstructions(service, event.systemPrompt);
|
|
162
141
|
return systemPrompt === undefined ? undefined : { systemPrompt };
|
|
163
142
|
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { ExtensionToolContext } from "../../types.ts";
|
|
2
|
+
import { type DispatchIdentity } from "../permission-system/dispatch.ts";
|
|
3
|
+
import type { McpToolCatalogEntry } from "./catalog.ts";
|
|
4
|
+
import { type McpAsyncErrorSink } from "./wrap.ts";
|
|
5
|
+
export interface McpInvocation {
|
|
6
|
+
readonly toolCallId: string;
|
|
7
|
+
readonly toolName: string;
|
|
8
|
+
readonly input: Record<string, unknown>;
|
|
9
|
+
readonly context?: ExtensionToolContext;
|
|
10
|
+
}
|
|
11
|
+
export interface McpInvocationError {
|
|
12
|
+
readonly kind: "unavailable" | "schema_changed" | "permission_denied";
|
|
13
|
+
readonly server: string;
|
|
14
|
+
readonly tool: string;
|
|
15
|
+
readonly message: string;
|
|
16
|
+
readonly currentDefinition?: McpToolCatalogEntry["schema"];
|
|
17
|
+
}
|
|
18
|
+
export type McpPreparedInvocation = {
|
|
19
|
+
readonly kind: "ready";
|
|
20
|
+
readonly entry: McpToolCatalogEntry;
|
|
21
|
+
readonly isCurrent: () => boolean;
|
|
22
|
+
} | {
|
|
23
|
+
readonly kind: "refused";
|
|
24
|
+
readonly error: McpInvocationError;
|
|
25
|
+
};
|
|
26
|
+
export interface McpInvocationResolver {
|
|
27
|
+
identity(entry: McpToolCatalogEntry): DispatchIdentity;
|
|
28
|
+
resolve(entry: McpToolCatalogEntry, args: Record<string, unknown>, invocation: McpInvocation | undefined, signal: AbortSignal | undefined): Promise<McpPreparedInvocation>;
|
|
29
|
+
}
|
|
30
|
+
interface InvocationScope {
|
|
31
|
+
readonly owner: object;
|
|
32
|
+
readonly sink: McpAsyncErrorSink;
|
|
33
|
+
readonly configuration: string;
|
|
34
|
+
readonly contextRequired: boolean;
|
|
35
|
+
readonly sessionManager?: object;
|
|
36
|
+
readonly isCurrent: () => boolean;
|
|
37
|
+
readonly ready: () => Promise<void>;
|
|
38
|
+
readonly current: (tool: string) => McpToolCatalogEntry | undefined;
|
|
39
|
+
}
|
|
40
|
+
export declare function createMcpInvocationResolver(scope: InvocationScope): McpInvocationResolver;
|
|
41
|
+
export {};
|
|
42
|
+
//# sourceMappingURL=invocation.d.ts.map
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { Type } from "typebox";
|
|
2
|
+
import { Check } from "typebox/value";
|
|
3
|
+
import { authorizeToolDispatch, hasDispatchAuthorizer } from "../permission-system/dispatch.js";
|
|
4
|
+
import { getDispatchIdentity } from "../permission-system/dispatch-metadata.js";
|
|
5
|
+
import { TimeoutError } from "./errors.js";
|
|
6
|
+
import { safeTimer } from "./wrap.js";
|
|
7
|
+
export function createMcpInvocationResolver(scope) {
|
|
8
|
+
const identity = (entry) => ({
|
|
9
|
+
owner: scope.owner,
|
|
10
|
+
operation: JSON.stringify([entry.server, entry.tool]),
|
|
11
|
+
metadata: JSON.stringify([
|
|
12
|
+
scope.configuration,
|
|
13
|
+
entry.server,
|
|
14
|
+
entry.tool,
|
|
15
|
+
entry.schema,
|
|
16
|
+
entry.description,
|
|
17
|
+
entry.annotations,
|
|
18
|
+
]),
|
|
19
|
+
});
|
|
20
|
+
return {
|
|
21
|
+
identity,
|
|
22
|
+
async resolve(offered, args, invocation, signal) {
|
|
23
|
+
const refuse = (kind, message, current) => ({
|
|
24
|
+
kind: "refused",
|
|
25
|
+
error: {
|
|
26
|
+
kind,
|
|
27
|
+
message,
|
|
28
|
+
server: offered.server,
|
|
29
|
+
tool: offered.tool,
|
|
30
|
+
currentDefinition: current?.schema,
|
|
31
|
+
},
|
|
32
|
+
});
|
|
33
|
+
signal?.throwIfAborted();
|
|
34
|
+
if (!scope.isCurrent())
|
|
35
|
+
return refuse("unavailable", "MCP session or server configuration was replaced.");
|
|
36
|
+
const ctx = invocation?.context;
|
|
37
|
+
if (scope.contextRequired &&
|
|
38
|
+
(ctx === undefined || (scope.sessionManager !== undefined && ctx.sessionManager !== scope.sessionManager))) {
|
|
39
|
+
return refuse("unavailable", "MCP invocation does not belong to this session.");
|
|
40
|
+
}
|
|
41
|
+
const deadline = new AbortController();
|
|
42
|
+
const timeout = safeTimer("invocation.catalog.deadline", offered.requestTimeoutMs, () => deadline.abort(new TimeoutError("MCP metadata was not ready before the request timeout.", {
|
|
43
|
+
phase: "catalog",
|
|
44
|
+
serverName: offered.server,
|
|
45
|
+
})), scope.sink);
|
|
46
|
+
const readySignal = signal === undefined ? deadline.signal : AbortSignal.any([signal, deadline.signal]);
|
|
47
|
+
const ready = scope.ready();
|
|
48
|
+
let abort = () => { };
|
|
49
|
+
const interrupted = new Promise((_resolve, reject) => {
|
|
50
|
+
abort = () => reject(readySignal.reason);
|
|
51
|
+
readySignal.addEventListener("abort", abort, { once: true });
|
|
52
|
+
if (readySignal.aborted)
|
|
53
|
+
abort();
|
|
54
|
+
});
|
|
55
|
+
try {
|
|
56
|
+
await Promise.race([ready, interrupted]);
|
|
57
|
+
}
|
|
58
|
+
finally {
|
|
59
|
+
clearTimeout(timeout);
|
|
60
|
+
readySignal.removeEventListener("abort", abort);
|
|
61
|
+
}
|
|
62
|
+
signal?.throwIfAborted();
|
|
63
|
+
if (!scope.isCurrent())
|
|
64
|
+
return refuse("unavailable", "MCP session or server configuration was replaced.");
|
|
65
|
+
const current = scope.current(offered.tool);
|
|
66
|
+
if (current === undefined)
|
|
67
|
+
return refuse("unavailable", "MCP tool is no longer available.");
|
|
68
|
+
// Validate the current raw schema, never the coercing preflight schema.
|
|
69
|
+
if (!Check(Type.Unsafe(current.schema), args)) {
|
|
70
|
+
return refuse("schema_changed", "Arguments do not match the current MCP tool schema.", current);
|
|
71
|
+
}
|
|
72
|
+
const preparedIdentity = identity(current);
|
|
73
|
+
let permissionFence;
|
|
74
|
+
let operationFence;
|
|
75
|
+
if (ctx !== undefined &&
|
|
76
|
+
invocation !== undefined &&
|
|
77
|
+
(scope.contextRequired || ctx.sessionManager !== undefined)) {
|
|
78
|
+
operationFence = () => {
|
|
79
|
+
const registered = ctx.tools.find((tool) => tool.name === invocation.toolName);
|
|
80
|
+
const registeredIdentity = registered === undefined ? undefined : getDispatchIdentity(registered.parameters, invocation.input);
|
|
81
|
+
return (registeredIdentity?.owner === preparedIdentity.owner &&
|
|
82
|
+
registeredIdentity.operation === preparedIdentity.operation);
|
|
83
|
+
};
|
|
84
|
+
if (!operationFence()) {
|
|
85
|
+
return refuse("unavailable", "MCP invocation name no longer identifies this operation.", current);
|
|
86
|
+
}
|
|
87
|
+
if (hasDispatchAuthorizer(ctx.sessionManager)) {
|
|
88
|
+
try {
|
|
89
|
+
permissionFence = await authorizeToolDispatch(ctx.sessionManager, { ...invocation, identity: preparedIdentity }, ctx, signal);
|
|
90
|
+
}
|
|
91
|
+
catch (error) {
|
|
92
|
+
signal?.throwIfAborted();
|
|
93
|
+
return refuse("permission_denied", error instanceof Error ? error.message : String(error), current);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
else if (ctx.loadedExtensionPaths === undefined ||
|
|
97
|
+
ctx.loadedExtensionPaths.some((path) => path.includes("permission-system"))) {
|
|
98
|
+
return refuse("permission_denied", "Permission authorizer is unavailable.", current);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
return {
|
|
102
|
+
kind: "ready",
|
|
103
|
+
entry: current,
|
|
104
|
+
isCurrent: () => {
|
|
105
|
+
if (signal?.aborted || !scope.isCurrent())
|
|
106
|
+
return false;
|
|
107
|
+
const latest = scope.current(offered.tool);
|
|
108
|
+
return (latest !== undefined &&
|
|
109
|
+
identity(latest).metadata === preparedIdentity.metadata &&
|
|
110
|
+
Check(Type.Unsafe(latest.schema), args) &&
|
|
111
|
+
(operationFence === undefined || operationFence()) &&
|
|
112
|
+
(permissionFence === undefined || permissionFence()));
|
|
113
|
+
},
|
|
114
|
+
};
|
|
115
|
+
},
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
//# sourceMappingURL=invocation.js.map
|
|
@@ -16,6 +16,9 @@ interface SessionConnectionOptions {
|
|
|
16
16
|
readonly ui: McpElicitationUiProvider;
|
|
17
17
|
readonly artifacts: McpOutputArtifacts;
|
|
18
18
|
readonly shouldReconnect: (entry: McpConnectionEntry) => boolean;
|
|
19
|
+
readonly credentialIdentity?: string;
|
|
20
|
+
readonly credentialsCurrent?: () => boolean;
|
|
21
|
+
readonly onCredentialsChanged?: () => Promise<void>;
|
|
19
22
|
}
|
|
20
23
|
export declare function createMcpSessionConnection(options: SessionConnectionOptions): McpConnectionEntry;
|
|
21
24
|
export declare function disposeEntryConnection(entry: McpConnectionEntry, registry: HostMcpRegistry, owner: object): Promise<void>;
|
|
@@ -20,6 +20,8 @@ export function createMcpSessionConnection(options) {
|
|
|
20
20
|
for (const warning of detectLiteralBearerWarnings(name, config))
|
|
21
21
|
logger.warn(warning);
|
|
22
22
|
const connectionOptions = {
|
|
23
|
+
credentialIdentity: options.credentialIdentity,
|
|
24
|
+
credentialsCurrent: options.credentialsCurrent,
|
|
23
25
|
authProvider: authPlan.provider,
|
|
24
26
|
config,
|
|
25
27
|
env: session.env,
|
|
@@ -39,6 +41,10 @@ export function createMcpSessionConnection(options) {
|
|
|
39
41
|
artifacts: options.artifacts,
|
|
40
42
|
authPlan,
|
|
41
43
|
cacheRefreshedAfterConnect: false,
|
|
44
|
+
isCurrent: () => options.shouldReconnect(entry),
|
|
45
|
+
credentialIdentity: options.credentialIdentity,
|
|
46
|
+
credentialsCurrent: options.credentialsCurrent,
|
|
47
|
+
onCredentialsChanged: options.onCredentialsChanged,
|
|
42
48
|
key,
|
|
43
49
|
name,
|
|
44
50
|
configHash: options.configHash,
|
|
@@ -7,6 +7,10 @@ type McpToolRegistrar = Pick<ExtensionAPI, "getActiveTools" | "setActiveTools" |
|
|
|
7
7
|
export interface McpServiceDirectToolRegistrationOptions {
|
|
8
8
|
readonly refreshActiveSetWhenEmpty?: boolean;
|
|
9
9
|
readonly onRegistered?: (entry: McpConnectionEntry, identity: string) => void;
|
|
10
|
+
readonly isCurrent?: (entry: McpConnectionEntry) => boolean;
|
|
11
|
+
readonly contextRequired?: boolean;
|
|
12
|
+
readonly sessionManager?: object;
|
|
13
|
+
readonly publishCurrent?: () => Promise<void>;
|
|
10
14
|
}
|
|
11
15
|
export declare function registerMcpServiceDirectTools(pi: McpToolRegistrar, config: ResolvedMcpConfig, entries: Iterable<McpConnectionEntry>, toolSearchService: ToolSearchService, options?: McpServiceDirectToolRegistrationOptions): Promise<McpSessionRegistration | undefined>;
|
|
12
16
|
export {};
|
|
@@ -1,15 +1,59 @@
|
|
|
1
|
+
import { cachedToolsToCatalogEntries } from "./catalog.js";
|
|
2
|
+
import { computeMcpExposurePolicy } from "./expose/policy.js";
|
|
1
3
|
import { registerDirectMcpTools } from "./expose/session.js";
|
|
4
|
+
import { ensureMcpToolCallConnection } from "./health.js";
|
|
5
|
+
import { createMcpInvocationResolver } from "./invocation.js";
|
|
2
6
|
import { connectAndRefreshMcpCatalog } from "./startup-race.js";
|
|
3
7
|
export async function registerMcpServiceDirectTools(pi, config, entries, toolSearchService, options = {}) {
|
|
4
8
|
return await registerDirectMcpTools(pi, config, [...entries].map((entry) => {
|
|
5
9
|
const serverConfig = config.servers[entry.name]?.config;
|
|
6
10
|
const claim = entry.startupCatalogClaim;
|
|
7
11
|
const startupCatalogPending = claim?.ownsRegistration() === true;
|
|
12
|
+
const invocation = createMcpInvocationResolver({
|
|
13
|
+
owner: entry,
|
|
14
|
+
sink: { logger: entry.logger },
|
|
15
|
+
configuration: entry.configHash,
|
|
16
|
+
contextRequired: options.contextRequired === true,
|
|
17
|
+
sessionManager: options.sessionManager,
|
|
18
|
+
isCurrent: () => entry.isCurrent?.() !== false && options.isCurrent?.(entry) !== false,
|
|
19
|
+
ready: async () => {
|
|
20
|
+
await ensureMcpToolCallConnection(entry.connection, () => entry.authPlan?.refresh?.ensureFresh().then(() => undefined) ?? Promise.resolve());
|
|
21
|
+
await connectAndRefreshMcpCatalog(entry, serverConfig);
|
|
22
|
+
await entry.startupCatalogClaim?.settled();
|
|
23
|
+
if (entry.isCurrent?.() === false || options.isCurrent?.(entry) === false)
|
|
24
|
+
return;
|
|
25
|
+
await options.publishCurrent?.();
|
|
26
|
+
},
|
|
27
|
+
current: (tool) => {
|
|
28
|
+
if (serverConfig === undefined ||
|
|
29
|
+
entry.credentialsCurrent?.() === false ||
|
|
30
|
+
entry.cachedCatalog === undefined ||
|
|
31
|
+
!entry.cacheRefreshedAfterConnect ||
|
|
32
|
+
entry.catalogGeneration !== entry.connection.generation ||
|
|
33
|
+
entry.connection.state !== "connected") {
|
|
34
|
+
return undefined;
|
|
35
|
+
}
|
|
36
|
+
const current = cachedToolsToCatalogEntries(entry.name, entry.cachedCatalog, entry.connection, serverConfig.requestTimeoutMs, () => connectAndRefreshMcpCatalog(entry, serverConfig), {
|
|
37
|
+
agentDir: entry.agentDir,
|
|
38
|
+
artifacts: entry.artifacts,
|
|
39
|
+
ensureFresh: () => entry.authPlan?.refresh?.ensureFresh().then(() => undefined) ?? Promise.resolve(),
|
|
40
|
+
outputGuard: config.settings.outputGuard,
|
|
41
|
+
invocation,
|
|
42
|
+
});
|
|
43
|
+
return computeMcpExposurePolicy(current, serverConfig, config.settings).filteredEntries.find((candidate) => candidate.tool === tool);
|
|
44
|
+
},
|
|
45
|
+
});
|
|
8
46
|
return {
|
|
9
47
|
agentDir: entry.agentDir,
|
|
10
48
|
artifacts: entry.artifacts,
|
|
11
|
-
cachedCatalog:
|
|
49
|
+
cachedCatalog: entry.credentialsCurrent?.() === false
|
|
50
|
+
? undefined
|
|
51
|
+
: startupCatalogPending
|
|
52
|
+
? claim?.cachedCatalog
|
|
53
|
+
: entry.cachedCatalog,
|
|
12
54
|
startupCatalogPending,
|
|
55
|
+
localCatalogOnly: true,
|
|
56
|
+
invocation,
|
|
13
57
|
onRegistered: (identity) => options.onRegistered?.(entry, identity),
|
|
14
58
|
connection: entry.connection,
|
|
15
59
|
ensureFresh: () => entry.authPlan?.refresh?.ensureFresh().then(() => undefined) ?? Promise.resolve(),
|
|
@@ -40,17 +40,33 @@ export async function refreshMcpToolsOnListChanged(entry, targets, config, conne
|
|
|
40
40
|
const server = config.servers[entry.name];
|
|
41
41
|
if (server?.config === undefined || entry.connection.state !== "connected")
|
|
42
42
|
return;
|
|
43
|
+
if (entry.isCurrent?.() === false)
|
|
44
|
+
return;
|
|
45
|
+
if (entry.credentialsCurrent?.() === false) {
|
|
46
|
+
await entry.onCredentialsChanged?.();
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
const generation = entry.connection.generation;
|
|
50
|
+
const ownsCatalog = () => entry.isCurrent?.() !== false &&
|
|
51
|
+
entry.credentialsCurrent?.() !== false &&
|
|
52
|
+
entry.connection.generation === generation &&
|
|
53
|
+
entry.connection.state === "connected";
|
|
43
54
|
// The startup connect that still owns registration registers its refreshed catalog itself.
|
|
44
55
|
if (connectOnly && entry.startupCatalogClaim?.ownsRegistration() === true)
|
|
45
56
|
return;
|
|
46
57
|
const registeredCatalog = entry.cachedCatalog;
|
|
47
58
|
if (entry.connection instanceof SharedMcpLease) {
|
|
48
|
-
|
|
59
|
+
const refreshed = await entry.connection.catalog();
|
|
60
|
+
if (!ownsCatalog())
|
|
61
|
+
return;
|
|
62
|
+
entry.cachedCatalog = refreshed;
|
|
49
63
|
}
|
|
50
64
|
const catalog = await collectToolCatalog(entry.name, entry.connection, server.config, {
|
|
51
65
|
agentDir: entry.agentDir,
|
|
52
66
|
outputGuard: config.settings.outputGuard,
|
|
53
67
|
});
|
|
68
|
+
if (!ownsCatalog())
|
|
69
|
+
return;
|
|
54
70
|
const newNames = mapMcpCatalogNames(catalog).map(({ name }) => name);
|
|
55
71
|
// Before the first refresh, the registered names are the catalog the startup pass registered.
|
|
56
72
|
const knownNames = entry.knownToolNames ??
|
|
@@ -65,9 +81,14 @@ export async function refreshMcpToolsOnListChanged(entry, targets, config, conne
|
|
|
65
81
|
// Registration reads entry.cachedCatalog. A shared lease refreshed it above; nothing else
|
|
66
82
|
// refreshes a non-shared connection's catalog after its startup connect (#2188).
|
|
67
83
|
if (!(entry.connection instanceof SharedMcpLease)) {
|
|
68
|
-
|
|
69
|
-
|
|
84
|
+
const refreshed = await collectServerCatalogForCache(entry.connection, server.config, entry.configHash, entry.credentialIdentity);
|
|
85
|
+
if (!ownsCatalog())
|
|
86
|
+
return;
|
|
87
|
+
entry.cachedCatalog = refreshed;
|
|
88
|
+
await writeMcpCachedServer(entry.agentDir, entry.name, refreshed, ownsCatalog);
|
|
70
89
|
}
|
|
90
|
+
if (!ownsCatalog())
|
|
91
|
+
return;
|
|
71
92
|
// Tombstone removed tools BEFORE re-registration so the subsequent
|
|
72
93
|
// setActiveTools (which excludes them) leaves the tombstones inactive.
|
|
73
94
|
const seen = new Set(initial.map((target) => target.pi));
|
|
@@ -75,6 +96,8 @@ export async function refreshMcpToolsOnListChanged(entry, targets, config, conne
|
|
|
75
96
|
let pending = stale;
|
|
76
97
|
while (pending.length > 0) {
|
|
77
98
|
for (const target of pending) {
|
|
99
|
+
if (!ownsCatalog())
|
|
100
|
+
return;
|
|
78
101
|
try {
|
|
79
102
|
for (const removed of diff.removed)
|
|
80
103
|
target.pi.registerTool(buildMcpTombstoneDefinition(removed, entry.name));
|