Skip to main content

Class 6: Spring AI MCP Server Starter, the Spring-Native Path

Duration: 30 minutes | Difficulty: Intermediate | Prerequisites: Class 5.


Companion code

This lesson carries on from Class 5. If you followed along, keep working on the project you already have. If you skipped it, clone the mcp_class_5 branch to start from the same place:

git clone --branch mcp_class_5 https://github.com/graphqlguy/graphQLMovieDB-MCP.git

By the end of this lesson your existing moviedb Spring Boot application also serves an MCP endpoint. It registers one placeholder tool, callable from MCP Inspector, which proves the wiring before Class 7 adds the real GraphQL translation. Track B replaces Track A's Apollo binary, YAML config file and operations directory with a Maven dependency, a few lines of application.yaml, and an annotation on a Spring service. The same Boot deployable now exposes both /graphql and /mcp.

This lesson uses several terms that may be new. Here is a short definition of each, and each one gets a fuller explanation where the lesson puts it to work:

  • Spring Boot starter is a dependency that brings in a feature's libraries together with the configuration that sets them up.
  • Auto-configuration is Spring Boot's mechanism for creating beans automatically when certain classes are on the classpath.
  • Bill of materials (BOM) is a Maven artifact that pins one consistent set of versions for a group of libraries.
  • JSON-RPC is the request and response format that MCP messages use.
  • Streamable HTTP is the MCP transport for servers reached over HTTP, with one endpoint for requests and streamed responses.
  • stdio is the MCP transport where the host starts the server as a subprocess and exchanges messages over standard input and output.
  • Server-Sent Events (SSE) is a one-way HTTP stream from a server to a client.
  • Capabilities are the features that a server and a client announce to each other during the MCP handshake.
  • SYNC and ASYNC are the two Spring AI MCP server types: blocking tool methods, or reactive ones that return Mono and Flux.
  • @McpTool is the Spring AI annotation that turns a method on a Spring bean into an MCP tool.
  • MCP Inspector is the MCP project's developer tool for connecting to a server and calling its tools by hand.

The Track B Trade​

Track A gave you "GraphQL endpoint plus binary equals MCP server." Track B trades the standalone process for an annotation in your existing Spring code. The MCP traffic terminates inside the Boot app's web container alongside the GraphQL traffic, so both endpoints share the app's Spring Security configuration and its Micrometer registry. You operate one deployable, with one set of credentials and one observability setup.

The cost is that Track B commits you to Spring as the language of your MCP server. For teams whose GraphQL backend is also Spring, that commitment is already made. The MCP layer fits into the feature-sliced package layout the moviedb app already uses: an mcp package beside movie, review and watchlist, with the same testing conventions and the same security configuration. Track A remains the right answer for polyglot backends and federated supergraphs, as Class 5 set out. For a Spring team learning MCP, Track B is the simplest place to start.

One more thing follows from the in-process model. The MCP tool methods can call Spring beans directly, including ExecutionGraphQlService, the programmatic entry point to your local GraphQL execution engine. That call skips an HTTP request and a round of JSON serialization. The time saved is small next to the time a model takes to choose a tool, so the benefit that matters is simpler code, with the GraphQL engine one method call away. Class 7 builds on that, and this lesson puts the wiring in place.

Keeping the MCP server inside the GraphQL application is a choice we made for this course, because it gives us one project to build and run in every lesson. A production team would more likely deploy the MCP server as its own Spring Boot service that calls /graphql over HTTP. That service is an ordinary client of the GraphQL API, and it can scale, upgrade and fail independently of the API. Class 17 covers that decision, and we plan a follow-up course that builds the standalone server.

Adding the Starter​

Add the Spring AI and one starter dependency to the existing moviedb pom.xml. Before that, check the Spring Boot version in the file's <parent> element. The edits go in different places, so take them one at a time.

