Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
58 changes: 57 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -411,8 +411,8 @@ public class CalculatorToolProvider {

@McpTool(name = "calculate-area",
description = "Calculate the area of a rectangle",
title = "Rectangle Area Calculator", // Human-readable display name
annotations = @McpTool.McpAnnotations(
title = "Rectangle Area Calculator",
readOnlyHint = true,
destructiveHint = false,
idempotentHint = true
Expand Down Expand Up @@ -490,6 +490,62 @@ public class CalculatorToolProvider {
}
```

#### Tool Title Attribute

The `@McpTool` annotation supports a `title` attribute that provides a human-readable display name for tools. This is intended for UI and end-user contexts, optimized to be easily understood even by those unfamiliar with domain-specific terminology.

**Title Precedence Order:**
1. If the `title` attribute is explicitly set, it takes precedence
2. If not set but `annotations.title` exists, that value is used
3. If neither is provided, the tool's `name` is used as the title
4. If the `name` is not set the method name is used as the title

Example usage:

```java
// Using the title attribute directly
@McpTool(name = "calc-area",
description = "Calculate rectangle area",
title = "Rectangle Area Calculator") // Human-friendly display name
public double calculateArea(double width, double height) {
return width * height;
}

// Title attribute takes precedence over annotations.title
@McpTool(name = "data-processor",
description = "Process complex data",
title = "Data Processor", // This takes precedence
annotations = @McpTool.McpAnnotations(
title = "Complex Data Handler" // This is overridden
))
public String processData(String input) {
return process(input);
}

// Using annotations.title when title attribute is not set
@McpTool(name = "file-converter",
description = "Convert file formats",
annotations = @McpTool.McpAnnotations(
title = "File Format Converter" // This will be used as title
))
public String convertFile(String filePath) {
return convert(filePath);
}

// Falls back to name when no title is provided
@McpTool(name = "simple-tool",
description = "A simple tool")
public String simpleTool(String input) {
// Title will be "simple-tool"
return input;
}
```

The title is particularly useful for:
- Displaying tools in user interfaces with friendly names
- Providing clear, non-technical names for end users
- Maintaining backward compatibility (tools without titles continue to work)

#### CallToolRequest Support

The library supports special `CallToolRequest` parameters in tool methods, enabling dynamic schema handling at runtime. This is useful when you need to:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,14 @@
*/
boolean generateOutputSchema() default true;

/**
* Intended for UI and end-user contexts — optimized to be human-readable and easily
* understood, even by those unfamiliar with domain-specific terminology. If not
* provided, the name should be used for display (except for Tool, where
* annotations.title should be given precedence over using name, if present).
*/
String title() default "";

/**
* Additional properties describing a Tool to clients.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,12 +75,12 @@ public List<AsyncToolSpecification> getToolSpecifications() {
|| Publisher.class.isAssignableFrom(method.getReturnType()))
.map(mcpToolMethod -> {

var toolAnnotation = doGetMcpToolAnnotation(mcpToolMethod);
var toolJavaAnnotation = doGetMcpToolAnnotation(mcpToolMethod);

String toolName = Utils.hasText(toolAnnotation.name()) ? toolAnnotation.name()
String toolName = Utils.hasText(toolJavaAnnotation.name()) ? toolJavaAnnotation.name()
: mcpToolMethod.getName();

String toolDescrption = toolAnnotation.description();
String toolDescrption = toolJavaAnnotation.description();

String inputSchema = JsonSchemaGenerator.generateForMethodInput(mcpToolMethod);

Expand All @@ -89,20 +89,36 @@ public List<AsyncToolSpecification> getToolSpecifications() {
.description(toolDescrption)
.inputSchema(inputSchema);

var title = toolJavaAnnotation.title();

// Tool annotations
if (toolAnnotation.annotations() != null) {
var toolAnnotations = toolAnnotation.annotations();
if (toolJavaAnnotation.annotations() != null) {
var toolAnnotations = toolJavaAnnotation.annotations();
toolBuilder.annotations(new McpSchema.ToolAnnotations(toolAnnotations.title(),
toolAnnotations.readOnlyHint(), toolAnnotations.destructiveHint(),
toolAnnotations.idempotentHint(), toolAnnotations.openWorldHint(), null));

// If not provided, the name should be used for display (except
// for Tool, where annotations.title should be given precedence
// over using name, if present).
if (!Utils.hasText(title)) {
title = toolAnnotations.title();
}
}

// If not provided, the name should be used for display (except
// for Tool, where annotations.title should be given precedence
// over using name, if present).
if (!Utils.hasText(title)) {
title = toolName;
}
toolBuilder.title(title);

// Generate Output Schema from the method return type.
// Output schema is not generated for primitive types, void,
// CallToolResult, simple value types (String, etc.)
// or if generateOutputSchema attribute is set to false.

if (toolAnnotation.generateOutputSchema()
if (toolJavaAnnotation.generateOutputSchema()
&& !ReactiveUtils.isReactiveReturnTypeOfVoid(mcpToolMethod)
&& !ReactiveUtils.isReactiveReturnTypeOfCallToolResult(mcpToolMethod)) {

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -79,12 +79,12 @@ public List<AsyncToolSpecification> getToolSpecifications() {
|| Publisher.class.isAssignableFrom(method.getReturnType()))
.map(mcpToolMethod -> {

var toolAnnotation = doGetMcpToolAnnotation(mcpToolMethod);
var toolJavaAnnotation = doGetMcpToolAnnotation(mcpToolMethod);

String toolName = Utils.hasText(toolAnnotation.name()) ? toolAnnotation.name()
String toolName = Utils.hasText(toolJavaAnnotation.name()) ? toolJavaAnnotation.name()
: mcpToolMethod.getName();

String toolDescrption = toolAnnotation.description();
String toolDescrption = toolJavaAnnotation.description();

String inputSchema = JsonSchemaGenerator.generateForMethodInput(mcpToolMethod);

Expand All @@ -93,20 +93,37 @@ public List<AsyncToolSpecification> getToolSpecifications() {
.description(toolDescrption)
.inputSchema(inputSchema);

var title = toolJavaAnnotation.title();

// Tool annotations
if (toolAnnotation.annotations() != null) {
var toolAnnotations = toolAnnotation.annotations();
if (toolJavaAnnotation.annotations() != null) {
var toolAnnotations = toolJavaAnnotation.annotations();
toolBuilder.annotations(new McpSchema.ToolAnnotations(toolAnnotations.title(),
toolAnnotations.readOnlyHint(), toolAnnotations.destructiveHint(),
toolAnnotations.idempotentHint(), toolAnnotations.openWorldHint(), null));

// If not provided, the name should be used for display (except
// for Tool, where annotations.title should be given precedence
// over using name, if present).
if (!Utils.hasText(title)) {
title = toolAnnotations.title();
}
}

// If not provided, the name should be used for display (except
// for Tool, where annotations.title should be given precedence
// over using name, if present).
if (!Utils.hasText(title)) {
title = toolName;
}
toolBuilder.title(title);

// Generate Output Schema from the method return type.
// Output schema is not generated for primitive types, void,
// CallToolResult, simple value types (String, etc.)
// or if generateOutputSchema attribute is set to false.

if (toolAnnotation.generateOutputSchema()
if (toolJavaAnnotation.generateOutputSchema()
&& !ReactiveUtils.isReactiveReturnTypeOfVoid(mcpToolMethod)
&& !ReactiveUtils.isReactiveReturnTypeOfCallToolResult(mcpToolMethod)) {

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,22 +22,20 @@
import java.util.function.BiFunction;
import java.util.stream.Stream;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springaicommunity.mcp.annotation.McpTool;
import org.springaicommunity.mcp.method.tool.ReactiveUtils;
import org.springaicommunity.mcp.method.tool.ReturnMode;
import org.springaicommunity.mcp.method.tool.SyncMcpToolMethodCallback;
import org.springaicommunity.mcp.method.tool.utils.ClassUtils;
import org.springaicommunity.mcp.method.tool.utils.JsonSchemaGenerator;

import io.modelcontextprotocol.server.McpServerFeatures.SyncToolSpecification;
import io.modelcontextprotocol.server.McpSyncServerExchange;
import io.modelcontextprotocol.spec.McpSchema;
import io.modelcontextprotocol.spec.McpSchema.CallToolRequest;
import io.modelcontextprotocol.spec.McpSchema.CallToolResult;
import io.modelcontextprotocol.util.Assert;
import io.modelcontextprotocol.util.Utils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springaicommunity.mcp.annotation.McpTool;
import org.springaicommunity.mcp.method.tool.ReturnMode;
import org.springaicommunity.mcp.method.tool.SyncMcpToolMethodCallback;
import org.springaicommunity.mcp.method.tool.utils.ClassUtils;
import org.springaicommunity.mcp.method.tool.utils.JsonSchemaGenerator;
import reactor.core.publisher.Mono;

/**
Expand Down Expand Up @@ -72,12 +70,12 @@ public List<SyncToolSpecification> getToolSpecifications() {
.filter(method -> !Mono.class.isAssignableFrom(method.getReturnType()))
.map(mcpToolMethod -> {

McpTool toolAnnotation = doGetMcpToolAnnotation(mcpToolMethod);
McpTool toolJavaAnnotation = doGetMcpToolAnnotation(mcpToolMethod);

String toolName = Utils.hasText(toolAnnotation.name()) ? toolAnnotation.name()
String toolName = Utils.hasText(toolJavaAnnotation.name()) ? toolJavaAnnotation.name()
: mcpToolMethod.getName();

String toolDescription = toolAnnotation.description();
String toolDescription = toolJavaAnnotation.description();

// Check if method has CallToolRequest parameter
boolean hasCallToolRequestParam = Arrays.stream(mcpToolMethod.getParameterTypes())
Expand All @@ -100,13 +98,30 @@ public List<SyncToolSpecification> getToolSpecifications() {
.description(toolDescription)
.inputSchema(inputSchema);

var title = toolJavaAnnotation.title();

// Tool annotations
if (toolAnnotation.annotations() != null) {
var toolAnnotations = toolAnnotation.annotations();
if (toolJavaAnnotation.annotations() != null) {
var toolAnnotations = toolJavaAnnotation.annotations();
toolBuilder.annotations(new McpSchema.ToolAnnotations(toolAnnotations.title(),
toolAnnotations.readOnlyHint(), toolAnnotations.destructiveHint(),
toolAnnotations.idempotentHint(), toolAnnotations.openWorldHint(), null));

// If not provided, the name should be used for display (except
// for Tool, where annotations.title should be given precedence
// over using name, if present).
if (!Utils.hasText(title)) {
title = toolAnnotations.title();
}
}

// If not provided, the name should be used for display (except
// for Tool, where annotations.title should be given precedence
// over using name, if present).
if (!Utils.hasText(title)) {
title = toolName;
}
toolBuilder.title(title);

// ReactiveUtils.isReactiveReturnTypeOfCallToolResult(mcpToolMethod);

Expand All @@ -115,7 +130,7 @@ public List<SyncToolSpecification> getToolSpecifications() {
// CallToolResult, simple value types (String, etc.)
// or if generateOutputSchema attribute is set to false.
Class<?> methodReturnType = mcpToolMethod.getReturnType();
if (toolAnnotation.generateOutputSchema() && methodReturnType != null
if (toolJavaAnnotation.generateOutputSchema() && methodReturnType != null
&& methodReturnType != CallToolResult.class && methodReturnType != Void.class
&& methodReturnType != void.class && !ClassUtils.isPrimitiveOrWrapper(methodReturnType)
&& !ClassUtils.isSimpleValueType(methodReturnType)) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,12 +74,12 @@ public List<SyncToolSpecification> getToolSpecifications() {
.filter(method -> !Mono.class.isAssignableFrom(method.getReturnType()))
.map(mcpToolMethod -> {

var toolAnnotation = doGetMcpToolAnnotation(mcpToolMethod);
var toolJavaAnnotation = doGetMcpToolAnnotation(mcpToolMethod);

String toolName = Utils.hasText(toolAnnotation.name()) ? toolAnnotation.name()
String toolName = Utils.hasText(toolJavaAnnotation.name()) ? toolJavaAnnotation.name()
: mcpToolMethod.getName();

String toolDescrption = toolAnnotation.description();
String toolDescrption = toolJavaAnnotation.description();

String inputSchema = JsonSchemaGenerator.generateForMethodInput(mcpToolMethod);

Expand All @@ -88,21 +88,39 @@ public List<SyncToolSpecification> getToolSpecifications() {
.description(toolDescrption)
.inputSchema(inputSchema);

var title = toolJavaAnnotation.title();

// Tool annotations
if (toolAnnotation.annotations() != null) {
var toolAnnotations = toolAnnotation.annotations();
if (toolJavaAnnotation.annotations() != null) {
var toolAnnotations = toolJavaAnnotation.annotations();
toolBuilder.annotations(new McpSchema.ToolAnnotations(toolAnnotations.title(),
toolAnnotations.readOnlyHint(), toolAnnotations.destructiveHint(),
toolAnnotations.idempotentHint(), toolAnnotations.openWorldHint(), null));

// If not provided, the name should be used for display (except
// for Tool, where annotations.title should be given precedence
// over using name, if present).
if (!Utils.hasText(title)) {
title = toolAnnotations.title();
}
}

ReactiveUtils.isReactiveReturnTypeOfCallToolResult(mcpToolMethod);
// If not provided, the name should be used for display (except
// for Tool, where annotations.title should be given precedence
// over using name, if present).
if (!Utils.hasText(title)) {
title = toolName;
}
toolBuilder.title(title);

// ReactiveUtils.isReactiveReturnTypeOfCallToolResult(mcpToolMethod);

// Generate Output Schema from the method return type.
// Output schema is not generated for primitive types, void,
// CallToolResult, simple value types (String, etc.)
// or if generateOutputSchema attribute is set to false.
Class<?> methodReturnType = mcpToolMethod.getReturnType();
if (toolAnnotation.generateOutputSchema() && methodReturnType != null
if (toolJavaAnnotation.generateOutputSchema() && methodReturnType != null
&& methodReturnType != CallToolResult.class && methodReturnType != Void.class
&& methodReturnType != void.class && !ClassUtils.isPrimitiveOrWrapper(methodReturnType)
&& !ClassUtils.isSimpleValueType(methodReturnType)) {
Expand Down
Loading