@@ -3,6 +3,8 @@ import { createAgent } from "@intx/agent";
33import type { ReactorEmittedEvent } from "@intx/inference" ;
44
55import { createAgentWithLiveToolDispatch } from "../../src/agent/live-tool-dispatch.js" ;
6+ import type { MCPClient } from "../../src/mcp/client.js" ;
7+ import { mcpClientTools } from "../../src/mcp/plugin.js" ;
68import { createPermissionGate } from "../../src/permission/gate.js" ;
79import {
810 closeIntegrationSession ,
@@ -11,6 +13,20 @@ import {
1113} from "./harness.js" ;
1214
1315const LATE_MCP = "mcp__linear__list_issues" ;
16+ const LATE_MCP_SCHEMA = {
17+ type : "object" as const ,
18+ properties : {
19+ limit : { type : "integer" } ,
20+ team : { type : "string" } ,
21+ } ,
22+ } ;
23+
24+ interface AnthropicRequestBody {
25+ tools ?: {
26+ name : string ;
27+ input_schema : Record < string , unknown > ;
28+ } [ ] ;
29+ }
1430
1531function permissionGate ( ) {
1632 return createPermissionGate ( {
@@ -21,20 +37,27 @@ function permissionGate() {
2137 } ) ;
2238}
2339
24- function lateMcpTool ( ) {
25- return {
26- kind : "string" as const ,
27- definition : {
28- name : LATE_MCP ,
29- description : "list issues" ,
30- inputSchema : {
31- type : "object" as const ,
32- properties : { } ,
33- required : [ ] as string [ ] ,
40+ function lateMcpTools (
41+ onCall ?: ( toolName : string , args : Record < string , unknown > ) => void ,
42+ ) {
43+ const client : MCPClient = {
44+ serverName : "linear" ,
45+ tools : [
46+ {
47+ name : "list_issues" ,
48+ description : "list issues" ,
49+ inputSchema : LATE_MCP_SCHEMA ,
3450 } ,
51+ ] ,
52+ async call ( toolName , args ) {
53+ onCall ?.( toolName , args ) ;
54+ return "ISSUE-1" ;
55+ } ,
56+ async close ( ) {
57+ return undefined ;
3558 } ,
36- handler : async ( ) => "ISSUE-1" ,
3759 } ;
60+ return mcpClientTools ( client ) ;
3861}
3962
4063function toolDoneContents ( events : ReactorEmittedEvent [ ] ) : string [ ] {
@@ -62,7 +85,7 @@ describe("integration — late MCP dispatch", () => {
6285 } ) ;
6386
6487 try {
65- session . toolset . dynamicRunner . addTools ( [ lateMcpTool ( ) ] ) ;
88+ session . toolset . dynamicRunner . addTools ( lateMcpTools ( ) ) ;
6689 session . harness . scenario . replyOnce ( "anthropic" , {
6790 toolCalls : [ { name : LATE_MCP , args : { } } ] ,
6891 } ) ;
@@ -89,7 +112,7 @@ describe("integration — late MCP dispatch", () => {
89112 } ) ;
90113
91114 try {
92- session . toolset . dynamicRunner . addTools ( [ lateMcpTool ( ) ] ) ;
115+ session . toolset . dynamicRunner . addTools ( lateMcpTools ( ) ) ;
93116 session . harness . scenario . replyOnce ( "anthropic" , {
94117 toolCalls : [ { name : LATE_MCP , args : { } } ] ,
95118 } ) ;
@@ -106,4 +129,66 @@ describe("integration — late MCP dispatch", () => {
106129 }
107130 } ,
108131 ) ;
132+
133+ test . serial (
134+ "tool_search promotion preserves optional MCP arguments" ,
135+ async ( ) => {
136+ const session = await openIntegrationSession ( {
137+ permissionGate : permissionGate ( ) ,
138+ createAgentFn : createAgentWithLiveToolDispatch ,
139+ } ) ;
140+ let receivedArgs : Record < string , unknown > | undefined ;
141+
142+ try {
143+ const tools = lateMcpTools ( ( toolName , args ) => {
144+ if ( toolName === "list_issues" ) {
145+ receivedArgs = args ;
146+ }
147+ } ) ;
148+ expect ( tools ) . toHaveLength ( 1 ) ;
149+ expect ( tools [ 0 ] ?. kind ) . toBe ( "full" ) ;
150+ session . toolset . dynamicRunner . addTools ( tools ) ;
151+ session . toolset . setToolPromoter ( ( ) => {
152+ session . updateToolDefinitions (
153+ session . toolset . dynamicRunner . currentDefinitions ( ) ,
154+ ) ;
155+ } ) ;
156+ session . harness . scenario . replyOnce ( "anthropic" , {
157+ toolCalls : [
158+ { name : "tool_search" , args : { query : "linear list issues" } } ,
159+ ] ,
160+ } ) ;
161+ session . harness . scenario . replyOnce ( "anthropic" , {
162+ toolCalls : [ { name : LATE_MCP , args : { limit : 1 } } ] ,
163+ } ) ;
164+ session . harness . scenario . replyOnce ( "anthropic" , { text : "listed" } ) ;
165+
166+ const { events } = await runUntilDone ( session , "list one linear issue" ) ;
167+ const bodies = await Promise . all (
168+ session . harness . scenario
169+ . matchedRequests ( )
170+ . map (
171+ async ( request ) =>
172+ JSON . parse (
173+ await ( request . clone ( ) as unknown as Request ) . text ( ) ,
174+ ) as AnthropicRequestBody ,
175+ ) ,
176+ ) ;
177+ const publishedTool = bodies
178+ . flatMap ( ( body ) => body . tools ?? [ ] )
179+ . find ( ( tool ) => tool . name === LATE_MCP ) ;
180+
181+ expect ( publishedTool ?. input_schema . properties ) . toEqual (
182+ LATE_MCP_SCHEMA . properties ,
183+ ) ;
184+ expect (
185+ Object . hasOwn ( publishedTool ?. input_schema ?? { } , "required" ) ,
186+ ) . toBe ( false ) ;
187+ expect ( receivedArgs ) . toEqual ( { limit : 1 } ) ;
188+ expect ( toolDoneContents ( events ) ) . toContain ( "ISSUE-1" ) ;
189+ } finally {
190+ await closeIntegrationSession ( session ) ;
191+ }
192+ } ,
193+ ) ;
109194} ) ;
0 commit comments