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:
-
Streamable HTTP:
/mcp -
SSE:
/mcp/sse -
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.http.root-path=/mcp
# Named server "bravo"
quarkus.mcp.server.bravo.http.root-path=/bravo/mcp
# Named server "charlie"
quarkus.mcp.server.charlie.http.root-path=/charlie/mcp
Each server will have its own set of endpoints:
| Server | Streamable HTTP Endpoint | SSE 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
The @McpServer annotation is repeatable which allows you to associate a feature with multiple servers.
import io.quarkiverse.mcp.server.Tool;
import io.quarkiverse.mcp.server.McpServer;
public class MyFeatures {
@McpServer("bravo") (1)
@Tool(description = "Tool for bravo server")
String bravoTool() {
return "Bravo";
}
@McpServer("bravo") (2)
@McpServer("charlie") (3)
@Tool(description = "Tool for bravo and charlie servers")
String bravoAndCharlieTool() {
return "Charlie";
}
}
| 1 | Binds this tool to the "bravo" server |
| 2 | Binds this tool to the "bravo" server |
| 3 | Binds this tool to the "charlie" server |
|
Backward compatibility
In versions 1.10 and lower, it was not possible to bind a feature to multiple servers.
Also a binding declared on a class was applied to all features defined in the class that did not declare the annotation themselves.
However, this behavior makes no sense anymore.
Instead, all values declared on a class are now included in the set of servers for a feature.
If you need to revert to the previous behavior, you can set the |
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 "Shared tool on bravo and default servers";
}
}
| 1 | Class-level annotation sets default for all methods |
| 2 | These tools inherit the "bravo" server binding |
| 3 | Class-level and method-level annotations are combined |
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.http.root-path=/public/mcp
quarkus.mcp.server.secure.http.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
Define the per-server tools:
public class MyFeatures {
@Tool(description = "Public tool")
String publicTool() {
return "No authentication required";
}
@McpServer("secure")
@Tool(description = "Secured tool")
String secureTool() {
return "Authentication required";
}
}
Scope of the automatic list-changed notification
When a feature (tool, prompt, resource or resource template) is registered or removed programmatically at runtime, an automatic notifications/*_list_changed notification is sent to the affected clients.
By default, this notification is scoped to the server(s) the feature is bound to; only the clients connected to those servers are notified.
Clients connected to other servers are not notified, because their feature list did not change.
For example, registering a tool bound to the bravo server only notifies the clients connected to bravo:
toolManager.newTool("query")
.setServerName("bravo") (1)
.setDescription("...")
.setHandler(...)
.register(); (2)
| 1 | The tool is bound to the bravo server. |
| 2 | Only clients connected to bravo receive the notifications/tools/list_changed notification. |
This behavior is controlled by the quarkus.mcp.server.auto-list-changed-strategy configuration property:
# matching-server (default): notify only the clients connected to the affected server(s)
# all: notify all connected clients, regardless of the server they are connected to
quarkus.mcp.server.auto-list-changed-strategy=matching-server
Set it to all to restore the behavior of versions 2.0.0 and earlier, where the notification was broadcast to all connected clients.
This setting only affects the automatic notification sent on registration/removal. The FeatureManager#notifyListChanged(Predicate) method always uses the supplied filter and is not affected.
|
Suppressing the automatic notification for a single feature
The automatic notification can also be turned off per feature via setNotifyListChanged(false) on the definition builder. When disabled, no list_changed notification is sent when the feature is registered nor when it is later removed, and the caller becomes responsible for sending it — typically via notifyListChanged(Predicate).
toolManager.newTool("query")
.setServerName("bravo")
.setNotifyListChanged(false) (1)
.setDescription("...")
.setHandler(...)
.register();
| 1 | No automatic notifications/tools/list_changed is sent on register or on the later removeTool("query", "bravo"). |
This is useful when a filter hides the feature for some connections: an unconditional notification would be wasted for the connections that cannot see the feature anyway. The caller can then send a filter-aware notification with notifyListChanged(conn → …).
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.firstContent().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.firstContent().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 |