The companion branches use Spring Boot 4.0.8. A project built earlier, for example with the Spring GraphQL tutorial, may still use 4.0.5, which brings Spring Framework 7.0.6. That Framework release holds back server-sent events that another thread writes, and sends them only when the stream ends (spring-framework#36537). The MCP server uses SSE for progress notifications and for the elicitation and sampling requests it sends during a tool call. On Boot 4.0.5 those messages reach the client only when the call finishes, and an elicitation question times out before the user sees it. If your <parent> element says 4.0.5, change it to 4.0.8, which brings Spring Framework 7.0.9:

pom.xml (the existing parent element, with the new version)
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>4.0.8</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>

The moviedb pom.xml does not have a <dependencyManagement> element yet, so this one is new. Add it as a sibling of the existing <dependencies>, immediately above it:

pom.xml (new element, above the existing dependencies block)
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-bom</artifactId>
<version>2.0.0</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>

The starter itself is a single <dependency> that belongs inside the <dependencies> element the file already has, next to the Boot starters:

pom.xml (inside the existing dependencies block)
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-mcp-server-webmvc</artifactId>
</dependency>
A second <dependencies> will not parse

The <dependencies> nested inside <dependencyManagement> above is a different element from the top-level one, which is why both blocks are legal in the same file. A second top-level <dependencies> is not legal. Maven rejects the file with [FATAL] Non-parseable POM: Duplicated tag: 'dependencies', and every later command fails before it reaches your code. Class 2 warned about the same mistake in the schema, with a duplicate type Query block.

The BOM pins every Spring AI artifact to one version, so the starter does not declare a version of its own. This lesson pins spring-ai-bom to 2.0.0, the version the companion repository uses, even though newer 2.0.x patches exist. With the same version as the companion, what you build matches what this lesson shows. Spring AI 2.0.0 reached general availability (GA) on June 12, 2026. It requires Spring Boot 4, which the moviedb app already uses. Spring AI 2.0.0 ships MCP Java SDK 2.0.0. The @McpTool annotation model and the streamable HTTP transport were developed during the 1.1 line, and in 2.0 they are part of Spring AI core.

One note about versions, because you will meet it as soon as you search for documentation. Much of what was written about Spring AI's MCP support in 2025 and early 2026 targets the 1.1.x line. Teams still on Spring Boot 3 get fixes on 1.1.x, and new development goes into 2.0. On 1.1.x the MCP annotations live in a community project, under org.springaicommunity.mcp.annotation. In 2.0 they are part of the core framework, under org.springframework.ai.mcp.annotation. A blog post that imports the community package was written for 1.1: its concepts still apply, but you have to change the package names. The protocol side stays the same, because MCP Java SDK 2.x negotiates MCP specification revision 2025-11-25, which is the revision this stack targets.

Rebuild before going further, so Maven resolves the new coordinates:

./mvnw clean test

The clean matters, as Class 3 explained. A branch switch leaves the previous branch's compiled classes in target/classes, and those stale classes cause failures unrelated to your change. If you run the app from your IDE instead, reload the Maven project there so it picks up the new dependency. What the starter adds is small: mcp-spring-webmvc, the MCP Java SDK, and the Jackson modules required for .

What the Auto-Configuration Brings In​

The starter's auto-configuration makes choices for you, and it is small enough to read in full if you ever need to. When Spring Boot finds spring-ai-starter-mcp-server-webmvc on the classpath, it activates a few auto-configuration classes, one per transport and concern. Together they set up four things.

The first is the MCP server bean itself: a singleton that holds the protocol implementation, the negotiated capabilities, and the registry of tools, resources, and prompts. Your code does not call this bean directly. The other three parts are built around it.

The second is the tool registry. At startup, the auto-configuration scans every Spring bean for methods annotated with @McpTool and registers them as MCP tools. Each method's parameters become tool inputs. The parameter names become the input property names, and descriptions come from @McpToolParam annotations, so only parameters with that annotation get a description. The method's return value is JSON-serialized into the tool result's content by default, and advertising a structured output schema is opt-in through @McpTool(generateOutputSchema = true). That registry is what makes the annotations work. The DIY track in Classes 13 and 14 builds tool definitions by hand instead.

The third is the HTTP endpoint. The starter registers a Spring MVC controller that exposes the MCP server over the configured transport. For the streamable HTTP transport that is a single endpoint, handling both single-shot JSON-RPC requests and Server-Sent Events for streaming responses. Older deployments on the transport use a pair of endpoints instead. The controller integrates with Spring Security like any other Spring MVC controller, and that is where Class 15 wires the OAuth 2.1 resource server.

The fourth is the JSON-RPC layer: Jackson modules for the MCP message shapes, the mapping of JSON-RPC errors, and the negotiation of the protocol version. The starter provides all of it, and knowing it exists helps when we debug a message exchange.

Configuring the Transport​

Add the MCP server configuration to application.yaml. The file already has a top-level spring: key holding spring.datasource.* and spring.graphql.*, and YAML allows a given key only once per mapping. So this is a new ai: branch inside that existing spring: block. If you add it as a second top-level key, Boot fails at startup with the SnakeYAML error found duplicate key spring, before it reads any of your configuration. The listing shows the spring: line only so the indentation level is clear:

src/main/resources/application.yaml (inside the existing spring: block)
spring:
ai:
mcp:
server:
name: moviedb-mcp
version: 1.0.0
type: SYNC
protocol: STREAMABLE
capabilities:
tool: true
resource: false
prompt: false
completion: false

Each setting in the block does one thing. The name and version reach MCP clients as serverInfo during the initialization handshake. Together they identify the server, and MCP Inspector shows the name in its connection panel. Pick a name you will keep. We use moviedb-mcp throughout the tutorial.

type: SYNC selects synchronous tool execution. The tool method runs on the request thread until it returns, and its return value is sent back to the client. The alternative, ASYNC, uses the Project Reactor types Mono and Flux, and suits reactive stacks. This tutorial uses SYNC in every Track B lesson, including the streaming and progress work in Class 10. The sync model is easier to follow, and the features it lacks are ones this tutorial does not use.

protocol: STREAMABLE selects streamable HTTP, which is the spec-recommended transport for hosted MCP servers and the natural fit for a Spring web application. The other values the protocol property accepts are SSE (the older split-endpoint Server-Sent Events transport) and STATELESS (a stateless variant of streamable HTTP, available for both the WebMVC and WebFlux starters). A stateless server does not keep per-session state, which also means it cannot send client-directed requests such as elicitation, sampling, or pings. The transport used by Class 19 is enabled by a separate property (spring.ai.mcp.server.stdio: true), not via protocol. A production deployment of this project would use streamable HTTP.

All four capability flags default to true, so capabilities.tool: true only restates the default. The three false values are the lines that change behavior. We turn off resource, prompt and completion because this tutorial exposes GraphQL queries and mutations as tools, and leaves MCP resources and prompts unused. Class 1 covered that mapping in detail. We still write tool: true, so the one place that configures capabilities lists all four, and the config matches what a client sees advertised after the handshake.

A First @McpTool​

Create a new package com.graphqlguy.moviedb.mcp and add a smoke-test tool whose only job is to prove the wiring:

src/main/java/com/graphqlguy/moviedb/mcp/HelloMcpTool.java
package com.graphqlguy.moviedb.mcp;

import org.springframework.ai.mcp.annotation.McpTool;
import org.springframework.ai.mcp.annotation.McpToolParam;
import org.springframework.stereotype.Service;

@Service
public class HelloMcpTool {

@McpTool(
name = "ping",
description = "Smoke-test tool. Returns a greeting that echoes the supplied name. " +
"Used during Class 6 to verify the MCP server wiring before real tools are added in Class 7.")
public String ping(
@McpToolParam(
description = "Name to greet. Any non-empty string.",
required = true
) String name) {
return "hello, " + name;
}
}

Three details matter:

  • @Service makes the class a Spring bean, so the auto-configuration's tool scanner can find it.
  • @McpTool supplies the tool name, ping, which the client sees, and the description, which the agent reads when it decides whether to call the tool.
  • @McpToolParam on the method parameter supplies the input's description and marks the input required. Both become part of the tool's for inputs.

The tool's description matters. Class 3 gave four questions for schema descriptions, and the same four apply to tools:

  • What does the tool do?
  • When should the agent call it?
  • What does it expect as input?
  • What does it return?

The smoke-test description says the tool exists to verify the wiring, so an agent reading the tool list knows to skip it for real tasks.

Running and Inspecting​

Restart the Boot app. The MCP server now answers at http://localhost:8080/mcp, the default path for streamable HTTP. To use a different path, set spring.ai.mcp.server.streamable-http.mcp-endpoint.

Open MCP Inspector in another terminal:

npx @modelcontextprotocol/[email protected]

Add a server the same way Class 4 did: Add Servers, then + Add manually, with a Server ID of moviedb-mcp, Transport set to streamable-http, and a URL of http://localhost:8080/mcp. Press Add and flip the toggle on the new row to connect. Notice the port. Track A pointed Inspector at Apollo's own process on :8000, and here the Boot app itself serves the MCP endpoint on :8080. After the handshake, the Tools tab lists ping with its description. Open it, type a name into the input, run it, and check that the greeting comes back. That confirms the whole chain works.

Compare that diagram to Class 4's. Track A had three boxes (MCP client, Apollo, Spring); Track B has two (MCP client, Spring). The dotted line between /mcp and /graphql is what Class 7 makes real: an in-process method call instead of an HTTP hop.

Summary​

This lesson turned the existing moviedb application into an MCP server without adding a process. One pinned Maven dependency brought in the Spring AI MCP server starter. A short application.yaml block selected the SYNC server type and the streamable HTTP transport, and disabled the capabilities we do not use. A single @Service class with an @McpTool method gave the server its first tool. MCP Inspector connected to http://localhost:8080/mcp and called ping, which proves that the auto-configuration, tool scanning, JSON-RPC layer and transport are wired correctly. The line between /mcp and /graphql in the diagram is still dotted, and Class 7 turns it into a real in-process call.

Further Reading​

What's Next​

Class 7 takes the @McpTool pattern from this chapter and applies it to the agent-facing surface from Class 2: the two query operations you added, and the addToWatchlist mutation you documented for agents. Each becomes a tool method whose body calls ExecutionGraphQlService with a parameterized, pre-defined operation string. The persisted-operations principle from Class 1 still applies, with a different implementation. By the end of Class 7 the same agent-facing surface that Track A built with a separate process will be served from one JVM, with three real tools instead of one smoke test.