1010package io .openliberty .mcp .internal .fat .tool ;
1111
1212import static com .ibm .websphere .simplicity .ShrinkHelper .DeployOptions .SERVER_ONLY ;
13+ import static org .junit .Assert .assertFalse ;
1314import static org .junit .Assert .assertNotNull ;
1415
1516import java .util .List ;
16- import java .util .Map ;
1717
1818import org .jboss .shrinkwrap .api .ShrinkWrap ;
1919import org .jboss .shrinkwrap .api .spec .WebArchive ;
20+ import org .json .JSONArray ;
21+ import org .json .JSONException ;
22+ import org .json .JSONObject ;
2023import org .junit .AfterClass ;
2124import org .junit .BeforeClass ;
2225import org .junit .Rule ;
2326import org .junit .Test ;
2427import org .junit .runner .RunWith ;
2528import org .skyscreamer .jsonassert .JSONAssert ;
29+ import org .skyscreamer .jsonassert .JSONParser ;
2630
2731import com .ibm .websphere .simplicity .ShrinkHelper ;
2832
3640import io .openliberty .mcp .internal .fat .utils .McpClient ;
3741import io .openliberty .mcp .internal .fat .utils .ToolStatus ;
3842import io .openliberty .mcp .internal .fat .utils .ToolStatusClient ;
39- import jakarta .enterprise .util .TypeLiteral ;
40- import jakarta .json .bind .Jsonb ;
41- import jakarta .json .bind .JsonbBuilder ;
4243
4344@ RunWith (FATRunner .class )
4445public class AsyncToolsTest extends FATServletClient {
@@ -307,7 +308,6 @@ public void testCompletionStageThatNeverCompletes() throws Exception {
307308 client .callMCP (request );
308309 }
309310
310- @ SuppressWarnings ({ "unchecked" , "serial" })
311311 @ Test
312312 public void testAsyncObjectToolHasExpectedOutputSchema () throws Exception {
313313 String response = client .callMCP ("""
@@ -318,69 +318,120 @@ public void testAsyncObjectToolHasExpectedOutputSchema() throws Exception {
318318 }
319319 """ );
320320
321- Jsonb jsonb = JsonbBuilder .create ();
322- Map <String , Object > parsed = jsonb .fromJson (response , new TypeLiteral <Map <String , Object >>() {}.getType ());
323- Map <String , Object > result = (Map <String , Object >) parsed .get ("result" );
324- List <Map <String , Object >> tools = (List <Map <String , Object >>) result .get ("tools" );
321+ JSONObject root = (JSONObject ) JSONParser .parseJSON (response );
322+ JSONArray tools = root .getJSONObject ("result" ).getJSONArray ("tools" );
325323
326- Map < String , Object > asyncObjectTool = null ;
324+ JSONObject asyncObjectTool = null ;
327325
328- for (Map <String , Object > tool : tools ) {
329- if ("asyncObjectTool" .equals (tool .get ("name" ))) {
326+ for (int i = 0 ; i < tools .length (); i ++) {
327+ JSONObject tool = tools .getJSONObject (i );
328+ if ("asyncObjectTool" .equals (tool .getString ("name" ))) {
330329 asyncObjectTool = tool ;
331330 break ;
332331 }
333332 }
334333
335334 assertNotNull ("Tool 'asyncObjectTool' should be present in tool list" , asyncObjectTool );
336335
337- String actualToolJson = jsonb . toJson ( asyncObjectTool );
336+ String actualToolJson = asyncObjectTool . toString ( );
338337
339338 String expectedToolJson = """
340- {
341- "name": "asyncObjectTool",
342- "title": "Async Object Tool",
343- "description": "Returns a city object",
344- "inputSchema": {
345- "type": "object",
346- "properties": {
347- "name": {
348- "type": "string",
349- "description": "City name to fetch"
350- }
351- },
352- "required": ["name"]
353- },
354- "outputSchema": {
355- "type": "object",
356- "properties": {
357- "country": { "type": "string" },
358- "isCapital": { "type": "boolean" },
359- "name": { "type": "string" },
360- "population": { "type": "integer" }
361- },
362- "required": ["country", "isCapital", "name", "population"]
339+ {
340+ "name": "asyncObjectTool",
341+ "title": "Async asyncObjectTool",
342+ "description": "A tool to return an object of cities asynchronously",
343+ "inputSchema": {
344+ "type": "object",
345+ "properties": {
346+ "name": {
347+ "type": "string",
348+ "description": "name of your city"
363349 }
364- }
365- """ ;
350+ },
351+ "required": ["name"]
352+ },
353+ "outputSchema": {
354+ "type": "object",
355+ "properties": {
356+ "country": {"type": "string"},
357+ "isCapital": {"type": "boolean"},
358+ "name": {"type": "string"},
359+ "population": {"type": "integer"}
360+ },
361+ "required": ["name", "country", "population", "isCapital"]
362+ }
363+ }
364+ """ ;
366365
367366 JSONAssert .assertEquals (expectedToolJson , actualToolJson , true );
368367 }
369368
370- // @SuppressWarnings("unchecked")
371- // private List<Map<String, Object>> fetchToolMetadata() throws Exception {
372- // String response = client.callMCP("""
373- // {
374- // "jsonrpc": "2.0",
375- // "id": 1,
376- // "method": "tools/list"
377- // }
378- // """);
379- //
380- // Jsonb jsonb = JsonbBuilder.create();
381- // Map<String, Object> parsed = jsonb.fromJson(response, new TypeLiteral<Map<String, Object>>() {}.getType());
382- // Map<String, Object> result = (Map<String, Object>) parsed.get("result");
383- // return (List<Map<String, Object>>) result.get("tools");
384- // }
369+ @ Test
370+ public void testAsyncToolsWithoutStructuredContentHaveNoOutputSchema () throws Exception {
371+ String response = client .callMCP ("""
372+ {
373+ "jsonrpc": "2.0",
374+ "id": 1,
375+ "method": "tools/list"
376+ }
377+ """ );
378+
379+ JSONObject root = (JSONObject ) JSONParser .parseJSON (response );
380+ JSONArray tools = root .getJSONObject ("result" ).getJSONArray ("tools" );
381+
382+ List <String > toolNames = List .of (
383+ "asyncEcho" ,
384+ "asyncDelayedEcho" ,
385+ "asyncToolThatNeverCompletes" ,
386+ "asyncCancellationTool" );
387+
388+ for (String toolName : toolNames ) {
389+ JSONObject tool = findToolByName (tools , toolName );
390+ assertNotNull ("Tool " + toolName + " should be present" , tool );
391+ assertFalse ("Tool " + toolName + " should NOT have an output schema" , tool .has ("outputSchema" ));
392+ }
393+ }
394+
395+ private JSONObject findToolByName (JSONArray tools , String name ) throws JSONException {
396+ for (int i = 0 ; i < tools .length (); i ++) {
397+ JSONObject tool = tools .getJSONObject (i );
398+ if (name .equals (tool .getString ("name" ))) {
399+ return tool ;
400+ }
401+ }
402+ return null ;
403+ }
404+
405+ @ Test
406+ public void testAsyncToolsWithBasicOutputTypesShouldNotHaveOutputSchema () throws Exception {
407+ String response = client .callMCP ("""
408+ {
409+ "jsonrpc": "2.0",
410+ "id": 1,
411+ "method": "tools/list"
412+ }
413+ """ );
414+
415+ JSONObject root = (JSONObject ) JSONParser .parseJSON (response );
416+ JSONArray tools = root .getJSONObject ("result" ).getJSONArray ("tools" );
417+
418+ // Tools that SHOULD NOT have an output schema
419+ List <String > toolsWithoutSchema = List .of (
420+ "asyncEcho" ,
421+ "asyncDelayedEcho" ,
422+ "asyncCancellationTool" ,
423+ "asyncToolThatNeverCompletes" );
424+
425+ for (int i = 0 ; i < tools .length (); i ++) {
426+ JSONObject tool = tools .getJSONObject (i );
427+ String name = tool .getString ("name" );
428+
429+ if (toolsWithoutSchema .contains (name )) {
430+ assertFalse (
431+ "Tool '" + name + "' should NOT have an output schema" ,
432+ tool .has ("outputSchema" ));
433+ }
434+ }
435+ }
385436
386437}
0 commit comments