Bug description
When implementing an MCP server using Spring AI 1.1.0-M1 and annotating beans with @McpTool
, the tools are not registered at server startup, even though the property spring.ai.mcp.server.annotation-scanner.enabled
is explicitly set to true
. The documentation (https://docs.spring.io/spring-ai/reference/1.1/api/mcp/mcp-server-boot-starter-docs.html) suggests that this should work without manual registration.
As a workaround, tools are registered if I annotate beans with @Tool
and create a bean of type ToolCallbackProvider
that manually passes the beans (e.g., via MethodToolCallbackProvider.builder().toolObjects(productService, timeService).build()
). This workaround was standard in 1.0.x, but the documentation for 1.1.x indicates that @McpTool
with annotation scanning should suffice.
This points to a problem either in the property spring.ai.mcp.server.annotation-scanner.enabled
or the annotation scanning functionality for MCP tools in 1.1.x.
Environment
- Spring AI version: 1.1.0-M1
- Java version: 21
- MCP server transport: HTTP Streamable
- Property set: spring.ai.mcp.server.annotation-scanner.enabled=true
- Beans annotated with @McpTool
Steps to reproduce
1. Create a Spring Boot MCP server with Spring AI 1.1.0-M1.
2. Annotate beans with @McpTool
and define tool methods.
3. Set spring.ai.mcp.server.annotation-scanner.enabled=true
.
4. Start the server.
5. Observe that tools are not registered.
6. Switch to @Tool
annotation and manually register beans via a ToolCallbackProvider
bean; tools are registered as expected.
Expected behavior
Beans annotated with @McpTool
should be automatically registered as tools when the annotation scanner is enabled, per documentation, without requiring manual registration.
Minimal Complete Reproducible example
@Bean
ToolCallbackProvider productTools(ProductService productService, TimeService timeService) {
return MethodToolCallbackProvider.builder()
.toolObjects(productService, timeService)
.build();
}
But this should not be necessary when using @McpTool
and the annotation scanner property.
Please advise if there is a known issue or if a fix is planned for future releases.
Comment From: ilayaperumalg
@oburgosm Could you try the following and see if it works:
- Use
@McpTool
annotation for your tool - Register your ToolCallbackProvider to the client via .toolcallbacks(ToolCallbackProvider)
Comment From: oburgosm
Thank you for your response. I want to clarify that I do not have a client implementation using Spring AI. To test the MCP server, I am using MCPInspector.
The behavior I observe:
- When I annotate beans with @McpTool
, MCPInspector does not list any tools when exploring the server.
- However, if I use @Tool
and register the beans manually via a ToolCallbackProvider
, MCPInspector does list the tools as expected.
So, this issue not only affects the server's internal registration, but also the visibility of tools from external clients like MCPInspector.
Is there any additional configuration I should apply so MCPInspector can detect tools annotated with @McpTool
? Or does this confirm the problem is with the automatic registration of tools when using the new annotation?
Let me know if you need a code snippet or example of how I am using MCPInspector.
Comment From: tzolov
@oburgosm can you share please which mcp server boot starter are you using?
Comment From: oburgosm
This issue occurs with both spring-ai-starter-mcp-server-webmvc
and spring-ai-starter-mcp-server-webflux
.
Comment From: tzolov
Thanks @oburgosm and what is the inspector URL you've been using?
Comment From: tzolov
I've successfully tested the https://github.com/tzolov/spring-ai-mcp-blogpost/tree/main/mcp-weather-server with the MCP Inspector yesterday. Can spot what is the difference with your server?
Comment From: tzolov
@oburgosm can you share the signature of the tool method. Could it be that the return type uses Generics?
Comment From: oburgosm
Thanks for your follow-up.
- MCPInspector connects to
http://localhost:8081/my-app/mcp
. I have also tested with port8080
and context-path/
, with the same result. MCPInspector connects successfully in all cases, so I don't think the port or context-path is the issue. - Some methods annotated with
@McpTool
use generics. - I tested with the referenced sample project tzolov/spring-ai-mcp-blogpost, specifically the
mcp-weather-server
module. With the default configuration, the tool list is shown. However, settingspring.ai.mcp.server.type
toASYNC
results in an empty tool list, with both webmvc and webflux starters. - Additionally, if I change the type from ASYNC to SYNC in my application, MCPInspector returns errors like the following:
{
"error": "[\n {\n \"received\": \"array\",\n \"code\": \"invalid_literal\",\n \"expected\": \"object\",\n \"path\": [\n \"tools\",\n 7,\n \"outputSchema\",\n \"type\"\n ],\n \"message\": \"Invalid literal value, expected \\\"object\\\"\"\n },\n {\n \"received\": \"array\",\n \"code\": \"invalid_literal\",\n \"expected\": \"object\",\n \"path\": [\n \"tools\",\n 8,\n \"outputSchema\",\n \"type\"\n ],\n \"message\": \"Invalid literal value, expected \\\"object\\\"\"\n },\n {\n \"received\": \"array\",\n \"code\": \"invalid_literal\",\n \"expected\": \"object\",\n \"path\": [\n \"tools\",\n 9,\n \"outputSchema\",\n \"type\"\n ],\n \"message\": \"Invalid literal value, expected \\\"object\\\"\"\n },\n {\n \"received\": \"array\",\n \"code\": \"invalid_literal\",\n \"expected\": \"object\",\n \"path\": [\n \"tools\",\n 10,\n \"outputSchema\",\n \"type\"\n ],\n \"message\": \"Invalid literal value, expected \\\"object\\\"\"\n },\n {\n \"received\": \"array\",\n \"code\": \"invalid_literal\",\n \"expected\": \"object\",\n \"path\": [\n \"tools\",\n 11,\n \"outputSchema\",\n \"type\"\n ],\n \"message\": \"Invalid literal value, expected \\\"object\\\"\"\n },\n {\n \"received\": \"array\",\n \"code\": \"invalid_literal\",\n \"expected\": \"object\",\n \"path\": [\n \"tools\",\n 12,\n \"outputSchema\",\n \"type\"\n ],\n \"message\": \"Invalid literal value, expected \\\"object\\\"\"\n },\n {\n \"received\": \"array\",\n \"code\": \"invalid_literal\",\n \"expected\": \"object\",\n \"path\": [\n \"tools\",\n 13,\n \"outputSchema\",\n \"type\"\n ],\n \"message\": \"Invalid literal value, expected \\\"object\\\"\"\n }\n]"
}
Here is the relevant part of my service implementation (where the methods are annotated with @McpTool
):
@Service
public class ProductServiceImpl implements ProductService {
@Autowired
private ProductRepository productRepository;
@Autowired
private ClothingRepository clothingRepository;
@Autowired
private ClothingVariationRepository clothingVariationRepository;
@Autowired
private PerfumeRepository perfumeRepository;
@Autowired
private PerfumeVariationRepository perfumeVariationRepository;
@Autowired
private ShoesRepository shoesRepository;
@Autowired
private ShoesVariationRepository shoesVariationRepository;
// Product operations
@Override
@McpTool(description = "Retrieve a product by its ID")
public Optional<Product> getProductById(Long id) { ... }
@Override
@McpTool(description = "Retrieve all products")
public List<Product> getAllProducts() { ... }
@Override
@McpTool(description = "Delete a product by its ID")
public void deleteProduct(Long id) { ... }
// Clothing operations
@Override
@McpTool(description = "Save a new clothing item")
public Clothing saveClothing(Clothing clothing) { ... }
@Override
@McpTool(description = "Retrieve a clothing item by its ID")
public Optional<Clothing> getClothingById(Long id) { ... }
@Override
@McpTool(description = "Retrieve all clothing items")
public List<Clothing> getAllClothing() { ... }
@Override
@McpTool(description = "Delete a clothing item by its ID")
public void deleteClothing(Long id) { ... }
// ClothingVariation operations
@Override
@McpTool(description = "Save a new clothing variation")
public ClothingVariation saveClothingVariation(ClothingVariation variation) { ... }
@Override
@McpTool(description = "Retrieve a clothing variation by its ID")
public Optional<ClothingVariation> getClothingVariationById(Long id) { ... }
@Override
@McpTool(description = "Retrieve all clothing variations")
public List<ClothingVariation> getAllClothingVariations() { ... }
@Override
@McpTool(description = "Retrieve clothing variations by size")
public Optional<ClothingVariation> getClothingVariationsBySize(String size) { ... }
@Override
@McpTool(description = "Delete a clothing variation by its ID")
public void deleteClothingVariation(Long id) { ... }
// Perfume operations
@Override
@McpTool(description = "Save a new perfume")
public Perfume savePerfume(Perfume perfume) { ... }
@Override
@McpTool(description = "Retrieve a perfume by its ID")
public Optional<Perfume> getPerfumeById(Long id) { ... }
@Override
@McpTool(description = "Retrieve a perfume by its name")
public Optional<Perfume> getPerfumeByName(String name) { ... }
@Override
@McpTool(description = "Retrieve all perfumes")
public List<Perfume> getAllPerfumes() { ... }
@Override
@McpTool(description = "Delete a perfume by its ID")
public void deletePerfume(Long id) { ... }
// PerfumeVariation operations
@Override
@McpTool(description = "Save a new perfume variation")
public PerfumeVariation savePerfumeVariation(PerfumeVariation variation) { ... }
@Override
@McpTool(description = "Retrieve a perfume variation by its ID")
public Optional<PerfumeVariation> getPerfumeVariationById(Long id) { ... }
@Override
@McpTool(description = "Retrieve all perfume variations")
public List<PerfumeVariation> getAllPerfumeVariations() { ... }
@Override
@McpTool(description = "Retrieve perfume variations by volume")
public Optional<PerfumeVariation> getPerfumeVariationsByVolume(int volume) { ... }
@Override
@McpTool(description = "Delete a perfume variation by its ID")
public void deletePerfumeVariation(Long id) { ... }
// Shoes operations
@Override
@McpTool(description = "Save a new shoes item")
public Shoes saveShoes(Shoes shoes) { ... }
@Override
@McpTool(description = "Retrieve a shoes item by its ID")
public Optional<Shoes> getShoesById(Long id) { ... }
@Override
@McpTool(description = "Retrieve all shoes items")
public List<Shoes> getAllShoes() { ... }
@Override
@McpTool(description = "Delete a shoes item by its ID")
public void deleteShoes(Long id) { ... }
// ShoesVariation operations
@Override
@McpTool(description = "Save a new shoes variation")
public ShoesVariation saveShoesVariation(ShoesVariation variation) { ... }
@Override
@McpTool(description = "Retrieve a shoes variation by its ID")
public Optional<ShoesVariation> getShoesVariationById(Long id) { ... }
@Override
@McpTool(description = "Retrieve all shoes variations")
public List<ShoesVariation> getAllShoesVariations() { ... }
@Override
@McpTool(description = "Retrieve shoes variations by size")
public Optional<ShoesVariation> getShoesVariationsBySize(int size) { ... }
@Override
@McpTool(description = "Delete a shoes variation by its ID")
public void deleteShoesVariation(Long id) { ... }
}
Let me know if you need more details.
Comment From: oburgosm
After further debugging, I can confirm the following:
- With
spring.ai.mcp.server.type=SYNC
: - Methods returning
void
, a Java object, orOptional<Object>
are listed correctly by MCPInspector. -
Methods returning a
List<Object>
cause errors when listing tools. For example, for a method like:java @McpTool(description = "Retrieve all clothing items") public List<Clothing> getAllClothing() {}
MCPInspector returns:{ "error": "[{\"received\": \"array\", \"code\": \"invalid_literal\", \"expected\": \"object\", \"path\": [\"tools\",0,\"outputSchema\",\"type\"], \"message\": \"Invalid literal value, expected \\\"object\\\"\"}]" }
-
With
spring.ai.mcp.server.type=ASYNC
: - The list of tools is always empty.
So, I believe there are two bugs: 1. No tools are listed when the type is ASYNC. 2. Listing tools fails for methods returning a list.
Let me know if you need more details or examples.
Comment From: YunKuiLu
Hi @tzolov @ilayaperumalg @oburgosm , I also encountered this problem, and I created a pr to fix it, can you help cr it? #4396 The modified code runs correctly on my end.
It should be because the loading order of AutoConfiguration
is incorrect, resulting in @McpTool
being unable to register into Spring properly.
Comment From: quaff
2. Listing tools fails for methods returning a list.
It's duplicate of GH-4373 which should be fixed by https://github.com/modelcontextprotocol/java-sdk/commit/d8959ef854aa75748bfa6d383c256bc9db2a8836