diff --git a/mcp/common/src/main/java/org/springframework/ai/mcp/McpToolUtils.java b/mcp/common/src/main/java/org/springframework/ai/mcp/McpToolUtils.java index 2f8f366d076..6aadc18d83e 100644 --- a/mcp/common/src/main/java/org/springframework/ai/mcp/McpToolUtils.java +++ b/mcp/common/src/main/java/org/springframework/ai/mcp/McpToolUtils.java @@ -30,6 +30,7 @@ import io.modelcontextprotocol.server.McpSyncServerExchange; import io.modelcontextprotocol.spec.McpSchema; import io.modelcontextprotocol.spec.McpSchema.Role; +import org.springframework.ai.tool.definition.ToolDefinition; import reactor.core.publisher.Mono; import reactor.core.scheduler.Schedulers; @@ -167,8 +168,9 @@ public static McpServerFeatures.SyncToolSpecification toSyncToolSpecification(To public static McpServerFeatures.SyncToolSpecification toSyncToolSpecification(ToolCallback toolCallback, MimeType mimeType) { - var tool = new McpSchema.Tool(toolCallback.getToolDefinition().name(), - toolCallback.getToolDefinition().description(), toolCallback.getToolDefinition().inputSchema()); + ToolDefinition toolDefinition = toolCallback.getToolDefinition(); + var tool = new McpSchema.Tool(toolDefinition.name(), toolDefinition.title(), toolDefinition.description(), + ModelOptionsUtils.jsonToObject(toolDefinition.inputSchema(), McpSchema.JsonSchema.class), null); return new McpServerFeatures.SyncToolSpecification(tool, (exchange, request) -> { try { diff --git a/spring-ai-model/src/main/java/org/springframework/ai/tool/annotation/Tool.java b/spring-ai-model/src/main/java/org/springframework/ai/tool/annotation/Tool.java index d17b2fc4773..7cac322df07 100644 --- a/spring-ai-model/src/main/java/org/springframework/ai/tool/annotation/Tool.java +++ b/spring-ai-model/src/main/java/org/springframework/ai/tool/annotation/Tool.java @@ -41,6 +41,11 @@ */ String name() default ""; + /** + * The title of the tool. If not provided, the method name will be used. + */ + String title() default ""; + /** * The description of the tool. If not provided, the method name will be used. */ diff --git a/spring-ai-model/src/main/java/org/springframework/ai/tool/definition/DefaultToolDefinition.java b/spring-ai-model/src/main/java/org/springframework/ai/tool/definition/DefaultToolDefinition.java index cafd1a70364..b3e3801b43d 100644 --- a/spring-ai-model/src/main/java/org/springframework/ai/tool/definition/DefaultToolDefinition.java +++ b/spring-ai-model/src/main/java/org/springframework/ai/tool/definition/DefaultToolDefinition.java @@ -16,7 +16,7 @@ package org.springframework.ai.tool.definition; -import org.springframework.ai.util.ParsingUtils; +import org.springframework.ai.tool.support.ToolUtils; import org.springframework.util.Assert; import org.springframework.util.StringUtils; @@ -26,10 +26,12 @@ * @author Thomas Vitale * @since 1.0.0 */ -public record DefaultToolDefinition(String name, String description, String inputSchema) implements ToolDefinition { +public record DefaultToolDefinition(String name, String title, String description, + String inputSchema) implements ToolDefinition { public DefaultToolDefinition { Assert.hasText(name, "name cannot be null or empty"); + Assert.hasText(title, "title cannot be null or empty"); Assert.hasText(description, "description cannot be null or empty"); Assert.hasText(inputSchema, "inputSchema cannot be null or empty"); } @@ -42,6 +44,8 @@ public static final class Builder { private String name; + private String title; + private String description; private String inputSchema; @@ -54,6 +58,11 @@ public Builder name(String name) { return this; } + public Builder title(String title) { + this.title = title; + return this; + } + public Builder description(String description) { this.description = description; return this; @@ -65,11 +74,13 @@ public Builder inputSchema(String inputSchema) { } public ToolDefinition build() { + if (!StringUtils.hasText(this.title)) { + this.title = ToolUtils.getToolTitleFromName(this.name); + } if (!StringUtils.hasText(this.description)) { - Assert.hasText(this.name, "toolName cannot be null or empty"); - this.description = ParsingUtils.reConcatenateCamelCase(this.name, " "); + this.description = ToolUtils.getToolDescriptionFromName(this.name); } - return new DefaultToolDefinition(this.name, this.description, this.inputSchema); + return new DefaultToolDefinition(this.name, this.title, this.description, this.inputSchema); } } diff --git a/spring-ai-model/src/main/java/org/springframework/ai/tool/definition/ToolDefinition.java b/spring-ai-model/src/main/java/org/springframework/ai/tool/definition/ToolDefinition.java index 517a0061712..0ae4e4d8baa 100644 --- a/spring-ai-model/src/main/java/org/springframework/ai/tool/definition/ToolDefinition.java +++ b/spring-ai-model/src/main/java/org/springframework/ai/tool/definition/ToolDefinition.java @@ -29,6 +29,11 @@ public interface ToolDefinition { */ String name(); + /** + * The human-readable title for the tool. + */ + String title(); + /** * The tool description, used by the AI model to determine what the tool does. */ diff --git a/spring-ai-model/src/main/java/org/springframework/ai/tool/support/ToolDefinitions.java b/spring-ai-model/src/main/java/org/springframework/ai/tool/support/ToolDefinitions.java index 68d4646333a..c2b96e14cbd 100644 --- a/spring-ai-model/src/main/java/org/springframework/ai/tool/support/ToolDefinitions.java +++ b/spring-ai-model/src/main/java/org/springframework/ai/tool/support/ToolDefinitions.java @@ -48,6 +48,7 @@ public static DefaultToolDefinition.Builder builder(Method method) { Assert.notNull(method, "method cannot be null"); return DefaultToolDefinition.builder() .name(ToolUtils.getToolName(method)) + .title(ToolUtils.getToolTitle(method)) .description(ToolUtils.getToolDescription(method)) .inputSchema(JsonSchemaGenerator.generateForMethodInput(method)); } diff --git a/spring-ai-model/src/main/java/org/springframework/ai/tool/support/ToolUtils.java b/spring-ai-model/src/main/java/org/springframework/ai/tool/support/ToolUtils.java index 4186d935acc..4cf7da0b938 100644 --- a/spring-ai-model/src/main/java/org/springframework/ai/tool/support/ToolUtils.java +++ b/spring-ai-model/src/main/java/org/springframework/ai/tool/support/ToolUtils.java @@ -50,6 +50,20 @@ public static String getToolName(Method method) { return StringUtils.hasText(tool.name()) ? tool.name() : method.getName(); } + public static String getToolTitleFromName(String toolName) { + Assert.hasText(toolName, "toolName cannot be null or empty"); + return ParsingUtils.reConcatenateCamelCase(toolName, " "); + } + + public static String getToolTitle(Method method) { + Assert.notNull(method, "method cannot be null"); + var tool = AnnotatedElementUtils.findMergedAnnotation(method, Tool.class); + if (tool == null) { + return ParsingUtils.reConcatenateCamelCase(method.getName(), " "); + } + return StringUtils.hasText(tool.title()) ? tool.title() : method.getName(); + } + public static String getToolDescriptionFromName(String toolName) { Assert.hasText(toolName, "toolName cannot be null or empty"); return ParsingUtils.reConcatenateCamelCase(toolName, " "); diff --git a/spring-ai-model/src/test/java/org/springframework/ai/tool/execution/DefaultToolExecutionExceptionProcessorTests.java b/spring-ai-model/src/test/java/org/springframework/ai/tool/execution/DefaultToolExecutionExceptionProcessorTests.java index 955194dd2a3..7cbb11e2d48 100644 --- a/spring-ai-model/src/test/java/org/springframework/ai/tool/execution/DefaultToolExecutionExceptionProcessorTests.java +++ b/spring-ai-model/src/test/java/org/springframework/ai/tool/execution/DefaultToolExecutionExceptionProcessorTests.java @@ -33,8 +33,8 @@ class DefaultToolExecutionExceptionProcessorTests { private final IllegalStateException toolException = new IllegalStateException("Inner exception"); - private final DefaultToolDefinition toolDefinition = new DefaultToolDefinition("toolName", "toolDescription", - "inputSchema"); + private final DefaultToolDefinition toolDefinition = new DefaultToolDefinition("toolName", "toolTitle", + "toolDescription", "inputSchema"); private final ToolExecutionException toolExecutionException = new ToolExecutionException(toolDefinition, toolException);