Skip to content

Commit 4bbd78c

Browse files
feat(tools): promote discovered tools only on execute (#1226)
* feat(tools): promote discovered tools only on execute Search returns cards only. A call declares that one name, then dispatches. * fix(tools): keep list_dir off the advertised wire on execute Promote-on-execute flushed any registered unadvertised name, so a list_dir call put its schema on the wire until fold. Search already hides that builtin in favor of glob; dispatch it without advertising. * fix(tools): advertise late MCP tools on execute in tests Search no longer grows the advertised set. The late-dispatch harness now uses the same call-gate promoter as TUI and exec so schemas land on the next infer.
1 parent 0333c14 commit 4bbd78c

14 files changed

Lines changed: 663 additions & 146 deletions

‎e2e/mcp-late-dispatch.test.ts‎

Lines changed: 31 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,10 @@ import { createAgentWithLiveToolDispatch } from "../src/agent/live-tool-dispatch
66
import type { MCPClient } from "../src/mcp/client.js";
77
import { mcpClientTools } from "../src/mcp/plugin.js";
88
import { createPermissionGate } from "../src/permission/gate.js";
9+
import { createAdvertisedToolset } from "../src/session/assemble-runtime.js";
910
import {
1011
closeIntegrationSession,
12+
INTEGRATION_SOURCE,
1113
openIntegrationSession,
1214
runUntilDone,
1315
toolDoneEvents,
@@ -108,13 +110,35 @@ function toolDoneContents(events: ReactorEmittedEvent[]): string[] {
108110
);
109111
}
110112

111-
// Registers dynamic tools as promotable so the next request advertises them.
113+
// Wires production promote-on-execute: search returns cards only; a call to a
114+
// registered-but-unadvertised name declares that one schema, then dispatches.
112115
function promoteDynamicTools(session: IntegrationSession): void {
113-
session.toolset.setToolPromoter(() => {
116+
const advertised = createAdvertisedToolset({
117+
sessionMode: "orchestrator",
118+
toolAvailability: { languageServerAvailable: false },
119+
getProvider: () => ({
120+
providerName: INTEGRATION_SOURCE.provider,
121+
model: INTEGRATION_SOURCE.model,
122+
}),
123+
});
124+
session.toolset.dynamicRunner.setCallGate(
125+
(name) => advertised.isAdvertised(name),
126+
{ isActivated: (name) => advertised.activated.has(name) },
127+
);
128+
session.toolset.setToolPromoter((names) => {
129+
if (!advertised.activated.activate(names)) return;
130+
if (!advertised.flushPromotions()) return;
114131
session.updateToolDefinitions(
115-
session.toolset.dynamicRunner.currentDefinitions(),
132+
advertised.computeAdvertised(
133+
session.toolset.dynamicRunner.currentDefinitions(),
134+
),
116135
);
117136
});
137+
session.updateToolDefinitions(
138+
advertised.computeAdvertised(
139+
session.toolset.dynamicRunner.currentDefinitions(),
140+
),
141+
);
118142
}
119143

120144
function replyScript(
@@ -318,7 +342,10 @@ describe("integration — late MCP dispatch", () => {
318342
promotionScript(session, call);
319343

320344
const { events } = await runUntilDone(session, "list one linear issue");
321-
const published = publishedTool(await requestBodies(session));
345+
const bodies = await requestBodies(session);
346+
expect(bodies.length).toBeGreaterThan(0);
347+
expect(publishedTool(bodies.slice(0, 1))).toBeUndefined();
348+
const published = publishedTool(bodies);
322349

323350
if (schema === undefined) {
324351
expect(published?.input_schema.properties).toEqual(

0 commit comments

Comments
 (0)