Multiple Server Configurations
This guide covers how to configure and run multiple MCP servers within a single Quarkus application, each with its own endpoint, features, and security policies.
Overview
The Quarkus MCP Server extension supports running multiple independent MCP server instances in a single application. Each server can have:
-
Different root paths for HTTP transports
-
Tools, resources, and prompts specific to each server
-
Different authentication/authorization policies
-
Per-server settings (traffic logging, etc.)
This is useful for:
-
To serve different clients with isolated feature sets
-
To handle different security requirements for different APIs
-
To run multiple API versions simultaneously
-
To gather multiple logical services in one deployment
Default Server
By default, a single MCP server is configured and features are registered to it:
import io.quarkiverse.mcp.server.Tool;
public class MyFeatures {
@Tool(description = "Default server tool")
String defaultTool() {
return "Hello from default server";
}
}
This tool is automatically registered to the default server, accessible at the default endpoints:
-
SSE:
/mcp/sse -
Streamable HTTP:
/mcp -
WebSocket:
/ws/mcp
Configuring Multiple Servers
Step 1: Configure Server Endpoints
Define different root paths for each named server in application.properties:
# Default server (unnamed)
quarkus.mcp.server.sse.root-path=/mcp
# Named server "bravo"
quarkus.mcp.server.bravo.sse.root-path=/bravo/mcp
# Named server "charlie"
quarkus.mcp.server.charlie.sse.root-path=/charlie/mcp
Each server will have its own set of endpoints:
| Server | SSE Endpoint | Streamable HTTP Endpoint |
|---|---|---|
Default |
|
|
bravo |
|
|
charlie |
|
|
Step 2: Bind Features to Servers
Use the @McpServer annotation to bind features to specific servers:
import io.quarkiverse.mcp.server.Tool;
import io.quarkiverse.mcp.server.McpServer;
public class MyFeatures {
@Tool(description = "Tool for default server")
String defaultTool() { (1)
return "Default";
}
@McpServer("bravo") (2)
@Tool(description = "Tool for bravo server")
String bravoTool() {
return "Bravo";
}
@McpServer("charlie") (3)
@Tool(description = "Tool for charlie server")
String charlieTool() {
return "Charlie";
}
}
| 1 | Without @McpServer, the tool goes to the default server |
| 2 | Binds this tool to the "bravo" server |
| 3 | Binds this tool to the "charlie" server |
Now:
-
defaultToolis available only at/mcp -
bravoToolis available only at/bravo/mcp -
charlieToolis available only at/charlie/mcp
Using @McpServer Annotation
Method-Level Binding
Bind individual features to specific servers:
public class MultiServerFeatures {
@Tool
String publicTool() {
return "Available on default server";
}
@McpServer("admin")
@Tool
String adminTool() {
return "Available on admin server only";
}
@McpServer("api-v2")
@Resource(uri = "config://settings")
TextResourceContents v2Config() {
return TextResourceContents.create(
"config://settings",
"{\"version\": 2}");
}
}
Class-Level Binding
Bind all features in a class to a server by default:
@McpServer("bravo") (1)
public class BravoFeatures {
@Tool
String bravoTool1() { (2)
return "Tool 1 on bravo server";
}
@Tool
String bravoTool2() { (2)
return "Tool 2 on bravo server";
}
@McpServer(McpServer.DEFAULT) (3)
@Tool
String sharedTool() {
return "Override: on default server";
}
}
| 1 | Class-level annotation sets default for all methods |
| 2 | These tools inherit the "bravo" server binding |
| 3 | Method-level annotation overrides class-level |
Default Server Constant
Use McpServer.DEFAULT to explicitly reference the default (unnamed) server:
import static io.quarkiverse.mcp.server.McpServer.DEFAULT;
@McpServer("special")
public class SpecialFeatures {
@Tool
String specialTool() {
return "On special server"; (1)
}
@McpServer(DEFAULT) (2)
@Tool
String defaultTool() {
return "On default server";
}
}
| 1 | Inherits "special" from class-level annotation |
| 2 | Explicitly override to use default server |
Per-Server Configuration
Configure each server independently using the naming pattern:
quarkus.mcp.server.<server-name>.<property>
Security Configuration
Each server can have different security policies.
Example: Public and Secured Servers
# Server endpoints
quarkus.mcp.server.sse.root-path=/public/mcp
quarkus.mcp.server.secure.sse.root-path=/secure/mcp
# Secure server requires authentication
quarkus.http.auth.permission.secure.paths=/secure/mcp/*
quarkus.http.auth.permission.secure.policy=authenticated
# Public server: no authentication
quarkus.http.auth.permission.public.paths=/public/mcp/*
quarkus.http.auth.permission.public.policy=permit
public class MyFeatures {
@Tool(description = "Public tool")
String publicTool() {
return "No authentication required";
}
@McpServer("secure")
@Tool(description = "Secured tool")
String secureTool() {
return "Authentication required";
}
}
Testing Multiple Servers
Test each server independently using McpAssured:
import io.quarkiverse.mcp.server.test.McpAssured;
import io.quarkiverse.mcp.server.test.McpAssured.McpStreamableTestClient;
@Test
public void testDefaultServer() {
McpStreamableTestClient client = McpAssured.newStreamableClient()
.setMcpPath("/mcp") (1)
.build()
.connect();
client.when()
.toolsCall("defaultTool", response ->
assertEquals("Default", response.content().get(0).asText().text()))
.toolsCall("bravoTool") (2)
.withErrorAssert(error ->
assertEquals("Invalid tool name: bravoTool", error.message()))
.send()
.thenAssertResults();
}
@Test
public void testBravoServer() {
McpStreamableTestClient client = McpAssured.newStreamableClient()
.setMcpPath("/bravo/mcp") (3)
.build()
.connect();
client.when()
.toolsCall("bravoTool", response ->
assertEquals("Bravo", response.content().get(0).asText().text()))
.toolsCall("defaultTool") (4)
.withErrorAssert(error ->
assertEquals("Invalid tool name: defaultTool", error.message()))
.send()
.thenAssertResults();
}
| 1 | Connect to default server |
| 2 | Tools from other servers are not available |
| 3 | Connect to bravo server |
| 4 | Default server tools are not available here |