agent-trellis 0.18.0 → 0.20.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/dist/cli.js +12 -1
- package/dist/commands/doctor.d.ts +8 -0
- package/dist/commands/doctor.js +87 -2
- package/dist/commands/evolve.d.ts +43 -0
- package/dist/commands/evolve.js +479 -0
- package/dist/commands/mcpGateway.d.ts +28 -0
- package/dist/commands/mcpGateway.js +40 -6
- package/dist/evolve/agentView.d.ts +32 -0
- package/dist/evolve/agentView.js +34 -0
- package/dist/evolve/apply.d.ts +71 -0
- package/dist/evolve/apply.js +162 -0
- package/dist/evolve/archive.d.ts +23 -0
- package/dist/evolve/archive.js +58 -0
- package/dist/evolve/erase.d.ts +17 -0
- package/dist/evolve/erase.js +65 -0
- package/dist/evolve/extension.d.ts +37 -0
- package/dist/evolve/extension.js +63 -0
- package/dist/evolve/gate.d.ts +95 -0
- package/dist/evolve/gate.js +295 -0
- package/dist/evolve/importHistory.d.ts +91 -0
- package/dist/evolve/importHistory.js +239 -0
- package/dist/evolve/jobMain.d.ts +22 -0
- package/dist/evolve/jobMain.js +52 -0
- package/dist/evolve/jobRun.d.ts +94 -0
- package/dist/evolve/jobRun.js +273 -0
- package/dist/evolve/jobShell.d.ts +46 -0
- package/dist/evolve/jobShell.js +75 -0
- package/dist/evolve/l2.d.ts +93 -0
- package/dist/evolve/l2.js +81 -0
- package/dist/evolve/lesson.d.ts +57 -0
- package/dist/evolve/lesson.js +97 -0
- package/dist/evolve/pending.d.ts +69 -0
- package/dist/evolve/pending.js +116 -0
- package/dist/evolve/probeWorkspace.d.ts +57 -0
- package/dist/evolve/probeWorkspace.js +69 -0
- package/dist/evolve/propose.d.ts +73 -0
- package/dist/evolve/propose.js +136 -0
- package/dist/evolve/recorder.d.ts +68 -0
- package/dist/evolve/recorder.js +169 -0
- package/dist/evolve/report.d.ts +26 -0
- package/dist/evolve/report.js +37 -0
- package/dist/evolve/reportView.d.ts +96 -0
- package/dist/evolve/reportView.js +311 -0
- package/dist/evolve/risk.d.ts +106 -0
- package/dist/evolve/risk.js +224 -0
- package/dist/evolve/sources.d.ts +19 -0
- package/dist/evolve/sources.js +29 -0
- package/dist/evolve/stateDir.d.ts +21 -0
- package/dist/evolve/stateDir.js +56 -0
- package/dist/evolve/surface.d.ts +34 -0
- package/dist/evolve/surface.js +72 -0
- package/dist/evolve/trajectory.d.ts +25 -0
- package/dist/evolve/trajectory.js +88 -0
- package/dist/evolve/treeToCanonical.d.ts +28 -0
- package/dist/evolve/treeToCanonical.js +79 -0
- package/dist/evolve/trellisSubstrate.d.ts +65 -0
- package/dist/evolve/trellisSubstrate.js +154 -0
- package/dist/evolve/trigger.d.ts +177 -0
- package/dist/evolve/trigger.js +400 -0
- package/dist/evolve/types.d.ts +126 -0
- package/dist/evolve/types.js +15 -0
- package/dist/evolve/validate.d.ts +139 -0
- package/dist/evolve/validate.js +315 -0
- package/dist/host/checks.d.ts +57 -0
- package/dist/host/checks.js +126 -0
- package/dist/host/delivery.d.ts +42 -0
- package/dist/host/delivery.js +59 -0
- package/dist/host/edits.d.ts +87 -0
- package/dist/host/edits.js +102 -0
- package/dist/host/hostEvents.d.ts +74 -0
- package/dist/host/hostEvents.js +45 -0
- package/dist/host/index.d.ts +22 -0
- package/dist/host/index.js +17 -0
- package/dist/host/levels.d.ts +106 -0
- package/dist/host/levels.js +206 -0
- package/dist/host/staging.d.ts +56 -0
- package/dist/host/staging.js +106 -0
- package/dist/host/surface.d.ts +36 -0
- package/dist/host/surface.js +20 -0
- package/dist/lib/backup.js +12 -3
- package/dist/lib/fsAtomic.d.ts +7 -0
- package/dist/lib/fsAtomic.js +14 -0
- package/dist/lib/gatewayBackend.d.ts +72 -2
- package/dist/lib/gatewayBackend.js +254 -13
- package/dist/lib/hostEventBus.d.ts +47 -0
- package/dist/lib/hostEventBus.js +94 -0
- package/dist/lib/hostExtensions.d.ts +75 -0
- package/dist/lib/hostExtensions.js +196 -0
- package/dist/lib/mcpRuntime.d.ts +12 -1
- package/dist/lib/mcpRuntime.js +34 -2
- package/dist/lib/mcpToolRegistry.d.ts +28 -2
- package/dist/lib/mcpToolRegistry.js +47 -8
- package/dist/lib/upstreamResources.d.ts +5 -0
- package/dist/lib/upstreamResources.js +10 -0
- package/dist/pi-bridge/bundle.js +33 -5
- package/docs/magpie-gateway-research.md +422 -0
- package/docs/roadmap.md +22 -12
- package/docs/runtime-infra.md +74 -0
- package/docs/self-evolution.md +138 -44
- package/package.json +7 -2
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* External host extension loading (host-extension-points spec, 8.6).
|
|
3
|
+
*
|
|
4
|
+
* The ONLY way an external module becomes a Trellis provider or event
|
|
5
|
+
* subscriber: it is declared in the canonical extension manifest at
|
|
6
|
+
* `~/.trellis/mcp/extensions.yaml` (entries carry `id` + a `load` module
|
|
7
|
+
* specifier) and its module exports `activate(ctx)` returning
|
|
8
|
+
* `{ providers?, subscribers? }`. There is no directory scan and no
|
|
9
|
+
* naming convention — a module on disk that the manifest does not name
|
|
10
|
+
* is never imported.
|
|
11
|
+
*
|
|
12
|
+
* Fault isolation is per extension and total: a manifest that fails to
|
|
13
|
+
* parse, an entry of the wrong shape, an import that throws, an
|
|
14
|
+
* `activate` that throws, or an activation of the wrong shape are each
|
|
15
|
+
* recorded as a failure (and warned), and none of them affect the core
|
|
16
|
+
* providers, upstream servers, or any other extension. With no manifest
|
|
17
|
+
* the loader returns empty everything and the gateway is byte-identical
|
|
18
|
+
* to the pre-extension world.
|
|
19
|
+
*/
|
|
20
|
+
import { existsSync, readFileSync, mkdirSync } from "node:fs";
|
|
21
|
+
import { dirname, join } from "node:path";
|
|
22
|
+
import { parse as parseYaml, stringify as stringifyYaml } from "yaml";
|
|
23
|
+
import { writeFileAtomicSync } from "./fsAtomic.js";
|
|
24
|
+
/** Manifest location relative to the home directory — canonical, not
|
|
25
|
+
* user-home-adjacent: extension declarations are canonical content. */
|
|
26
|
+
export const EXTENSIONS_MANIFEST_REL = join(".trellis", "mcp", "extensions.yaml");
|
|
27
|
+
export function extensionsManifestPath(homeDir) {
|
|
28
|
+
return join(homeDir, EXTENSIONS_MANIFEST_REL);
|
|
29
|
+
}
|
|
30
|
+
function failureMessage(err) {
|
|
31
|
+
return err instanceof Error ? err.message : String(err);
|
|
32
|
+
}
|
|
33
|
+
/** Minimal structural check for one provider handed back by an
|
|
34
|
+
* extension: it must at least be able to answer a call. */
|
|
35
|
+
function isProviderShape(value) {
|
|
36
|
+
return (typeof value === "object" && value !== null &&
|
|
37
|
+
typeof value.id === "string" &&
|
|
38
|
+
typeof value.callTool === "function");
|
|
39
|
+
}
|
|
40
|
+
function isSubscriberShape(value) {
|
|
41
|
+
return typeof value === "function";
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Load every extension the canonical manifest declares. Never throws:
|
|
45
|
+
* the worst case is a `failures` entry per broken extension.
|
|
46
|
+
*/
|
|
47
|
+
export async function loadHostExtensions(context, warn = console.error) {
|
|
48
|
+
const providers = [];
|
|
49
|
+
const subscribers = [];
|
|
50
|
+
const failures = [];
|
|
51
|
+
const report = (failure) => {
|
|
52
|
+
failures.push(failure);
|
|
53
|
+
warn(`trellis-mcp-gateway: extension "${failure.id}" failed (${failure.stage}): ${failure.message}`);
|
|
54
|
+
};
|
|
55
|
+
const manifestPath = extensionsManifestPath(context.homeDir);
|
|
56
|
+
if (!existsSync(manifestPath)) {
|
|
57
|
+
return { providers, subscribers, failures };
|
|
58
|
+
}
|
|
59
|
+
let parsed;
|
|
60
|
+
try {
|
|
61
|
+
parsed = (parseYaml(readFileSync(manifestPath, "utf-8")) ?? {});
|
|
62
|
+
}
|
|
63
|
+
catch (err) {
|
|
64
|
+
report({ id: "<manifest>", stage: "manifest", message: failureMessage(err) });
|
|
65
|
+
return { providers, subscribers, failures };
|
|
66
|
+
}
|
|
67
|
+
const rawEntries = parsed.extensions;
|
|
68
|
+
if (rawEntries === undefined || rawEntries === null) {
|
|
69
|
+
return { providers, subscribers, failures };
|
|
70
|
+
}
|
|
71
|
+
if (!Array.isArray(rawEntries)) {
|
|
72
|
+
report({ id: "<manifest>", stage: "manifest", message: "extensions must be a list of {id, load} entries" });
|
|
73
|
+
return { providers, subscribers, failures };
|
|
74
|
+
}
|
|
75
|
+
for (const raw of rawEntries) {
|
|
76
|
+
const entry = raw;
|
|
77
|
+
if (typeof entry !== "object" || entry === null ||
|
|
78
|
+
typeof entry.id !== "string" || entry.id === "" ||
|
|
79
|
+
typeof entry.load !== "string" || entry.load === "") {
|
|
80
|
+
report({ id: "<manifest>", stage: "invalid", message: `entry is not a {id, load} pair: ${JSON.stringify(raw)}` });
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
let mod;
|
|
84
|
+
try {
|
|
85
|
+
mod = await import(entry.load);
|
|
86
|
+
}
|
|
87
|
+
catch (err) {
|
|
88
|
+
report({ id: entry.id, stage: "import", message: failureMessage(err) });
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
if (typeof mod.activate !== "function") {
|
|
92
|
+
report({ id: entry.id, stage: "invalid", message: "module does not export an activate function" });
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
let activation;
|
|
96
|
+
try {
|
|
97
|
+
activation = (await mod.activate(context)) ?? {};
|
|
98
|
+
}
|
|
99
|
+
catch (err) {
|
|
100
|
+
report({ id: entry.id, stage: "activate", message: failureMessage(err) });
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
if (typeof activation !== "object") {
|
|
104
|
+
report({ id: entry.id, stage: "invalid", message: "activate did not return an object" });
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
const returnedProviders = Array.isArray(activation.providers) ? activation.providers : [];
|
|
108
|
+
for (const provider of returnedProviders) {
|
|
109
|
+
if (isProviderShape(provider)) {
|
|
110
|
+
providers.push(provider);
|
|
111
|
+
}
|
|
112
|
+
else {
|
|
113
|
+
report({ id: entry.id, stage: "invalid", message: "activation.providers contains a non-provider entry" });
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
const returnedSubscribers = Array.isArray(activation.subscribers) ? activation.subscribers : [];
|
|
117
|
+
for (const subscriber of returnedSubscribers) {
|
|
118
|
+
if (isSubscriberShape(subscriber)) {
|
|
119
|
+
subscribers.push(subscriber);
|
|
120
|
+
}
|
|
121
|
+
else {
|
|
122
|
+
report({ id: entry.id, stage: "invalid", message: "activation.subscribers contains a non-function entry" });
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
return { providers, subscribers, failures };
|
|
127
|
+
}
|
|
128
|
+
// ---------- 清单写入(声明面的唯一写路径) ----------
|
|
129
|
+
/**
|
|
130
|
+
* 写入(或原位更新)一条清单条目;幂等——同 id 旧条目被替换,其余条目
|
|
131
|
+
* 与未知键原样保留。清单是 canonical 内容,写入是显式管理动作(进化
|
|
132
|
+
* 启用命令 8.1),loader 侧永不写。
|
|
133
|
+
*/
|
|
134
|
+
export function upsertExtensionEntry(homeDir, entry) {
|
|
135
|
+
if (typeof entry.id !== "string" || entry.id === "" || typeof entry.load !== "string" || entry.load === "") {
|
|
136
|
+
throw new Error(`extensions.yaml: entry must be a non-empty {id, load} pair, got ${JSON.stringify(entry)}`);
|
|
137
|
+
}
|
|
138
|
+
const manifestPath = extensionsManifestPath(homeDir);
|
|
139
|
+
let parsed = {};
|
|
140
|
+
if (existsSync(manifestPath)) {
|
|
141
|
+
const raw = parseYaml(readFileSync(manifestPath, "utf-8"));
|
|
142
|
+
if (raw !== null && raw !== undefined) {
|
|
143
|
+
if (typeof raw !== "object" || Array.isArray(raw)) {
|
|
144
|
+
throw new Error("extensions.yaml: top level must be a mapping with an `extensions` list");
|
|
145
|
+
}
|
|
146
|
+
parsed = raw;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
const existing = parsed.extensions;
|
|
150
|
+
if (existing !== undefined && !Array.isArray(existing)) {
|
|
151
|
+
// 非数组的 extensions 键是手误产物:抛错而不是静默重置,避免
|
|
152
|
+
// 其余条目无声丢失。
|
|
153
|
+
throw new Error("extensions.yaml: `extensions` must be a list of {id, load} entries");
|
|
154
|
+
}
|
|
155
|
+
const entries = (existing ?? []);
|
|
156
|
+
const next = [];
|
|
157
|
+
let replaced = false;
|
|
158
|
+
for (const raw of entries) {
|
|
159
|
+
const candidate = raw;
|
|
160
|
+
if (candidate !== null && typeof candidate === "object" && candidate.id === entry.id) {
|
|
161
|
+
next.push({ id: entry.id, load: entry.load });
|
|
162
|
+
replaced = true;
|
|
163
|
+
}
|
|
164
|
+
else {
|
|
165
|
+
next.push(raw);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
if (!replaced)
|
|
169
|
+
next.push({ id: entry.id, load: entry.load });
|
|
170
|
+
parsed.extensions = next;
|
|
171
|
+
mkdirSync(dirname(manifestPath), { recursive: true });
|
|
172
|
+
writeFileAtomicSync(manifestPath, stringifyYaml(parsed));
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* 移除一条清单条目;不存在时是无副作用的 no-op(返回 false)。不清空
|
|
176
|
+
* 文件、不动其他条目——清单还可能声明别的扩展。
|
|
177
|
+
*/
|
|
178
|
+
export function removeExtensionEntry(homeDir, id) {
|
|
179
|
+
const manifestPath = extensionsManifestPath(homeDir);
|
|
180
|
+
if (!existsSync(manifestPath))
|
|
181
|
+
return false;
|
|
182
|
+
const raw = parseYaml(readFileSync(manifestPath, "utf-8"));
|
|
183
|
+
if (raw === null || raw === undefined || typeof raw !== "object" || Array.isArray(raw))
|
|
184
|
+
return false;
|
|
185
|
+
const parsed = raw;
|
|
186
|
+
const existing = Array.isArray(parsed.extensions) ? parsed.extensions : [];
|
|
187
|
+
const next = existing.filter((item) => {
|
|
188
|
+
const candidate = item;
|
|
189
|
+
return !(candidate !== null && typeof candidate === "object" && candidate.id === id);
|
|
190
|
+
});
|
|
191
|
+
if (next.length === existing.length)
|
|
192
|
+
return false;
|
|
193
|
+
parsed.extensions = next;
|
|
194
|
+
writeFileAtomicSync(manifestPath, stringifyYaml(parsed));
|
|
195
|
+
return true;
|
|
196
|
+
}
|
package/dist/lib/mcpRuntime.d.ts
CHANGED
|
@@ -3,6 +3,7 @@ import type { CallToolResult, GetPromptResult, Implementation, Prompt, ReadResou
|
|
|
3
3
|
import type { AgentId } from "../core/types.js";
|
|
4
4
|
import type { GatewayBackend } from "./gatewayBackend.js";
|
|
5
5
|
import type { SkillEntry } from "./skillManifest.js";
|
|
6
|
+
import type { ToolCallInfo } from "../host/hostEvents.js";
|
|
6
7
|
export interface RuntimeContext {
|
|
7
8
|
agentId: AgentId;
|
|
8
9
|
homeDir: string;
|
|
@@ -23,14 +24,24 @@ export interface TrellisProvider {
|
|
|
23
24
|
export declare class BuiltinRegistry {
|
|
24
25
|
private readonly providers;
|
|
25
26
|
private readonly warn;
|
|
27
|
+
/** Event choke point (host-extension-points): called after every routed
|
|
28
|
+
* tool call with metadata only. Absent = pre-event behavior, unchanged. */
|
|
29
|
+
private readonly onToolCall?;
|
|
26
30
|
private readonly toolOwners;
|
|
27
31
|
private readonly originalToolOwners;
|
|
28
32
|
private readonly exposedToOriginal;
|
|
29
33
|
private readonly resourceOwners;
|
|
30
34
|
private readonly promptOwners;
|
|
31
|
-
constructor(providers: readonly TrellisProvider[], warn?: (message: string) => void
|
|
35
|
+
constructor(providers: readonly TrellisProvider[], warn?: (message: string) => void,
|
|
36
|
+
/** Event choke point (host-extension-points): called after every routed
|
|
37
|
+
* tool call with metadata only. Absent = pre-event behavior, unchanged. */
|
|
38
|
+
onToolCall?: ((info: ToolCallInfo) => void) | undefined);
|
|
32
39
|
listTools(context: RuntimeContext): Promise<Tool[]>;
|
|
33
40
|
callTool(name: string, args: unknown, context: RuntimeContext): Promise<CallToolResult>;
|
|
41
|
+
/** Metadata-only event emission at the call choke point. Fully
|
|
42
|
+
* transparent: a failing observer never changes the tool call's result or
|
|
43
|
+
* error semantics (host-events transparency requirement). */
|
|
44
|
+
private emitToolCall;
|
|
34
45
|
listResources(context: RuntimeContext): Promise<Resource[]>;
|
|
35
46
|
readResource(uri: URL, context: RuntimeContext): Promise<ReadResourceResult>;
|
|
36
47
|
listSkills(context: RuntimeContext): Promise<SkillEntry[]>;
|
package/dist/lib/mcpRuntime.js
CHANGED
|
@@ -8,17 +8,32 @@ function compactBuiltinToolName(name) {
|
|
|
8
8
|
function providerError(provider, error) {
|
|
9
9
|
return `trellis-mcp-runtime: provider "${provider}" failed: ${error instanceof Error ? error.message : String(error)}`;
|
|
10
10
|
}
|
|
11
|
+
function resultCategory(result) {
|
|
12
|
+
if (!result?.isError)
|
|
13
|
+
return "ok";
|
|
14
|
+
const text = JSON.stringify(result.content ?? "");
|
|
15
|
+
return /timed? ?out/i.test(text) ? "timeout" : "error";
|
|
16
|
+
}
|
|
17
|
+
function thrownCategory(err) {
|
|
18
|
+
const message = err instanceof Error ? `${err.name} ${err.message}` : String(err);
|
|
19
|
+
return /timed? ?out/i.test(message) ? "timeout" : "error";
|
|
20
|
+
}
|
|
11
21
|
export class BuiltinRegistry {
|
|
12
22
|
providers;
|
|
13
23
|
warn;
|
|
24
|
+
onToolCall;
|
|
14
25
|
toolOwners = new Map();
|
|
15
26
|
originalToolOwners = new Map();
|
|
16
27
|
exposedToOriginal = new Map();
|
|
17
28
|
resourceOwners = new Map();
|
|
18
29
|
promptOwners = new Map();
|
|
19
|
-
constructor(providers, warn = console.error
|
|
30
|
+
constructor(providers, warn = console.error,
|
|
31
|
+
/** Event choke point (host-extension-points): called after every routed
|
|
32
|
+
* tool call with metadata only. Absent = pre-event behavior, unchanged. */
|
|
33
|
+
onToolCall) {
|
|
20
34
|
this.providers = providers;
|
|
21
35
|
this.warn = warn;
|
|
36
|
+
this.onToolCall = onToolCall;
|
|
22
37
|
}
|
|
23
38
|
async listTools(context) {
|
|
24
39
|
const candidates = [];
|
|
@@ -67,13 +82,30 @@ export class BuiltinRegistry {
|
|
|
67
82
|
}
|
|
68
83
|
if (!provider)
|
|
69
84
|
return { content: [{ type: "text", text: `unknown Trellis tool "${name}"` }], isError: true };
|
|
85
|
+
const started = Date.now();
|
|
70
86
|
try {
|
|
71
|
-
|
|
87
|
+
const result = await provider.callTool(originalName, args, context);
|
|
88
|
+
this.emitToolCall({ provider: provider.id, tool: name, durationMs: Date.now() - started, result: resultCategory(result) });
|
|
89
|
+
return result;
|
|
72
90
|
}
|
|
73
91
|
catch (err) {
|
|
92
|
+
this.emitToolCall({ provider: provider.id, tool: name, durationMs: Date.now() - started, result: thrownCategory(err) });
|
|
74
93
|
return { content: [{ type: "text", text: providerError(provider.id, err) }], isError: true };
|
|
75
94
|
}
|
|
76
95
|
}
|
|
96
|
+
/** Metadata-only event emission at the call choke point. Fully
|
|
97
|
+
* transparent: a failing observer never changes the tool call's result or
|
|
98
|
+
* error semantics (host-events transparency requirement). */
|
|
99
|
+
emitToolCall(info) {
|
|
100
|
+
if (!this.onToolCall)
|
|
101
|
+
return;
|
|
102
|
+
try {
|
|
103
|
+
this.onToolCall(info);
|
|
104
|
+
}
|
|
105
|
+
catch (err) {
|
|
106
|
+
this.warn(`trellis-mcp-runtime: tool-call observer failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
77
109
|
async listResources(context) {
|
|
78
110
|
const resources = [];
|
|
79
111
|
this.resourceOwners.clear();
|
|
@@ -52,6 +52,17 @@ export declare function prefixedToolName(serverName: string, toolName: string):
|
|
|
52
52
|
export interface ToolNameCandidate {
|
|
53
53
|
serverName: string;
|
|
54
54
|
toolName: string;
|
|
55
|
+
/**
|
|
56
|
+
* Force the `server__tool` prefix regardless of uniqueness or length.
|
|
57
|
+
* Gateway aggregation sets this on every candidate so an Agent can always
|
|
58
|
+
* read the owning server from the exposed name (mcp-tool-name-preservation:
|
|
59
|
+
* "Prefix every upstream tool with its server name"). A tool whose name
|
|
60
|
+
* already starts with its server's name (e.g. `tanka_memo_search` on
|
|
61
|
+
* `tanka`) is not prefixed again — the name is already source-qualified.
|
|
62
|
+
* Other consumers (e.g. the built-in Runtime registry) leave it unset to
|
|
63
|
+
* keep their own naming contracts.
|
|
64
|
+
*/
|
|
65
|
+
alwaysPrefix?: boolean;
|
|
55
66
|
}
|
|
56
67
|
/** Allocate all exposed names together so normalization collisions are
|
|
57
68
|
* deterministic and no upstream tool is silently lost. The returned array
|
|
@@ -82,10 +93,25 @@ export declare class McpToolRegistry {
|
|
|
82
93
|
* aggregated, with no special-casing here.
|
|
83
94
|
*/
|
|
84
95
|
add(serverName: string, upstream: ToolUpstream, tools: readonly AggregatedTool[]): AddResult;
|
|
96
|
+
/**
|
|
97
|
+
* Swap one server's entire registration — the mid-session recovery path.
|
|
98
|
+
* Every candidate the server previously contributed is dropped before the
|
|
99
|
+
* fresh tool set is added, so a changed upstream tool set is reflected
|
|
100
|
+
* exactly rather than accumulated. Exposed names are re-allocated across
|
|
101
|
+
* the whole registry, so a name this server frees may be re-assigned
|
|
102
|
+
* deterministically to another server's colliding tool — accepted drift,
|
|
103
|
+
* pinned by the spec scenario.
|
|
104
|
+
*/
|
|
105
|
+
replace(serverName: string, upstream: ToolUpstream, tools: readonly AggregatedTool[]): AddResult;
|
|
85
106
|
private exposedNameFor;
|
|
86
107
|
/** Every registered tool, under its prefixed name, in registration
|
|
87
|
-
* order. Each tool's own `
|
|
88
|
-
*
|
|
108
|
+
* order. Each tool's own `inputSchema` and any additional fields the
|
|
109
|
+
* upstream sent are preserved verbatim; `name` changes, and the
|
|
110
|
+
* description gains a trailing `Source: <server>` line so an Agent can
|
|
111
|
+
* confirm the owning server without relying on the name alone
|
|
112
|
+
* (mcp-gateway-hosting: "Resold tool metadata identifies its upstream
|
|
113
|
+
* source"). The upstream's own description text is never altered or
|
|
114
|
+
* removed — the source line is appended. */
|
|
89
115
|
listTools(): AggregatedTool[];
|
|
90
116
|
/** The owning upstream and original tool name for a prefixed name, or
|
|
91
117
|
* `undefined` if nothing owns it. */
|
|
@@ -81,12 +81,23 @@ export function allocateExposedToolNames(candidates) {
|
|
|
81
81
|
// registry (for example `fixture__env` reaching the Runtime provider).
|
|
82
82
|
// Do not add a second provider prefix around it.
|
|
83
83
|
const sourceQualified = normalized.includes("__") || normalized === source || normalized.startsWith(`${source}_`) || normalized.startsWith(`${source}__`);
|
|
84
|
+
// Narrower than `sourceQualified`: for forced-prefix candidates the
|
|
85
|
+
// exemption applies only when the tool name already starts with ITS
|
|
86
|
+
// OWN server's name (a second prefix would be redundant, e.g.
|
|
87
|
+
// `tanka__tanka_memo_search`). A tool that merely contains `__`
|
|
88
|
+
// (`b__c`) or gained a `__hash` length bound still needs its server
|
|
89
|
+
// prefix to be identifiable.
|
|
90
|
+
const serverRedundant = normalized === source || normalized.startsWith(`${source}_`) || normalized.startsWith(`${source}__`);
|
|
84
91
|
const unique = (counts.get(candidate.toolName) ?? 0) === 1;
|
|
85
|
-
const base =
|
|
86
|
-
?
|
|
87
|
-
: unique && sourceQualified
|
|
92
|
+
const base = candidate.alwaysPrefix
|
|
93
|
+
? serverRedundant
|
|
88
94
|
? normalized
|
|
89
|
-
: prefixedToolName(candidate.serverName, candidate.toolName)
|
|
95
|
+
: prefixedToolName(candidate.serverName, candidate.toolName)
|
|
96
|
+
: unique && normalized.length > SHORT_EXPOSED_TOOL_NAME_LENGTH
|
|
97
|
+
? normalized
|
|
98
|
+
: unique && sourceQualified
|
|
99
|
+
? normalized
|
|
100
|
+
: prefixedToolName(candidate.serverName, candidate.toolName);
|
|
90
101
|
let exposed = base;
|
|
91
102
|
let suffix = 2;
|
|
92
103
|
while (used.has(exposed)) {
|
|
@@ -122,7 +133,26 @@ export class McpToolRegistry {
|
|
|
122
133
|
*/
|
|
123
134
|
add(serverName, upstream, tools) {
|
|
124
135
|
for (const tool of tools)
|
|
125
|
-
this.candidates.push({ serverName, toolName: tool.name, upstream, tool });
|
|
136
|
+
this.candidates.push({ serverName, toolName: tool.name, upstream, tool, alwaysPrefix: true });
|
|
137
|
+
this.rebuild();
|
|
138
|
+
const added = tools.map((tool) => this.exposedNameFor(serverName, tool.name)).filter((name) => name !== undefined);
|
|
139
|
+
return { added, skipped: [] };
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Swap one server's entire registration — the mid-session recovery path.
|
|
143
|
+
* Every candidate the server previously contributed is dropped before the
|
|
144
|
+
* fresh tool set is added, so a changed upstream tool set is reflected
|
|
145
|
+
* exactly rather than accumulated. Exposed names are re-allocated across
|
|
146
|
+
* the whole registry, so a name this server frees may be re-assigned
|
|
147
|
+
* deterministically to another server's colliding tool — accepted drift,
|
|
148
|
+
* pinned by the spec scenario.
|
|
149
|
+
*/
|
|
150
|
+
replace(serverName, upstream, tools) {
|
|
151
|
+
const kept = this.candidates.filter((candidate) => candidate.serverName !== serverName);
|
|
152
|
+
this.candidates.length = 0;
|
|
153
|
+
this.candidates.push(...kept);
|
|
154
|
+
for (const tool of tools)
|
|
155
|
+
this.candidates.push({ serverName, toolName: tool.name, upstream, tool, alwaysPrefix: true });
|
|
126
156
|
this.rebuild();
|
|
127
157
|
const added = tools.map((tool) => this.exposedNameFor(serverName, tool.name)).filter((name) => name !== undefined);
|
|
128
158
|
return { added, skipped: [] };
|
|
@@ -135,8 +165,13 @@ export class McpToolRegistry {
|
|
|
135
165
|
return undefined;
|
|
136
166
|
}
|
|
137
167
|
/** Every registered tool, under its prefixed name, in registration
|
|
138
|
-
* order. Each tool's own `
|
|
139
|
-
*
|
|
168
|
+
* order. Each tool's own `inputSchema` and any additional fields the
|
|
169
|
+
* upstream sent are preserved verbatim; `name` changes, and the
|
|
170
|
+
* description gains a trailing `Source: <server>` line so an Agent can
|
|
171
|
+
* confirm the owning server without relying on the name alone
|
|
172
|
+
* (mcp-gateway-hosting: "Resold tool metadata identifies its upstream
|
|
173
|
+
* source"). The upstream's own description text is never altered or
|
|
174
|
+
* removed — the source line is appended. */
|
|
140
175
|
listTools() {
|
|
141
176
|
this.rebuild();
|
|
142
177
|
return this.order.map((name) => {
|
|
@@ -144,7 +179,11 @@ export class McpToolRegistry {
|
|
|
144
179
|
const title = name === entry.toolName || entry.tool.title !== undefined
|
|
145
180
|
? entry.tool.title
|
|
146
181
|
: `${entry.serverName} / ${entry.toolName}`;
|
|
147
|
-
|
|
182
|
+
const sourceLine = `Source: ${entry.serverName}`;
|
|
183
|
+
const description = entry.tool.description
|
|
184
|
+
? `${entry.tool.description}\n\n${sourceLine}`
|
|
185
|
+
: sourceLine;
|
|
186
|
+
return { ...entry.tool, name, description, ...(title === undefined ? {} : { title }) };
|
|
148
187
|
});
|
|
149
188
|
}
|
|
150
189
|
/** The owning upstream and original tool name for a prefixed name, or
|
|
@@ -13,6 +13,11 @@ export declare class UpstreamResources {
|
|
|
13
13
|
private readonly files;
|
|
14
14
|
constructor(timeoutMs: number, warn: (message: string) => void);
|
|
15
15
|
add(source: string, client: Client): void;
|
|
16
|
+
/** Forget one source entirely: its client and every file route it
|
|
17
|
+
* registered. Called before a source is re-added mid-session — stale
|
|
18
|
+
* routes would otherwise route reads at the closed previous client
|
|
19
|
+
* (`listResources`/`listSkills` re-register the fresh client's routes). */
|
|
20
|
+
removeSource(source: string): void;
|
|
16
21
|
private skillsEnabled;
|
|
17
22
|
private isolated;
|
|
18
23
|
listResources(): Promise<Resource[]>;
|
|
@@ -54,6 +54,16 @@ export class UpstreamResources {
|
|
|
54
54
|
this.warn = warn;
|
|
55
55
|
}
|
|
56
56
|
add(source, client) { this.clients.set(source, client); }
|
|
57
|
+
/** Forget one source entirely: its client and every file route it
|
|
58
|
+
* registered. Called before a source is re-added mid-session — stale
|
|
59
|
+
* routes would otherwise route reads at the closed previous client
|
|
60
|
+
* (`listResources`/`listSkills` re-register the fresh client's routes). */
|
|
61
|
+
removeSource(source) {
|
|
62
|
+
this.clients.delete(source);
|
|
63
|
+
for (const [uri, route] of this.files)
|
|
64
|
+
if (route.source === source)
|
|
65
|
+
this.files.delete(uri);
|
|
66
|
+
}
|
|
57
67
|
skillsEnabled(client) {
|
|
58
68
|
return Boolean(client.getServerCapabilities()?.resources && client.getDiscoverResult()?.capabilities.extensions?.[EXTENSION]);
|
|
59
69
|
}
|
package/dist/pi-bridge/bundle.js
CHANGED
|
@@ -46008,8 +46008,9 @@ function allocateExposedToolNames(candidates) {
|
|
|
46008
46008
|
const normalized = normalizedToolName(candidate.toolName);
|
|
46009
46009
|
const source = compactSourceSegment(candidate.serverName);
|
|
46010
46010
|
const sourceQualified = normalized.includes("__") || normalized === source || normalized.startsWith(`${source}_`) || normalized.startsWith(`${source}__`);
|
|
46011
|
+
const serverRedundant = normalized === source || normalized.startsWith(`${source}_`) || normalized.startsWith(`${source}__`);
|
|
46011
46012
|
const unique = (counts.get(candidate.toolName) ?? 0) === 1;
|
|
46012
|
-
const base = unique && normalized.length > SHORT_EXPOSED_TOOL_NAME_LENGTH ? normalized : unique && sourceQualified ? normalized : prefixedToolName(candidate.serverName, candidate.toolName);
|
|
46013
|
+
const base = candidate.alwaysPrefix ? serverRedundant ? normalized : prefixedToolName(candidate.serverName, candidate.toolName) : unique && normalized.length > SHORT_EXPOSED_TOOL_NAME_LENGTH ? normalized : unique && sourceQualified ? normalized : prefixedToolName(candidate.serverName, candidate.toolName);
|
|
46013
46014
|
let exposed = base;
|
|
46014
46015
|
let suffix = 2;
|
|
46015
46016
|
while (used.has(exposed)) {
|
|
@@ -46042,7 +46043,25 @@ var McpToolRegistry = class {
|
|
|
46042
46043
|
* aggregated, with no special-casing here.
|
|
46043
46044
|
*/
|
|
46044
46045
|
add(serverName, upstream, tools) {
|
|
46045
|
-
for (const tool of tools) this.candidates.push({ serverName, toolName: tool.name, upstream, tool });
|
|
46046
|
+
for (const tool of tools) this.candidates.push({ serverName, toolName: tool.name, upstream, tool, alwaysPrefix: true });
|
|
46047
|
+
this.rebuild();
|
|
46048
|
+
const added = tools.map((tool) => this.exposedNameFor(serverName, tool.name)).filter((name) => name !== void 0);
|
|
46049
|
+
return { added, skipped: [] };
|
|
46050
|
+
}
|
|
46051
|
+
/**
|
|
46052
|
+
* Swap one server's entire registration — the mid-session recovery path.
|
|
46053
|
+
* Every candidate the server previously contributed is dropped before the
|
|
46054
|
+
* fresh tool set is added, so a changed upstream tool set is reflected
|
|
46055
|
+
* exactly rather than accumulated. Exposed names are re-allocated across
|
|
46056
|
+
* the whole registry, so a name this server frees may be re-assigned
|
|
46057
|
+
* deterministically to another server's colliding tool — accepted drift,
|
|
46058
|
+
* pinned by the spec scenario.
|
|
46059
|
+
*/
|
|
46060
|
+
replace(serverName, upstream, tools) {
|
|
46061
|
+
const kept = this.candidates.filter((candidate) => candidate.serverName !== serverName);
|
|
46062
|
+
this.candidates.length = 0;
|
|
46063
|
+
this.candidates.push(...kept);
|
|
46064
|
+
for (const tool of tools) this.candidates.push({ serverName, toolName: tool.name, upstream, tool, alwaysPrefix: true });
|
|
46046
46065
|
this.rebuild();
|
|
46047
46066
|
const added = tools.map((tool) => this.exposedNameFor(serverName, tool.name)).filter((name) => name !== void 0);
|
|
46048
46067
|
return { added, skipped: [] };
|
|
@@ -46054,14 +46073,23 @@ var McpToolRegistry = class {
|
|
|
46054
46073
|
return void 0;
|
|
46055
46074
|
}
|
|
46056
46075
|
/** Every registered tool, under its prefixed name, in registration
|
|
46057
|
-
* order. Each tool's own `
|
|
46058
|
-
*
|
|
46076
|
+
* order. Each tool's own `inputSchema` and any additional fields the
|
|
46077
|
+
* upstream sent are preserved verbatim; `name` changes, and the
|
|
46078
|
+
* description gains a trailing `Source: <server>` line so an Agent can
|
|
46079
|
+
* confirm the owning server without relying on the name alone
|
|
46080
|
+
* (mcp-gateway-hosting: "Resold tool metadata identifies its upstream
|
|
46081
|
+
* source"). The upstream's own description text is never altered or
|
|
46082
|
+
* removed — the source line is appended. */
|
|
46059
46083
|
listTools() {
|
|
46060
46084
|
this.rebuild();
|
|
46061
46085
|
return this.order.map((name) => {
|
|
46062
46086
|
const entry = this.entries.get(name);
|
|
46063
46087
|
const title = name === entry.toolName || entry.tool.title !== void 0 ? entry.tool.title : `${entry.serverName} / ${entry.toolName}`;
|
|
46064
|
-
|
|
46088
|
+
const sourceLine = `Source: ${entry.serverName}`;
|
|
46089
|
+
const description = entry.tool.description ? `${entry.tool.description}
|
|
46090
|
+
|
|
46091
|
+
${sourceLine}` : sourceLine;
|
|
46092
|
+
return { ...entry.tool, name, description, ...title === void 0 ? {} : { title } };
|
|
46065
46093
|
});
|
|
46066
46094
|
}
|
|
46067
46095
|
/** The owning upstream and original tool name for a prefixed name, or
|