@@ -181,14 +181,107 @@ describe("normalizeProbeResult", () => {
181181 expect ( nestedKeys ) . toEqual ( [ "apple" , "zebra" ] ) ;
182182 } ) ;
183183
184- it ( "sorts arrays by JSON string representation" , ( ) => {
185- const input = [
186- { name : "zebra" , value : 1 } ,
187- { name : "apple" , value : 2 } ,
188- ] ;
189- const result = normalizeProbeResult ( input ) as Array < { name : string } > ;
190- expect ( result [ 0 ] . name ) . toBe ( "apple" ) ;
191- expect ( result [ 1 ] . name ) . toBe ( "zebra" ) ;
184+ it ( "sorts arrays of objects by 'name' field (tools)" , ( ) => {
185+ const input = {
186+ tools : [
187+ { name : "zebra_tool" , description : "Z tool" } ,
188+ { name : "apple_tool" , description : "A tool" } ,
189+ { name : "mango_tool" , description : "M tool" } ,
190+ ] ,
191+ } ;
192+ const result = normalizeProbeResult ( input ) as { tools : Array < { name : string } > } ;
193+ expect ( result . tools [ 0 ] . name ) . toBe ( "apple_tool" ) ;
194+ expect ( result . tools [ 1 ] . name ) . toBe ( "mango_tool" ) ;
195+ expect ( result . tools [ 2 ] . name ) . toBe ( "zebra_tool" ) ;
196+ } ) ;
197+
198+ it ( "sorts arrays of objects by 'uri' field (resources)" , ( ) => {
199+ const input = {
200+ resources : [
201+ { uri : "file:///z.txt" , name : "Z" } ,
202+ { uri : "file:///a.txt" , name : "A" } ,
203+ { uri : "file:///m.txt" , name : "M" } ,
204+ ] ,
205+ } ;
206+ const result = normalizeProbeResult ( input ) as { resources : Array < { uri : string } > } ;
207+ expect ( result . resources [ 0 ] . uri ) . toBe ( "file:///a.txt" ) ;
208+ expect ( result . resources [ 1 ] . uri ) . toBe ( "file:///m.txt" ) ;
209+ expect ( result . resources [ 2 ] . uri ) . toBe ( "file:///z.txt" ) ;
210+ } ) ;
211+
212+ it ( "sorts arrays of objects by 'uriTemplate' field (resource templates)" , ( ) => {
213+ const input = {
214+ resourceTemplates : [
215+ { uriTemplate : "file:///{z}" , name : "Z Template" } ,
216+ { uriTemplate : "file:///{a}" , name : "A Template" } ,
217+ ] ,
218+ } ;
219+ const result = normalizeProbeResult ( input ) as {
220+ resourceTemplates : Array < { uriTemplate : string } > ;
221+ } ;
222+ expect ( result . resourceTemplates [ 0 ] . uriTemplate ) . toBe ( "file:///{a}" ) ;
223+ expect ( result . resourceTemplates [ 1 ] . uriTemplate ) . toBe ( "file:///{z}" ) ;
224+ } ) ;
225+
226+ it ( "sorts arrays of objects by 'type' field (content items)" , ( ) => {
227+ const input = {
228+ content : [
229+ { type : "text" , text : "Hello" } ,
230+ { type : "image" , data : "base64..." } ,
231+ { type : "audio" , data : "base64..." } ,
232+ ] ,
233+ } ;
234+ const result = normalizeProbeResult ( input ) as { content : Array < { type : string } > } ;
235+ expect ( result . content [ 0 ] . type ) . toBe ( "audio" ) ;
236+ expect ( result . content [ 1 ] . type ) . toBe ( "image" ) ;
237+ expect ( result . content [ 2 ] . type ) . toBe ( "text" ) ;
238+ } ) ;
239+
240+ it ( "sorts prompt arguments by name" , ( ) => {
241+ const input = {
242+ prompts : [
243+ {
244+ name : "test-prompt" ,
245+ arguments : [
246+ { name : "zebra_arg" , required : true } ,
247+ { name : "apple_arg" , required : false } ,
248+ { name : "mango_arg" , required : true } ,
249+ ] ,
250+ } ,
251+ ] ,
252+ } ;
253+ const result = normalizeProbeResult ( input ) as {
254+ prompts : Array < { arguments : Array < { name : string } > } > ;
255+ } ;
256+ const args = result . prompts [ 0 ] . arguments ;
257+ expect ( args [ 0 ] . name ) . toBe ( "apple_arg" ) ;
258+ expect ( args [ 1 ] . name ) . toBe ( "mango_arg" ) ;
259+ expect ( args [ 2 ] . name ) . toBe ( "zebra_arg" ) ;
260+ } ) ;
261+
262+ it ( "sorts tool inputSchema properties deterministically" , ( ) => {
263+ const input = {
264+ tools : [
265+ {
266+ name : "my_tool" ,
267+ inputSchema : {
268+ type : "object" ,
269+ properties : {
270+ zebra : { type : "string" } ,
271+ apple : { type : "number" } ,
272+ } ,
273+ required : [ "zebra" , "apple" ] ,
274+ } ,
275+ } ,
276+ ] ,
277+ } ;
278+ const result = normalizeProbeResult ( input ) as {
279+ tools : Array < { inputSchema : { properties : Record < string , unknown > ; required : string [ ] } } > ;
280+ } ;
281+ const propKeys = Object . keys ( result . tools [ 0 ] . inputSchema . properties ) ;
282+ expect ( propKeys ) . toEqual ( [ "apple" , "zebra" ] ) ;
283+ // Required array should also be sorted
284+ expect ( result . tools [ 0 ] . inputSchema . required ) . toEqual ( [ "apple" , "zebra" ] ) ;
192285 } ) ;
193286
194287 it ( "handles embedded JSON in text fields" , ( ) => {
@@ -238,4 +331,103 @@ describe("normalizeProbeResult", () => {
238331
239332 expect ( result1 ) . toBe ( result2 ) ;
240333 } ) ;
334+
335+ it ( "produces consistent JSON for complete MCP responses regardless of ordering" , ( ) => {
336+ // Simulate two identical tool responses with different initial ordering
337+ const response1 = {
338+ tools : [
339+ {
340+ name : "get_user" ,
341+ description : "Gets user info" ,
342+ inputSchema : {
343+ type : "object" ,
344+ required : [ "id" , "name" ] ,
345+ properties : { name : { type : "string" } , id : { type : "number" } } ,
346+ } ,
347+ } ,
348+ {
349+ name : "add_numbers" ,
350+ description : "Adds two numbers" ,
351+ inputSchema : {
352+ type : "object" ,
353+ properties : { a : { type : "number" } , b : { type : "number" } } ,
354+ required : [ "a" , "b" ] ,
355+ } ,
356+ } ,
357+ ] ,
358+ } ;
359+
360+ const response2 = {
361+ tools : [
362+ {
363+ description : "Adds two numbers" ,
364+ name : "add_numbers" ,
365+ inputSchema : {
366+ required : [ "a" , "b" ] ,
367+ type : "object" ,
368+ properties : { b : { type : "number" } , a : { type : "number" } } ,
369+ } ,
370+ } ,
371+ {
372+ inputSchema : {
373+ properties : { id : { type : "number" } , name : { type : "string" } } ,
374+ required : [ "name" , "id" ] ,
375+ type : "object" ,
376+ } ,
377+ description : "Gets user info" ,
378+ name : "get_user" ,
379+ } ,
380+ ] ,
381+ } ;
382+
383+ const normalized1 = JSON . stringify ( normalizeProbeResult ( response1 ) , null , 2 ) ;
384+ const normalized2 = JSON . stringify ( normalizeProbeResult ( response2 ) , null , 2 ) ;
385+
386+ expect ( normalized1 ) . toBe ( normalized2 ) ;
387+
388+ // Verify the order is deterministic (add_numbers before get_user)
389+ const parsed = JSON . parse ( normalized1 ) as { tools : Array < { name : string } > } ;
390+ expect ( parsed . tools [ 0 ] . name ) . toBe ( "add_numbers" ) ;
391+ expect ( parsed . tools [ 1 ] . name ) . toBe ( "get_user" ) ;
392+ } ) ;
393+
394+ it ( "is idempotent - normalizing twice produces same result" , ( ) => {
395+ const input = {
396+ tools : [
397+ { name : "z_tool" , description : "Last" } ,
398+ { name : "a_tool" , description : "First" } ,
399+ ] ,
400+ resources : [
401+ { uri : "file:///z.txt" } ,
402+ { uri : "file:///a.txt" } ,
403+ ] ,
404+ } ;
405+
406+ const once = normalizeProbeResult ( input ) ;
407+ const twice = normalizeProbeResult ( once ) ;
408+
409+ expect ( JSON . stringify ( once ) ) . toBe ( JSON . stringify ( twice ) ) ;
410+ } ) ;
411+
412+ it ( "handles arrays without identifiable sort keys" , ( ) => {
413+ const input = {
414+ data : [ 3 , 1 , 4 , 1 , 5 , 9 , 2 , 6 ] ,
415+ } ;
416+ const result = normalizeProbeResult ( input ) as { data : number [ ] } ;
417+ // Numbers sorted as strings
418+ expect ( result . data ) . toEqual ( [ 1 , 1 , 2 , 3 , 4 , 5 , 6 , 9 ] ) ;
419+ } ) ;
420+
421+ it ( "handles mixed arrays with objects lacking standard keys" , ( ) => {
422+ const input = {
423+ items : [
424+ { value : 3 , label : "three" } ,
425+ { value : 1 , label : "one" } ,
426+ ] ,
427+ } ;
428+ const result = normalizeProbeResult ( input ) as { items : Array < { value : number } > } ;
429+ // Falls back to JSON string comparison
430+ expect ( result . items [ 0 ] . value ) . toBe ( 1 ) ;
431+ expect ( result . items [ 1 ] . value ) . toBe ( 3 ) ;
432+ } ) ;
241433} ) ;
0 commit comments