Skip to content
Open
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
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
Expand All @@ -30,6 +30,12 @@
@Documented
public @interface McpToolParam {

/**
* The external name of the tool argument exposed in the MCP input schema.
* When empty, the Java parameter or field name is used.
*/
String name() default "";

/**
* Whether the tool argument is required.
*/
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
/*
* Copyright 2023-present the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package org.springframework.ai.mcp.annotation;

import java.lang.reflect.Parameter;

import com.github.victools.jsonschema.generator.MemberScope;
import org.jspecify.annotations.Nullable;

import org.springframework.util.StringUtils;

/**
* Utilities for resolving external MCP tool argument names from {@link McpToolParam}.
*/
public abstract class McpToolParamUtils {

private McpToolParamUtils() {
}

public static String resolveExternalName(Parameter parameter) {
McpToolParam annotation = parameter.getAnnotation(McpToolParam.class);
if (annotation != null && StringUtils.hasText(annotation.name())) {
return annotation.name();
}
return parameter.getName();
}

public static @Nullable String resolveExternalPropertyName(MemberScope<?, ?> member) {
McpToolParam annotation = member.getAnnotationConsideringFieldAndGetter(McpToolParam.class);
if (annotation != null && StringUtils.hasText(annotation.name())) {
return annotation.name();
}
return null;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
import org.springframework.ai.mcp.annotation.McpMeta;
import org.springframework.ai.mcp.annotation.McpProgressToken;
import org.springframework.ai.mcp.annotation.McpTool;
import org.springframework.ai.mcp.annotation.McpToolParamUtils;
import org.springframework.ai.mcp.annotation.context.McpAsyncRequestContext;
import org.springframework.ai.mcp.annotation.context.McpRequestContextTypes;
import org.springframework.ai.mcp.annotation.context.McpSyncRequestContext;
Expand Down Expand Up @@ -131,7 +132,7 @@ protected Object[] buildMethodArguments(T exchangeOrContext, Map<String, Object>
return exchangeOrContext;
}

Object rawArgument = toolInputArguments.get(parameter.getName());
Object rawArgument = toolInputArguments.get(McpToolParamUtils.resolveExternalName(parameter));
return buildTypedArgument(rawArgument, parameter.getParameterizedType());
}).toArray();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@
import org.springframework.ai.mcp.annotation.McpMeta;
import org.springframework.ai.mcp.annotation.McpProgressToken;
import org.springframework.ai.mcp.annotation.McpToolParam;
import org.springframework.ai.mcp.annotation.McpToolParamUtils;
import org.springframework.ai.mcp.annotation.context.McpAsyncRequestContext;
import org.springframework.ai.mcp.annotation.context.McpSyncRequestContext;
import org.springframework.ai.model.KotlinModule;
Expand Down Expand Up @@ -153,7 +154,7 @@ private static String internalGenerateFromMethodArguments(Method method) {

for (int i = 0; i < method.getParameterCount(); i++) {
Parameter parameter = method.getParameters()[i];
String parameterName = parameter.getName();
String parameterName = McpToolParamUtils.resolveExternalName(parameter);
Type parameterType = method.getGenericParameterTypes()[i];

// Skip parameters annotated with @McpProgressToken
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
Expand All @@ -17,15 +17,17 @@
package org.springframework.ai.mcp.annotation.method.tool.utils;

import com.github.victools.jsonschema.generator.MemberScope;
import com.github.victools.jsonschema.generator.SchemaGeneratorConfigBuilder;
import org.jspecify.annotations.Nullable;

import org.springframework.ai.mcp.annotation.McpToolParam;
import org.springframework.ai.mcp.annotation.McpToolParamUtils;
import org.springframework.ai.util.json.schema.AbstractSpringAiSchemaModule;
import org.springframework.util.StringUtils;

/**
* JSON Schema Generator Module for Spring AI MCP.
* <p>
*
* This module provides a set of customizations to the JSON Schema generator to support
* the Spring AI MCP framework. It allows extracting descriptions from
* {@code @McpToolParam(description = ...)} annotations and to determine whether a
Expand All @@ -42,7 +44,13 @@ public McpSpringAiSchemaModule(Option... options) {
}

@Override
protected @Nullable String resolveToolParamDescription(MemberScope<?, ?> member) {
public void applyToConfigBuilder(SchemaGeneratorConfigBuilder builder) {
super.applyToConfigBuilder(builder);
builder.forFields().withPropertyNameOverrideResolver(McpToolParamUtils::resolveExternalPropertyName);
}

@Override
protected @Nullable String resolveToolParamDescription(MemberScope member) {
var annotation = member.getAnnotationConsideringFieldAndGetter(McpToolParam.class);
if (annotation != null && StringUtils.hasText(annotation.description())) {
return annotation.description();
Expand All @@ -51,7 +59,7 @@ public McpSpringAiSchemaModule(Option... options) {
}

@Override
protected @Nullable Boolean resolveToolParamRequired(MemberScope<?, ?> member) {
protected @Nullable Boolean resolveToolParamRequired(MemberScope member) {
var annotation = member.getAnnotationConsideringFieldAndGetter(McpToolParam.class);
return annotation != null ? annotation.required() : null;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,21 @@
*/
public class SyncMcpToolMethodCallbackTests {

@Test
public void testToolWithCustomParameterName() throws Exception {
TestToolProvider provider = new TestToolProvider();
Method method = TestToolProvider.class.getMethod("getWeather", String.class);
SyncMcpToolMethodCallback callback = new SyncMcpToolMethodCallback(ReturnMode.TEXT, method, provider);

McpSyncServerExchange exchange = mock(McpSyncServerExchange.class);
CallToolRequest request = new CallToolRequest("get-weather", Map.of("city_name", "Paris"));

CallToolResult result = callback.apply(exchange, request);

assertThat(result.isError()).isFalse();
assertThat(((TextContent) result.content().get(0)).text()).isEqualTo("Paris");
}

@Test
public void testSimpleToolCallback() throws Exception {
TestToolProvider provider = new TestToolProvider();
Expand Down Expand Up @@ -598,6 +613,11 @@ public String processObject(TestObject obj) {
return "Object: " + obj.name + " - " + obj.value;
}

@McpTool(name = "get-weather", description = "Get weather for a city")
public String getWeather(@McpToolParam(name = "city_name") String cityName) {
return cityName;
}

@McpTool(name = "optional-params-tool", description = "Tool with optional parameters")
public String toolWithOptionalParams(@McpToolParam(required = true) String required,
@McpToolParam(required = false) String optional) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@
import java.util.List;

import org.junit.jupiter.api.Test;

import org.springframework.ai.mcp.annotation.McpToolParam;
import tools.jackson.databind.JsonNode;

import org.springframework.ai.util.JsonHelper;
Expand Down Expand Up @@ -151,6 +153,26 @@ void generateFromTypeProducesValidObjectSchema() {
assertThat(schemaNode.has("properties")).isTrue();
}


@Test
void generateSchemaUsesCustomMcpToolParamName() throws Exception {
Method method = CustomNameMethods.class.getDeclaredMethod("getWeather", String.class);

String schema = McpJsonSchemaGenerator.generateForMethodInput(method);
JsonNode schemaNode = jsonHelper.fromJson(schema, JsonNode.class);

assertThat(schemaNode.at("/properties/city_name").has("type")).isTrue();
assertThat(schemaNode.at("/properties/cityName").isMissingNode()).isTrue();
assertThat(schemaNode.get("required").get(0).asString()).isEqualTo("city_name");
}

static class CustomNameMethods {

public void getWeather(@McpToolParam(name = "city_name") String cityName) {
}

}

static class TestMethods {

public void searchBooksMethod(SearchRequest request) {
Expand Down