Architecture
Part 1 - MCP Server
Dependency
Note that we don't need AI models in the MCP server.
implementation "org.springframework.ai:spring-ai-starter-mcp-server-webmvc"
Configuration
server:
port: 8080
spring:
application:
name: movie-mcp-server
ai:
mcp:
server:
name: movie-mcp-server
version: 1.0.0
type: SYNC
protocol: STREAMABLE
instructions: >
Provides tools for listing, finding, searching, and filtering movies.
streamable-http:
mcp-endpoint: /mcp
Repository
public record Movie(
Long id,
String title,
String director,
int releaseYear,
String genre,
double rating
) {
}
@Repository
public class MovieRepository {
private final List<Movie> movies = List.of(
new Movie(1L, "The Shawshank Redemption", "Frank Darabont", 1994, "Drama", 9.3),
new Movie(2L, "The Godfather", "Francis Ford Coppola", 1972, "Crime", 9.2),
new Movie(3L, "The Dark Knight", "Christopher Nolan", 2008, "Action", 9.0),
new Movie(4L, "Inception", "Christopher Nolan", 2010, "Science Fiction", 8.8),
new Movie(5L, "Interstellar", "Christopher Nolan", 2014, "Science Fiction", 8.7),
new Movie(6L, "Parasite", "Bong Joon Ho", 2019, "Thriller", 8.5),
new Movie(7L, "Spirited Away", "Hayao Miyazaki", 2001, "Animation", 8.6),
new Movie(8L, "Gladiator", "Ridley Scott", 2000, "Action", 8.5)
);
public List<Movie> findAll() {
return movies;
}
public Optional<Movie> findById(Long id) {
return movies.stream()
.filter(movie -> movie.id().equals(id))
.findFirst();
}
public List<Movie> searchByTitle(String title) {
String value = title.toLowerCase(Locale.ROOT);
return movies.stream()
.filter(movie -> movie.title().toLowerCase(Locale.ROOT).contains(value))
.toList();
}
}
Tool
@Component
public class MovieTools {
private final MovieRepository movieRepository;
public MovieTools(MovieRepository movieRepository) {
this.movieRepository = movieRepository;
}
@McpTool(
name = "listMovies",
description = "List all movies in the catalog",
generateOutputSchema = true
)
public List<Movie> listMovies() {
return movieRepository.findAll();
}
@McpTool(
name = "getMovie",
description = "Find a movie by its numeric ID",
generateOutputSchema = true
)
public Movie getMovie(
@McpToolParam(
description = "Numeric movie ID",
required = true
)
Long id
) {
return movieRepository.findById(id)
.orElseThrow(() -> new IllegalArgumentException("Movie not found: " + id));
}
@McpTool(
name = "searchMovies",
description = "Search for movies whose titles contain the supplied text",
generateOutputSchema = true
)
public List<Movie> searchMovies(
@McpToolParam(
description = "Text contained in the movie title",
required = true
)
String title
) {
return movieRepository.searchByTitle(title);
}
}
Spring AI automatically discovers the methods, creates tool schemas, exposes them via MCP and handles JSON-RPC.
Part 2 - MCP Client
Dependencies
implementation "org.springframework.ai:spring-ai-starter-model-openai"
implementation "org.springframework.ai:spring-ai-starter-mcp-client"
Configuration
server:
port: 8081
spring:
application:
name: movie-mcp-chat-client
ai:
openai:
api-key: ${OPENAI_API_KEY}
mcp:
client:
enabled: true
name: movie-chat-client
version: 1.0.0
type: SYNC
initialized: true
request-timeout: 30s
toolcallback:
enabled: true
streamable-http:
connections:
movie-server:
url: http://localhost:8080
endpoint: /mcp
Register Tools
@Configuration
public class ChatClientConfig {
@Bean
ChatClient movieChatClient(
ChatClient.Builder builder,
SyncMcpToolCallbackProvider mcpToolCallbackProvider
) {
return builder
.defaultSystem("""
You are a movie assistant.
Use the available movie tools whenever the user asks about
movies in the catalog. Do not invent catalog entries,
ratings, genres, release years, directors, or IDs.
When no matching movie is returned, clearly say that no
matching movie was found in the catalog.
""")
.defaultTools(mcpToolCallbackProvider)
.build();
}
}
Call the Model
@Service
public class MovieChatService {
private final ChatClient chatClient;
public MovieChatService(ChatClient chatClient) {
this.chatClient = chatClient;
}
public String chat(String question) {
return chatClient.prompt()
.user(question)
.call()
.content();
}
}
@RestController
@RequestMapping("/chat")
public class MovieChatController {
private final MovieChatService movieChatService;
public MovieChatController(MovieChatService movieChatService) {
this.movieChatService = movieChatService;
}
@PostMapping
public ChatResponse chat(@RequestBody ChatRequest request) {
return new ChatResponse(movieChatService.chat(request.question()));
}
}
public record ChatRequest(String question) {
}
public record ChatResponse(String answer) {
}
What Spring AI Handles
- Tool discovery
- JSON-RPC
- Capability discovery
- Tool execution
- Serialization
- Transport
- Session management
You mainly write @McpTool on the server and .defaultTools(toolProvider) on the client.
Testing the Server Independently
Now we can chat with the client just like any other AI agent. Before doing that, let's see how to test the MCP server independently. Although the MCP Inspector is the easiest way to test an MCP server, I'll use curl commands here so we can better understand the complete request and response flow.
export MCP_URL="http://localhost:8080/mcp"
export PROTOCOL_VERSION="2025-11-25"
export HEADERS_FILE="/tmp/movie-mcp-headers.txt"
Initialize
curl -i \
-D "$HEADERS_FILE" \
-X POST "$MCP_URL" \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-H "MCP-Protocol-Version: $PROTOCOL_VERSION" \
--data '{
"jsonrpc": "2.0",
"id": 1,
"method": "initialize",
"params": {
"protocolVersion": "2025-11-25",
"capabilities": {},
"clientInfo": {
"name": "curl-client",
"version": "1.0.0"
}
}
}'
Extract session id
export MCP_SESSION_ID="b527c982-1bdd-41ce-b125-f225edf4331c"
Send initialized notification
curl -i \
-X POST "$MCP_URL" \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-H "MCP-Protocol-Version: $PROTOCOL_VERSION" \
-H "Mcp-Session-Id: $MCP_SESSION_ID" \
--data '{
"jsonrpc": "2.0",
"method": "notifications/initialized"
}'
List tools
curl -sS \
-X POST "$MCP_URL" \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-H "MCP-Protocol-Version: $PROTOCOL_VERSION" \
-H "Mcp-Session-Id: $MCP_SESSION_ID" \
--data '{
"jsonrpc": "2.0",
"id": 2,
"method": "tools/list",
"params": {}
}'
List all movies
curl -sS \
-X POST "$MCP_URL" \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-H "MCP-Protocol-Version: $PROTOCOL_VERSION" \
-H "Mcp-Session-Id: $MCP_SESSION_ID" \
--data '{
"jsonrpc": "2.0",
"id": 3,
"method": "tools/call",
"params": {
"name": "listMovies",
"arguments": {}
}
}'
Get movie by id
curl -sS \
-X POST "$MCP_URL" \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-H "MCP-Protocol-Version: $PROTOCOL_VERSION" \
-H "Mcp-Session-Id: $MCP_SESSION_ID" \
--data '{
"jsonrpc": "2.0",
"id": 4,
"method": "tools/call",
"params": {
"name": "getMovie",
"arguments": {
"id": 4
}
}
}'
Close the session
curl -i \
-X DELETE "$MCP_URL" \
-H "MCP-Protocol-Version: $PROTOCOL_VERSION" \
-H "Mcp-Session-Id: $MCP_SESSION_ID"
Summary
Spring AI makes building both an MCP server and an MCP client remarkably simple by hiding the protocol details while letting developers focus on business logic.


Comments
Post a Comment