Ein minimaler MCP-Server (Model Context Protocol) auf Basis von Spring Boot 4 und Spring AI 2.0.
Das Model Context Protocol ist ein offener Standard, mit dem KI-Modelle (z. B. Claude, GPT) strukturiert auf externe Tools und Dienste zugreifen können.
Dieses Projekt zeigt den kleinstmöglichen Aufbau:
| Schicht | Technologie |
|---|---|
| HTTP-Transport | Spring WebMVC oder WebFlux (SSE) |
| MCP-Protokollstack | spring-ai-starter-mcp-server-webmvc / -webflux |
| Tool-Definition | @Tool-Annotation auf einfachen Spring-Beans |
| Build | Standard-Maven-Build (spring-boot-maven-plugin) |
Die enthaltenen Beispiel-Tools (greet, serverTime) demonstrieren das Grundprinzip
und lassen sich als Vorlage für eigene Integrationen verwenden.
- Java 25
- Maven 3.9+
curl -s -o spring-init.zip "https://start.spring.io/starter.zip?\
type=maven-project&language=java&bootVersion=4.1.0\
&groupId=com.example&artifactId=helloworld\
&packageName=com.example.helloworld&javaVersion=25\
&dependencies=configuration-processor,spring-ai-mcp-server"
unzip spring-init.zipSpring Initializr liefert ein fertiges Maven-Projekt mit mvnw, .gitignore und
einer leeren HelloworldApplication.java.
Hinweis:
spring-ai-mcp-servererzeugt den Servlet-Starterspring-ai-starter-mcp-server. In Schritt 2 wird das Artifact auf die WebFlux-Variantespring-ai-starter-mcp-server-webfluxgeändert.
Eine Anpassung gegenüber dem generierten Stand:
Generierten MCP-Starter auf WebMVC oder WebFlux-Variante umstellen:
Der Initializr erzeugt den Servlet-basierten Starter. Den artifactId auf die
reaktive Variante ändern:
<!-- generiert (ersetzen): -->
<artifactId>spring-ai-starter-mcp-server</artifactId>
<!-- ersetzen durch: -->
<artifactId>spring-ai-starter-mcp-server-webmvc</artifactId>
<!-- ersetzen durch: -->
<artifactId>spring-ai-starter-mcp-server-webflux</artifactId>Der Starter zieht WebFlux, Reactor und den MCP-Protokollstack selbst mit —
spring-boot-starter-webflux muss nicht separat eingetragen werden.
MCP-Tools sind einfache Spring-Beans, deren Methoden mit @Tool annotiert
werden. Die description erscheint im MCP-Toolkatalog und wird vom
KI-Modell für die Tool-Auswahl genutzt.
@Service
public class HelloWorldTools {
@Tool(description = "Returns a greeting message for the given name")
public String greet(String name) {
return "Hello, %s! Welcome to the MCP Hello World Server.".formatted(name);
}
@Tool(description = "Returns the current server time as ISO-8601 string")
public String serverTime() {
return java.time.Instant.now().toString();
}
}Spring AI benötigt einen ToolCallbackProvider-Bean, der dem MCP-Server
mitteilt, welche Tools exportiert werden sollen:
@Bean
public ToolCallbackProvider helloWorldToolProvider(HelloWorldTools helloWorldTools) {
return MethodToolCallbackProvider.builder()
.toolObjects(helloWorldTools)
.build();
}spring.application.name=helloworld
spring.ai.mcp.server.name=hello-world-mcp
spring.ai.mcp.server.version=1.0.0
spring.ai.mcp.server.type=ASYNC <-- nur für Webflux
spring.ai.mcp.server.protocol=STATELESS <-- siehe Abschnitt 5a
server.port=8080type=ASYNCaktiviert den reaktiven SSE-Transport (passend zu WebFlux).nameundversionerscheinen im MCP-Handshake.protocolbestimmt den Transport-Modus (SSE,STREAMABLE,STATELESS) — siehe Abschnitt 5a für Details und Konsequenzen. Aktuell in diesem Projekt:STATELESS.
Sowohl der WebMVC- als auch der WebFlux-Starter registrieren je nach Wert
dieser Property eine von drei unterschiedlichen Transport-Implementierungen
(McpServerSseWebMvcAutoConfiguration, McpServerStreamableHttpWebMvcAutoConfiguration,
McpServerStatelessWebMvcAutoConfiguration — bzw. die WebFlux-Pendants). Es
handelt sich also nicht nur um ein Flag, sondern um drei komplett
unterschiedliche Server-Implementierungen mit eigenem Endpoint-Verhalten.
spring.ai.mcp.server.protocol=SSE | STREAMABLE | STATELESS- Zwei getrennte Endpoints:
GET /sse(öffnet eine dauerhafte SSE-Verbindung, liefert einesessionId) undPOST /mcp/message(Client schickt JSON-RPC-Requests, verknüpft über diesessionIdaus dem SSE-Handshake). - Die zugehörigen Properties (
sse-endpoint,sse-message-endpoint,base-url,keep-alive-interval) sind in der aktuellen Version bereits als@deprecatedmarkiert — nur für ältere MCP-Clients gedacht, die das neue Streamable-HTTP-Protokoll noch nicht unterstützen. - Konsequenz: voll zustandsbehaftet. Die offene SSE-Verbindung lebt auf genau einer Server-Instanz; horizontale Skalierung erfordert Sticky Sessions oder gemeinsam genutzten Session-Speicher. Fällt die Instanz aus, bricht die Verbindung ab und der Client muss neu verbinden.
- Ein einzelner Endpoint (
spring.ai.mcp.server.streamable-http.mcp-endpoint, Default/mcp) für alle Requests. Der Server kann pro Response entweder direkt JSON zurückgeben oder auf eine SSE-Stream-Antwort „hochstufen“ (nötig für Server-Push wie Sampling-Requests oder Change-Notifications). - Beim
initialize-Call vergibt der Server eine Session, die der Client über den HeaderMcp-Session-Idbei allen Folgerequests mitschicken muss. Zusätzliche Optionen:keep-alive-interval(Ping-Intervall für offene Streams),disallow-delete(verbietetDELETE /mcp, also das aktive Beenden der Session durch den Client). - Konsequenz: zustandsbehaftet, aber flexibler als
SSE. Unterstützt Resumability (Wiederaufnahme unterbrochener Streams überLast-Event-ID) und bidirektionale Server→Client-Kommunikation. Für horizontale Skalierung wird trotzdem entweder Sticky Routing (Session-ID → gleiche Instanz) oder ein geteilter Session-Store benötigt.
- Derselbe
POST /mcp-Endpoint wie beiSTREAMABLE, aber ohne Session-Konzept: keinMcp-Session-Id-Header, keine offene SSE-Verbindung, keinDELETE /mcp. Jeder Request (initialize,tools/list,tools/call, …) wird unabhängig und vollständig in sich abgeschlossen verarbeitet. - Verifiziert durch direkten Test gegen diesen Server:
tools/listfunktioniert als eigenständigercurl-Aufruf ganz ohne vorherigesinitializein derselben Verbindung und ohne jeglichen Session-Header. - Konsequenz:
- ✅ Beliebig horizontal skalierbar hinter einem einfachen Load Balancer ohne Sticky Sessions. Jede Instanz kann jeden Request beantworten.
- ✅ Passt gut zu Serverless/Container-Umgebungen mit kurzlebigen Instanzen.
- ❌ Kein Server-initiierter Push: Sampling-Requests,
listChanged- Notifications für Tools/Resources/Prompts kommen beim Client nicht an, da kein offener Stream existiert, über den der Server sie schicken könnte. - ❌ Keine Resumability. Ein abgebrochener Request muss vom Client komplett wiederholt werden, es gibt nichts, an das angeknüpft werden könnte.
- Sinnvoll, wenn die Tools selbst zustandslos sind (wie
greetundserverTimein diesem Projekt) und keine der Push-Fähigkeiten benötigt werden.
Aktuell nutzt dieses Projekt (application.properties) den Modus
STATELESS.
./mvnw spring-boot:runDer Server lauscht auf http://localhost:8080.
Passend zum aktuell konfigurierten protocol=STATELESS (bzw. STREAMABLE)
wird der Streamable-HTTP-Endpoint /mcp verwendet, nicht /sse:
{
"mcpServers": {
"hello-world-mcp": {
"type": "streamable-http",
"url": "http://localhost:8080/mcp"
}
}
}Achtung:
/sseexistiert nur, wennspring.ai.mcp.server.protocol=SSEgesetzt ist (siehe Abschnitt 5a). Mit dem aktuellenSTATELESS-Modus ist dieser Endpoint nicht registriert — ein Client, der auftype: sse//ssekonfiguriert ist, kann sich nicht verbinden.