Skip to content

Commit 833c169

Browse files
committed
feat(spring-ai-model): Add title attribute to MCP @tool annotation
- Add title field to DefaultToolDefinition and ToolDefinition interfaces - Modify ToolDefinitions and ToolUtils to support new title field - Adjust McpToolUtils to use title in tool specification
1 parent aa590e8 commit 833c169

File tree

7 files changed

+47
-9
lines changed

7 files changed

+47
-9
lines changed

mcp/common/src/main/java/org/springframework/ai/mcp/McpToolUtils.java

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
import io.modelcontextprotocol.server.McpSyncServerExchange;
3131
import io.modelcontextprotocol.spec.McpSchema;
3232
import io.modelcontextprotocol.spec.McpSchema.Role;
33+
import org.springframework.ai.tool.definition.ToolDefinition;
3334
import reactor.core.publisher.Mono;
3435
import reactor.core.scheduler.Schedulers;
3536

@@ -167,8 +168,9 @@ public static McpServerFeatures.SyncToolSpecification toSyncToolSpecification(To
167168
public static McpServerFeatures.SyncToolSpecification toSyncToolSpecification(ToolCallback toolCallback,
168169
MimeType mimeType) {
169170

170-
var tool = new McpSchema.Tool(toolCallback.getToolDefinition().name(),
171-
toolCallback.getToolDefinition().description(), toolCallback.getToolDefinition().inputSchema());
171+
ToolDefinition toolDefinition = toolCallback.getToolDefinition();
172+
var tool = new McpSchema.Tool(toolDefinition.name(), toolDefinition.title(), toolDefinition.description(),
173+
ModelOptionsUtils.jsonToObject(toolDefinition.inputSchema(), McpSchema.JsonSchema.class), null);
172174

173175
return new McpServerFeatures.SyncToolSpecification(tool, (exchange, request) -> {
174176
try {

spring-ai-model/src/main/java/org/springframework/ai/tool/annotation/Tool.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,11 @@
4141
*/
4242
String name() default "";
4343

44+
/**
45+
* The title of the tool. If not provided, the method name will be used.
46+
*/
47+
String title() default "";
48+
4449
/**
4550
* The description of the tool. If not provided, the method name will be used.
4651
*/

spring-ai-model/src/main/java/org/springframework/ai/tool/definition/DefaultToolDefinition.java

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616

1717
package org.springframework.ai.tool.definition;
1818

19-
import org.springframework.ai.util.ParsingUtils;
19+
import org.springframework.ai.tool.support.ToolUtils;
2020
import org.springframework.util.Assert;
2121
import org.springframework.util.StringUtils;
2222

@@ -26,10 +26,12 @@
2626
* @author Thomas Vitale
2727
* @since 1.0.0
2828
*/
29-
public record DefaultToolDefinition(String name, String description, String inputSchema) implements ToolDefinition {
29+
public record DefaultToolDefinition(String name, String title, String description,
30+
String inputSchema) implements ToolDefinition {
3031

3132
public DefaultToolDefinition {
3233
Assert.hasText(name, "name cannot be null or empty");
34+
Assert.hasText(title, "title cannot be null or empty");
3335
Assert.hasText(description, "description cannot be null or empty");
3436
Assert.hasText(inputSchema, "inputSchema cannot be null or empty");
3537
}
@@ -42,6 +44,8 @@ public static final class Builder {
4244

4345
private String name;
4446

47+
private String title;
48+
4549
private String description;
4650

4751
private String inputSchema;
@@ -54,6 +58,11 @@ public Builder name(String name) {
5458
return this;
5559
}
5660

61+
public Builder title(String title) {
62+
this.title = title;
63+
return this;
64+
}
65+
5766
public Builder description(String description) {
5867
this.description = description;
5968
return this;
@@ -65,11 +74,13 @@ public Builder inputSchema(String inputSchema) {
6574
}
6675

6776
public ToolDefinition build() {
77+
if (!StringUtils.hasText(this.title)) {
78+
this.title = ToolUtils.getToolTitleFromName(this.name);
79+
}
6880
if (!StringUtils.hasText(this.description)) {
69-
Assert.hasText(this.name, "toolName cannot be null or empty");
70-
this.description = ParsingUtils.reConcatenateCamelCase(this.name, " ");
81+
this.description = ToolUtils.getToolDescriptionFromName(this.name);
7182
}
72-
return new DefaultToolDefinition(this.name, this.description, this.inputSchema);
83+
return new DefaultToolDefinition(this.name, this.title, this.description, this.inputSchema);
7384
}
7485

7586
}

spring-ai-model/src/main/java/org/springframework/ai/tool/definition/ToolDefinition.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,11 @@ public interface ToolDefinition {
2929
*/
3030
String name();
3131

32+
/**
33+
* The human-readable title for the tool.
34+
*/
35+
String title();
36+
3237
/**
3338
* The tool description, used by the AI model to determine what the tool does.
3439
*/

spring-ai-model/src/main/java/org/springframework/ai/tool/support/ToolDefinitions.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ public static DefaultToolDefinition.Builder builder(Method method) {
4848
Assert.notNull(method, "method cannot be null");
4949
return DefaultToolDefinition.builder()
5050
.name(ToolUtils.getToolName(method))
51+
.title(ToolUtils.getToolTitle(method))
5152
.description(ToolUtils.getToolDescription(method))
5253
.inputSchema(JsonSchemaGenerator.generateForMethodInput(method));
5354
}

spring-ai-model/src/main/java/org/springframework/ai/tool/support/ToolUtils.java

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,20 @@ public static String getToolName(Method method) {
5050
return StringUtils.hasText(tool.name()) ? tool.name() : method.getName();
5151
}
5252

53+
public static String getToolTitleFromName(String toolName) {
54+
Assert.hasText(toolName, "toolName cannot be null or empty");
55+
return ParsingUtils.reConcatenateCamelCase(toolName, " ");
56+
}
57+
58+
public static String getToolTitle(Method method) {
59+
Assert.notNull(method, "method cannot be null");
60+
var tool = AnnotatedElementUtils.findMergedAnnotation(method, Tool.class);
61+
if (tool == null) {
62+
return ParsingUtils.reConcatenateCamelCase(method.getName(), " ");
63+
}
64+
return StringUtils.hasText(tool.title()) ? tool.title() : method.getName();
65+
}
66+
5367
public static String getToolDescriptionFromName(String toolName) {
5468
Assert.hasText(toolName, "toolName cannot be null or empty");
5569
return ParsingUtils.reConcatenateCamelCase(toolName, " ");

spring-ai-model/src/test/java/org/springframework/ai/tool/execution/DefaultToolExecutionExceptionProcessorTests.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,8 @@ class DefaultToolExecutionExceptionProcessorTests {
3333

3434
private final IllegalStateException toolException = new IllegalStateException("Inner exception");
3535

36-
private final DefaultToolDefinition toolDefinition = new DefaultToolDefinition("toolName", "toolDescription",
37-
"inputSchema");
36+
private final DefaultToolDefinition toolDefinition = new DefaultToolDefinition("toolName", "toolTitle",
37+
"toolDescription", "inputSchema");
3838

3939
private final ToolExecutionException toolExecutionException = new ToolExecutionException(toolDefinition,
4040
toolException);

0 commit comments

Comments
 (0)