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 port 8080 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, setting spring.ai.mcp.server.type to ASYNC 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, or Optional<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