Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ public class ToolSchema {
private final String name;
private final String description;
private final Map<String, Object> parameters;
private final Map<String, Object> outputSchema;
private final Boolean strict;

/**
Expand All @@ -43,6 +44,10 @@ private ToolSchema(Builder builder) {
builder.parameters != null
? Collections.unmodifiableMap(new HashMap<>(builder.parameters))
: Collections.emptyMap();
this.outputSchema =
builder.outputSchema != null
? Collections.unmodifiableMap(new HashMap<>(builder.outputSchema))
: null;
this.strict = builder.strict;
}

Expand Down Expand Up @@ -73,6 +78,15 @@ public Map<String, Object> getParameters() {
return parameters;
}

/**
* Gets the optional tool output schema as a JSON Schema.
*
* @return an unmodifiable map containing the output schema, or null if unspecified
*/
public Map<String, Object> getOutputSchema() {
return outputSchema;
}

/**
* Gets the strict mode flag for schema validation.
*
Expand All @@ -98,6 +112,7 @@ public static class Builder {
private String name;
private String description;
private Map<String, Object> parameters;
private Map<String, Object> outputSchema;
private Boolean strict;

/**
Expand Down Expand Up @@ -133,6 +148,17 @@ public Builder parameters(Map<String, Object> parameters) {
return this;
}

/**
* Sets the optional tool output schema as a JSON Schema.
*
* @param outputSchema the output schema
* @return this builder instance
*/
public Builder outputSchema(Map<String, Object> outputSchema) {
this.outputSchema = outputSchema;
return this;
}

/**
* Sets the strict mode for schema validation.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,19 @@ public interface AgentTool {
*/
Map<String, Object> getParameters();

/**
* Gets the optional output schema for this tool in JSON Schema format.
*
* <p>Most tools do not expose a structured output schema to models, so the default
* implementation returns {@code null}. MCP tools can override this to surface the
* server-provided {@code outputSchema} definition.
*
* @return Map representing the JSON Schema for tool outputs, or null if unsupported
*/
default Map<String, Object> getOutputSchema() {
return null;
}

/**
* Execute the tool with the given parameters (asynchronous).
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -178,7 +178,12 @@ Mono<Void> registerMcpClient(
toolPresetParams != null
? toolPresetParams.keySet()
: Collections.emptySet()),
mcpClientWrapper);
mcpTool.outputSchema() != null
? new ConcurrentHashMap<>(
mcpTool.outputSchema())
: null,
mcpClientWrapper,
toolPresetParams);
Comment thread
LearningGp marked this conversation as resolved.
Outdated

// Register with group, MCP client name, and preset parameters via
// callback
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ List<ToolSchema> getToolSchemas() {
.name(toolName)
.description(tool.getDescription())
.parameters(registered.getExtendedParameters())
.outputSchema(tool.getOutputSchema())
.build();
schemas.add(schema);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ public class McpTool implements AgentTool {
private final String name;
private final String description;
private final Map<String, Object> parameters;
private final Map<String, Object> outputSchema;
private final McpClientWrapper clientWrapper;
private final Map<String, Object> presetArguments;

Expand All @@ -77,7 +78,7 @@ public McpTool(
String description,
Map<String, Object> parameters,
McpClientWrapper clientWrapper) {
this(name, description, parameters, clientWrapper, null);
this(name, description, parameters, null, clientWrapper, null);
}

/**
Expand All @@ -95,9 +96,30 @@ public McpTool(
Map<String, Object> parameters,
McpClientWrapper clientWrapper,
Map<String, Object> presetArguments) {
this(name, description, parameters, null, clientWrapper, presetArguments);
}

/**
* Constructs a new McpTool with an optional output schema and preset arguments.
*
* @param name the tool name
* @param description the tool description
* @param parameters the JSON schema for tool parameters
* @param outputSchema the JSON schema for tool outputs (can be null)
* @param clientWrapper the MCP client wrapper
* @param presetArguments preset arguments to merge with each call (can be null)
*/
public McpTool(
String name,
String description,
Map<String, Object> parameters,
Map<String, Object> outputSchema,
McpClientWrapper clientWrapper,
Map<String, Object> presetArguments) {
this.name = name;
this.description = description;
this.parameters = parameters;
this.outputSchema = outputSchema != null ? new HashMap<>(outputSchema) : null;
this.clientWrapper = clientWrapper;
this.presetArguments = presetArguments != null ? new HashMap<>(presetArguments) : null;
}
Expand Down Expand Up @@ -132,6 +154,11 @@ public Map<String, Object> getParameters() {
return parameters;
}

@Override
public Map<String, Object> getOutputSchema() {
return outputSchema != null ? new HashMap<>(outputSchema) : null;
}

/**
* Executes this MCP tool asynchronously with the given parameters.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -342,4 +342,71 @@ void testRegisterMcpClient_WithNullPresetParametersForTool() {
verify(clientWrapper).listTools();
assertTrue(callbackCalled[0]);
}

@Test
void testRegisterMcpClient_PreservesOutputSchemaInRegisteredTool() {
ToolRegistry toolRegistry = mock(ToolRegistry.class);
ToolGroupManager groupManager = mock(ToolGroupManager.class);
McpClientWrapper clientWrapper = mock(McpClientWrapper.class);

AgentTool[] registeredTool = new AgentTool[1];
String[] registeredGroupName = new String[1];
String[] registeredClientName = new String[1];
Map<String, Object>[] registeredPresetParams = new Map[1];

McpClientManager manager =
new McpClientManager(
toolRegistry,
groupManager,
(tool, groupName, mcpClientName, presetParams) -> {
registeredTool[0] = tool;
registeredGroupName[0] = groupName;
registeredClientName[0] = mcpClientName;
registeredPresetParams[0] = presetParams;
});

when(clientWrapper.getName()).thenReturn("test-client");
when(clientWrapper.initialize()).thenReturn(Mono.empty());

McpSchema.Tool mockMcpTool = mock(McpSchema.Tool.class);
when(mockMcpTool.name()).thenReturn("structured-tool");
when(mockMcpTool.description()).thenReturn("Tool with output schema");
when(mockMcpTool.inputSchema())
.thenReturn(
new McpSchema.JsonSchema(
"object",
Map.of("query", Map.of("type", "string")),
List.of("query"),
null,
null,
null));

Map<String, Object> outputSchema =
Map.of(
"type",
"object",
"properties",
Map.of(
"result",
Map.of("type", "string"),
"confidence",
Map.of("type", "number")));
when(mockMcpTool.outputSchema()).thenReturn(outputSchema);
when(clientWrapper.listTools()).thenReturn(Mono.just(List.of(mockMcpTool)));

Map<String, Object> toolPresetParams = Map.of("temperature", 0.2);
Map<String, Map<String, Object>> presetParamsMapping =
Map.of("structured-tool", toolPresetParams);

manager.registerMcpClient(clientWrapper, null, null, "mcp-group", presetParamsMapping)
.block();

verify(clientWrapper).initialize();
verify(clientWrapper).listTools();
assertNotNull(registeredTool[0]);
assertEquals(outputSchema, registeredTool[0].getOutputSchema());
assertEquals("mcp-group", registeredGroupName[0]);
assertEquals("test-client", registeredClientName[0]);
assertEquals(toolPresetParams, registeredPresetParams[0]);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,50 @@ void testGetToolSchemasForModelWithUngroupedTool() {
assertEquals("test_tool", schema.getName());
assertEquals("Test tool", schema.getDescription());
assertNotNull(schema.getParameters());
assertEquals(null, schema.getOutputSchema());
}

@Test
void testGetToolSchemasIncludesOutputSchemaWhenProvided() {
AgentTool tool =
new AgentTool() {
@Override
public String getName() {
return "schema_tool";
}

@Override
public String getDescription() {
return "Schema-aware tool";
}

@Override
public Map<String, Object> getParameters() {
return Map.of("type", "object", "properties", Map.of());
}

@Override
public Map<String, Object> getOutputSchema() {
return Map.of(
"type",
"object",
"properties",
Map.of("result", Map.of("type", "string")));
}

@Override
public Mono<ToolResultBlock> callAsync(ToolCallParam input) {
return Mono.just(ToolResultBlock.text("ok"));
}
};
RegisteredToolFunction registered = new RegisteredToolFunction(tool, null, null);
registry.registerTool("schema_tool", tool, registered);

List<ToolSchema> schemas = schemaProvider.getToolSchemas();

assertEquals(1, schemas.size());
assertNotNull(schemas.get(0).getOutputSchema());
assertEquals("object", schemas.get(0).getOutputSchema().get("type"));
}

@Test
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
import io.agentscope.core.tool.test.SampleTools;
import io.agentscope.core.tool.test.ToolTestUtils;
import io.agentscope.core.util.JsonUtils;
import io.modelcontextprotocol.spec.McpSchema;
import java.lang.reflect.Type;
import java.util.List;
import java.util.Map;
Expand Down Expand Up @@ -1223,4 +1224,43 @@ void testParseConverterFromAnnotation_NullAnnotation() {
AgentTool tool = toolkit.getTool("tool_with_default_converter");
assertNotNull(tool, "Tool should be registered");
}

@Test
@DisplayName("Should expose MCP output schema through getToolSchemas")
void testGetToolSchemasIncludesMcpOutputSchema() {
McpClientWrapper mcpClientWrapper = mock(McpClientWrapper.class);
when(mcpClientWrapper.getName()).thenReturn("mcp-client");
when(mcpClientWrapper.initialize()).thenReturn(Mono.empty());

McpSchema.Tool mcpTool = mock(McpSchema.Tool.class);
when(mcpTool.name()).thenReturn("structured_mcp_tool");
when(mcpTool.description()).thenReturn("Returns structured MCP output");
when(mcpTool.inputSchema())
.thenReturn(
new McpSchema.JsonSchema("object", Map.of(), List.of(), null, null, null));
when(mcpTool.outputSchema())
.thenReturn(
Map.of(
"type",
"object",
"properties",
Map.of("answer", Map.of("type", "string"))));
when(mcpClientWrapper.listTools()).thenReturn(Mono.just(List.of(mcpTool)));

toolkit.registerMcpClient(mcpClientWrapper).block();

ToolSchema schema =
toolkit.getToolSchemas().stream()
.filter(s -> "structured_mcp_tool".equals(getToolName(s)))
.findFirst()
.orElse(null);

assertNotNull(schema);
assertNotNull(schema.getOutputSchema());
assertEquals("object", schema.getOutputSchema().get("type"));
@SuppressWarnings("unchecked")
Map<String, Object> properties =
(Map<String, Object>) schema.getOutputSchema().get("properties");
assertTrue(properties.containsKey("answer"));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -61,10 +61,31 @@ void testConstructor_WithoutPresetArgs() {
assertEquals("test-tool", tool.getName());
assertEquals("A test tool", tool.getDescription());
assertEquals(parameters, tool.getParameters());
assertNull(tool.getOutputSchema());
assertEquals("test-client", tool.getClientName());
assertNull(tool.getPresetArguments());
}

@Test
void testConstructor_WithOutputSchema() {
Map<String, Object> outputSchema = new HashMap<>();
outputSchema.put("type", "object");
outputSchema.put("properties", Map.of("answer", Map.of("type", "string")));

McpTool tool =
new McpTool(
"test-tool",
"A test tool",
parameters,
outputSchema,
mockClientWrapper,
null);

assertNotNull(tool.getOutputSchema());
assertEquals("object", tool.getOutputSchema().get("type"));
assertTrue(tool.getOutputSchema().containsKey("properties"));
}

@Test
void testConstructor_WithPresetArgs() {
Map<String, Object> presetArgs = new HashMap<>();
Expand Down
Loading