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

/mcp

/mcp/sse

bravo

/bravo/mcp

/bravo/mcp/sse

charlie

/charlie/mcp

/charlie/mcp/sse

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:

  • defaultTool is available only at /mcp

  • bravoTool is available only at /bravo/mcp

  • charlieTool is 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 quarkus.mcp.server.support-multi-server-bindings configuration property to false. Then the previous rules will apply, and multiple server bindings will then result in a build failure.

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>

Traffic Logging

Enable traffic logging per server:

# Enable logging for bravo server only
quarkus.mcp.server.bravo.traffic-logging.enabled=true
quarkus.mcp.server.bravo.traffic-logging.text-limit=500

# Default server: no logging
quarkus.mcp.server.traffic-logging.enabled=false

Transport Configuration

Configure transports independently:

# Default server: HTTP only
quarkus.mcp.server.http.root-path=/api/mcp

# Admin server: Different path
quarkus.mcp.server.admin.http.root-path=/admin/mcp

# Public server: WebSocket
quarkus.mcp.server.public.ws.root-path=/public/ws/mcp

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