@@ -247,3 +247,142 @@ describe('patchValidateToolInput', () => {
247247 expect ( result ) . toEqual ( { foo : 'bar' } ) ;
248248 } ) ;
249249} ) ;
250+
251+ // ─── E2E with InMemoryTransport ──────────────────────────────────────────────
252+
253+ describe ( 'patchValidateToolInput (E2E with InMemoryTransport)' , ( ) => {
254+ it ( 'should report all missing fields in one response via MCP protocol' , async ( ) => {
255+ const { InMemoryTransport } = await import ( '@modelcontextprotocol/sdk/inMemory.js' ) ;
256+ const { Client } = await import ( '@modelcontextprotocol/sdk/client/index.js' ) ;
257+
258+ const server = new McpServer ( { name : 'e2e-test' , version : '1.0.0' } ) ;
259+ patchValidateToolInput ( server ) ;
260+
261+ server . tool (
262+ 'audit_store_findings' ,
263+ 'Store findings' ,
264+ {
265+ owner : z . string ( ) . describe ( 'Owner.' ) ,
266+ repo : z . string ( ) . describe ( 'Repo.' ) ,
267+ sourceLocation : z . string ( ) . describe ( 'Path.' ) ,
268+ } ,
269+ async ( { owner, repo, sourceLocation } ) => ( {
270+ content : [ { type : 'text' as const , text : `${ owner } /${ repo } :${ sourceLocation } ` } ] ,
271+ } ) ,
272+ ) ;
273+
274+ const [ clientTransport , serverTransport ] = InMemoryTransport . createLinkedPair ( ) ;
275+ const client = new Client ( { name : 'test-client' , version : '1.0.0' } ) ;
276+
277+ await server . connect ( serverTransport ) ;
278+ await client . connect ( clientTransport ) ;
279+
280+ try {
281+ // Call with no arguments — should report all three missing fields at once
282+ const result = await client . callTool ( {
283+ name : 'audit_store_findings' ,
284+ arguments : { } ,
285+ } ) ;
286+
287+ // The SDK converts McpError into a tool error result
288+ expect ( result . isError ) . toBe ( true ) ;
289+ const text = ( result . content as Array < { type : string ; text : string } > )
290+ . map ( ( c ) => c . text )
291+ . join ( '' ) ;
292+ expect ( text ) . toContain ( "'owner'" ) ;
293+ expect ( text ) . toContain ( "'repo'" ) ;
294+ expect ( text ) . toContain ( "'sourceLocation'" ) ;
295+ expect ( text ) . toContain ( 'must have required properties:' ) ;
296+ } finally {
297+ await client . close ( ) ;
298+ await server . close ( ) ;
299+ }
300+ } ) ;
301+
302+ it ( 'should report single missing field (singular) via MCP protocol' , async ( ) => {
303+ const { InMemoryTransport } = await import ( '@modelcontextprotocol/sdk/inMemory.js' ) ;
304+ const { Client } = await import ( '@modelcontextprotocol/sdk/client/index.js' ) ;
305+
306+ const server = new McpServer ( { name : 'e2e-test' , version : '1.0.0' } ) ;
307+ patchValidateToolInput ( server ) ;
308+
309+ server . tool (
310+ 'annotation_create' ,
311+ 'Create annotation' ,
312+ {
313+ category : z . string ( ) . describe ( 'Cat.' ) ,
314+ entityKey : z . string ( ) . describe ( 'Key.' ) ,
315+ } ,
316+ async ( { category, entityKey } ) => ( {
317+ content : [ { type : 'text' as const , text : `${ category } :${ entityKey } ` } ] ,
318+ } ) ,
319+ ) ;
320+
321+ const [ clientTransport , serverTransport ] = InMemoryTransport . createLinkedPair ( ) ;
322+ const client = new Client ( { name : 'test-client' , version : '1.0.0' } ) ;
323+
324+ await server . connect ( serverTransport ) ;
325+ await client . connect ( clientTransport ) ;
326+
327+ try {
328+ // Supply only one of two required fields
329+ const result = await client . callTool ( {
330+ name : 'annotation_create' ,
331+ arguments : { category : 'note' } ,
332+ } ) ;
333+
334+ expect ( result . isError ) . toBe ( true ) ;
335+ const text = ( result . content as Array < { type : string ; text : string } > )
336+ . map ( ( c ) => c . text )
337+ . join ( '' ) ;
338+ expect ( text ) . toContain ( "must have required property 'entityKey'" ) ;
339+ // Singular — should NOT contain "properties:"
340+ expect ( text ) . not . toContain ( 'properties:' ) ;
341+ } finally {
342+ await client . close ( ) ;
343+ await server . close ( ) ;
344+ }
345+ } ) ;
346+
347+ it ( 'should succeed when all required fields are provided' , async ( ) => {
348+ const { InMemoryTransport } = await import ( '@modelcontextprotocol/sdk/inMemory.js' ) ;
349+ const { Client } = await import ( '@modelcontextprotocol/sdk/client/index.js' ) ;
350+
351+ const server = new McpServer ( { name : 'e2e-test' , version : '1.0.0' } ) ;
352+ patchValidateToolInput ( server ) ;
353+
354+ server . tool (
355+ 'audit_store_findings' ,
356+ 'Store findings' ,
357+ {
358+ owner : z . string ( ) . describe ( 'Owner.' ) ,
359+ repo : z . string ( ) . describe ( 'Repo.' ) ,
360+ } ,
361+ async ( { owner, repo } ) => ( {
362+ content : [ { type : 'text' as const , text : `${ owner } /${ repo } ` } ] ,
363+ } ) ,
364+ ) ;
365+
366+ const [ clientTransport , serverTransport ] = InMemoryTransport . createLinkedPair ( ) ;
367+ const client = new Client ( { name : 'test-client' , version : '1.0.0' } ) ;
368+
369+ await server . connect ( serverTransport ) ;
370+ await client . connect ( clientTransport ) ;
371+
372+ try {
373+ const result = await client . callTool ( {
374+ name : 'audit_store_findings' ,
375+ arguments : { owner : 'octocat' , repo : 'hello' } ,
376+ } ) ;
377+
378+ expect ( result . isError ) . toBeFalsy ( ) ;
379+ const text = ( result . content as Array < { type : string ; text : string } > )
380+ . map ( ( c ) => c . text )
381+ . join ( '' ) ;
382+ expect ( text ) . toBe ( 'octocat/hello' ) ;
383+ } finally {
384+ await client . close ( ) ;
385+ await server . close ( ) ;
386+ }
387+ } ) ;
388+ } ) ;
0 commit comments