@@ -92,11 +92,16 @@ describe("integration — late MCP dispatch", () => {
9292 session . harness . scenario . replyOnce ( "anthropic" , { text : "listed" } ) ;
9393
9494 const { events } = await runUntilDone ( session , "list linear issues" ) ;
95- expect (
96- toolDoneContents ( events ) . some ( ( content ) =>
97- content . includes ( `unknown tool: ${ LATE_MCP } ` ) ,
98- ) ,
99- ) . toBe ( true ) ;
95+ const loud = events . find (
96+ (
97+ event ,
98+ ) : event is Extract < ReactorEmittedEvent , { type : "tool.done" } > =>
99+ event . type === "tool.done" &&
100+ typeof event . data . result . content === "string" &&
101+ event . data . result . content . includes ( `unknown tool: ${ LATE_MCP } ` ) ,
102+ ) ;
103+ expect ( loud ) . toBeDefined ( ) ;
104+ expect ( loud ?. data . result . isError ) . toBe ( true ) ;
100105 } finally {
101106 await closeIntegrationSession ( session ) ;
102107 }
@@ -130,6 +135,96 @@ describe("integration — late MCP dispatch", () => {
130135 } ,
131136 ) ;
132137
138+ test . serial (
139+ "unadvertised MCP tool dispatch fails loudly instead of altering results" ,
140+ async ( ) => {
141+ const session = await openIntegrationSession ( {
142+ permissionGate : permissionGate ( ) ,
143+ createAgentFn : createAgentWithLiveToolDispatch ,
144+ } ) ;
145+
146+ try {
147+ session . toolset . dynamicRunner . addTools ( lateMcpTools ( ) ) ;
148+ const missing = "mcp__linear__no_such_tool" ;
149+ session . harness . scenario . replyOnce ( "anthropic" , {
150+ toolCalls : [ { name : missing , args : { } } ] ,
151+ } ) ;
152+ session . harness . scenario . replyOnce ( "anthropic" , { text : "refused" } ) ;
153+
154+ const { events } = await runUntilDone ( session , "delete everything" ) ;
155+ const loud = events . find (
156+ (
157+ event ,
158+ ) : event is Extract < ReactorEmittedEvent , { type : "tool.done" } > =>
159+ event . type === "tool.done" &&
160+ typeof event . data . result . content === "string" &&
161+ event . data . result . content . includes ( `unknown tool: ${ missing } ` ) ,
162+ ) ;
163+ expect ( loud ) . toBeDefined ( ) ;
164+ expect ( loud ?. data . result . isError ) . toBe ( true ) ;
165+ } finally {
166+ await closeIntegrationSession ( session ) ;
167+ }
168+ } ,
169+ ) ;
170+
171+ test . serial (
172+ "out-of-scope MCP arguments fail loudly with an actionable error" ,
173+ async ( ) => {
174+ const session = await openIntegrationSession ( {
175+ permissionGate : permissionGate ( ) ,
176+ createAgentFn : createAgentWithLiveToolDispatch ,
177+ } ) ;
178+
179+ try {
180+ const client : MCPClient = {
181+ serverName : "linear" ,
182+ tools : [
183+ {
184+ name : "list_issues" ,
185+ description : "list issues" ,
186+ inputSchema : LATE_MCP_SCHEMA ,
187+ } ,
188+ ] ,
189+ async call ( toolName , args ) {
190+ if ( toolName === "list_issues" && args . team === "rogue" ) {
191+ throw new Error (
192+ 'scope denied: team "rogue" is not in scope for this connection' ,
193+ ) ;
194+ }
195+ return "ISSUE-1" ;
196+ } ,
197+ async close ( ) {
198+ return undefined ;
199+ } ,
200+ } ;
201+ session . toolset . dynamicRunner . addTools ( mcpClientTools ( client ) ) ;
202+ session . harness . scenario . replyOnce ( "anthropic" , {
203+ toolCalls : [ { name : LATE_MCP , args : { limit : 1 , team : "rogue" } } ] ,
204+ } ) ;
205+ session . harness . scenario . replyOnce ( "anthropic" , { text : "refused" } ) ;
206+
207+ const { events } = await runUntilDone (
208+ session ,
209+ "list rogue team issues" ,
210+ ) ;
211+ const loud = events . find (
212+ (
213+ event ,
214+ ) : event is Extract < ReactorEmittedEvent , { type : "tool.done" } > =>
215+ event . type === "tool.done" &&
216+ typeof event . data . result . content === "string" &&
217+ event . data . result . content . includes ( "scope denied" ) ,
218+ ) ;
219+ expect ( loud ) . toBeDefined ( ) ;
220+ expect ( loud ?. data . result . isError ) . toBe ( true ) ;
221+ expect ( loud ?. data . result . content ) . toContain ( "not in scope" ) ;
222+ } finally {
223+ await closeIntegrationSession ( session ) ;
224+ }
225+ } ,
226+ ) ;
227+
133228 test . serial (
134229 "tool_search promotion preserves optional MCP arguments" ,
135230 async ( ) => {
@@ -147,6 +242,7 @@ describe("integration — late MCP dispatch", () => {
147242 } ) ;
148243 expect ( tools ) . toHaveLength ( 1 ) ;
149244 expect ( tools [ 0 ] ?. kind ) . toBe ( "full" ) ;
245+ const expectedSchema = structuredClone ( LATE_MCP_SCHEMA ) ;
150246 session . toolset . dynamicRunner . addTools ( tools ) ;
151247 session . toolset . setToolPromoter ( ( ) => {
152248 session . updateToolDefinitions (
@@ -159,7 +255,7 @@ describe("integration — late MCP dispatch", () => {
159255 ] ,
160256 } ) ;
161257 session . harness . scenario . replyOnce ( "anthropic" , {
162- toolCalls : [ { name : LATE_MCP , args : { limit : 1 } } ] ,
258+ toolCalls : [ { name : LATE_MCP , args : { limit : 1 , team : "eng" } } ] ,
163259 } ) ;
164260 session . harness . scenario . replyOnce ( "anthropic" , { text : "listed" } ) ;
165261
@@ -179,12 +275,12 @@ describe("integration — late MCP dispatch", () => {
179275 . find ( ( tool ) => tool . name === LATE_MCP ) ;
180276
181277 expect ( publishedTool ?. input_schema . properties ) . toEqual (
182- LATE_MCP_SCHEMA . properties ,
278+ expectedSchema . properties ,
183279 ) ;
184280 expect (
185281 Object . hasOwn ( publishedTool ?. input_schema ?? { } , "required" ) ,
186282 ) . toBe ( false ) ;
187- expect ( receivedArgs ) . toEqual ( { limit : 1 } ) ;
283+ expect ( receivedArgs ) . toEqual ( { limit : 1 , team : "eng" } ) ;
188284 expect ( toolDoneContents ( events ) ) . toContain ( "ISSUE-1" ) ;
189285 } finally {
190286 await closeIntegrationSession ( session ) ;
@@ -230,6 +326,7 @@ describe("integration — late MCP dispatch", () => {
230326 return undefined ;
231327 } ,
232328 } ;
329+ const expectedSchema = structuredClone ( schema ) ;
233330 session . toolset . dynamicRunner . addTools ( mcpClientTools ( client ) ) ;
234331 session . toolset . setToolPromoter ( ( ) => {
235332 session . updateToolDefinitions (
@@ -242,7 +339,12 @@ describe("integration — late MCP dispatch", () => {
242339 ] ,
243340 } ) ;
244341 session . harness . scenario . replyOnce ( "anthropic" , {
245- toolCalls : [ { name : LATE_MCP , args : { limit : 1 } } ] ,
342+ toolCalls : [
343+ {
344+ name : LATE_MCP ,
345+ args : { limit : 1 , team : "eng" , customView : "mine" } ,
346+ } ,
347+ ] ,
246348 } ) ;
247349 session . harness . scenario . replyOnce ( "anthropic" , { text : "listed" } ) ;
248350
@@ -261,8 +363,12 @@ describe("integration — late MCP dispatch", () => {
261363 . flatMap ( ( body ) => body . tools ?? [ ] )
262364 . find ( ( tool ) => tool . name === LATE_MCP ) ;
263365
264- expect ( publishedTool ?. input_schema ) . toEqual ( schema ) ;
265- expect ( receivedArgs ) . toEqual ( { limit : 1 } ) ;
366+ expect ( publishedTool ?. input_schema ) . toEqual ( expectedSchema ) ;
367+ expect ( receivedArgs ) . toEqual ( {
368+ limit : 1 ,
369+ team : "eng" ,
370+ customView : "mine" ,
371+ } ) ;
266372 expect ( toolDoneContents ( events ) ) . toContain ( "ISSUE-1" ) ;
267373 } finally {
268374 await closeIntegrationSession ( session ) ;
0 commit comments