skip to content

On the MCP server side, how do you expose @Tool-annotated beans as MCP tools in a Spring AI application?

level: middleimportance: should knowfreq 38%

answer

  1. @Tool + @ToolParam on plain bean methods
  2. MethodToolCallbackProvider.builder().toolObjects(...)
  3. server auto-config aggregates ToolCallbackProvider beans
  4. spring.ai.mcp.server.name/version/type
  5. descriptions drive model tool selection

basics

~20 s

Add spring-ai-starter-mcp-server, annotate your methods with @Tool (params with @ToolParam), and register a ToolCallbackProvider bean built via MethodToolCallbackProvider from those objects. The MCP server auto-config discovers the provider and publishes each @Tool method as an MCP tool.

solid answer

~40 s

Include a server starter (e.g. spring-ai-starter-mcp-server or its webmvc/webflux variant). Write a normal Spring bean whose methods are annotated with @Tool(description=...), and describe arguments with @ToolParam so the generated JSON schema is meaningful. Then expose a ToolCallbackProvider bean: MethodToolCallbackProvider.builder().toolObjects(myService).build(). The MCP server auto-configuration collects all ToolCallbackProvider beans and advertises their tools over the negotiated transport, so any MCP client can tools/list and tools/call them. Configure server identity with spring.ai.mcp.server.name / .version and choose SYNC or ASYNC via spring.ai.mcp.server.type (async pairs with the WebFlux transport). The tool's name defaults to the method name and its description drives when the model calls it, so write clear descriptions. This is the same @Tool mechanism used for in-process tools — MCP just makes them remotely discoverable.

go deeper

for a junior

Know that @Tool methods plus a server starter make functions callable by MCP clients.

for a middle

Be able to write the @Tool/@ToolParam bean and register a MethodToolCallbackProvider, and set server name/version.

for a senior

Explain schema generation, SYNC vs ASYNC server type/transport, and that only ToolCallbackProvider beans are published.

for a principal

Own tool-surface governance: naming, descriptions, input validation/authz, versioning, and whether to expose internal capabilities externally at all.

## Goal You want capabilities defined in your Spring app to be callable by *external* MCP clients (other apps, IDEs, assistants). That means running an **MCP server** that advertises your functions as **MCP tools**. ## Step 1 — the starter Add an MCP **server** starter. `spring-ai-starter-mcp-server` gives a stdio server; `spring-ai-starter-mcp-server-webmvc` and `spring-ai-starter-mcp-server-webflux` give HTTP/SSE servers on the respective web stacks. This brings in Boot auto-configuration that stands up the MCP server and its transport. ## Step 2 — annotate tools Write an ordinary Spring bean and annotate the exposed methods with `@Tool` from `org.springframework.ai.tool.annotation.Tool`: ```java @Service class WeatherService { @Tool(description = "Get the current weather for a city") String getWeather(@ToolParam(description = "City name, e.g. 'Berlin'") String city) { return weatherApi.lookup(city); } } ``` `@Tool(description=...)` supplies the natural-language description the LLM uses to decide *whether* to call the tool; `@ToolParam(description=..., required=...)` documents each argument. Spring AI derives a **JSON input schema** from the method signature plus these annotations. The tool **name** defaults to the method name (can be overridden via the annotation's name attribute). ## Step 3 — register a ToolCallbackProvider The MCP server auto-config publishes tools from `ToolCallbackProvider` beans. Turn your annotated object(s) into one with `MethodToolCallbackProvider`: ```java @Bean ToolCallbackProvider weatherTools(WeatherService weatherService) { return MethodToolCallbackProvider.builder() .toolObjects(weatherService) // scan @Tool methods on these beans .build(); } ``` You can pass several objects to `toolObjects(...)`. Every `ToolCallbackProvider` bean in the context is aggregated and exposed. ## Step 4 — server config ```properties spring.ai.mcp.server.name=weather-server spring.ai.mcp.server.version=1.0.0 spring.ai.mcp.server.type=SYNC # or ASYNC (WebFlux) ``` `name`/`version` are reported in the `initialize` handshake. `type` picks a synchronous (blocking) or asynchronous (Reactor) server; ASYNC is paired with the WebFlux transport. ## What happens at runtime On client connect, capabilities are negotiated; the client's `tools/list` returns your tools with their descriptions and schemas; a `tools/call` invokes the underlying method, marshalling arguments from JSON and serializing the return value back. ## Gotchas - **Descriptions are load-bearing.** A vague `@Tool` description means the model calls the tool at the wrong time or not at all. - **Only `ToolCallbackProvider` beans are published** — a bean with `@Tool` methods that you never wrap in a provider won't be exposed by the MCP server. - **Argument types must be serializable** to/from JSON; complex nested types work but keep them simple and well-described. - **Security is yours.** The tool runs real code; validate inputs and enforce authz — MCP does not authenticate callers for you. - **Return type mapping**: keep returns to strings/simple DTOs so the JSON result is predictable.

  • Why does the description on @Tool matter so much?
    The LLM sees only the tool name, description, and input schema — not your code. The description is how the model decides when the tool is relevant, so ambiguous text causes wrong or missed calls.
  • You added @Tool methods but the client's tools/list shows nothing. Why?
    Most likely you never wrapped the bean in a ToolCallbackProvider (e.g. MethodToolCallbackProvider). The MCP server only publishes tools from ToolCallbackProvider beans, or the wrong/no server starter is on the classpath.

saying these in an interview costs you the question

  • Expecting @Tool methods to be exposed without any ToolCallbackProvider bean
  • Omitting descriptions and assuming the model still calls tools correctly
  • Assuming MCP authenticates callers so input validation isn't needed

context