Skip to content

Support runtime replacement in McpServer via put methods #229

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,28 @@ void testAddDuplicateTool() {
assertThatCode(() -> mcpAsyncServer.closeGracefully().block(Duration.ofSeconds(10))).doesNotThrowAnyException();
}

@Test
void testPutTool() {
var mcpAsyncServer = McpServer.async(createMcpTransportProvider())
.serverInfo("test-server", "1.0.0")
.capabilities(ServerCapabilities.builder().tools(true).build())
.build();

Tool toolV1 = new McpSchema.Tool(TEST_TOOL_NAME, "Tool with version 1.0.0", emptyJsonSchema);

StepVerifier.create(mcpAsyncServer.putTool(new McpServerFeatures.AsyncToolSpecification(toolV1,
(exchange, args) -> Mono.just(new CallToolResult(List.of(), false)))))
.verifyComplete();

Tool toolV2 = new McpSchema.Tool(TEST_TOOL_NAME, "Tool with version 2.0.0", emptyJsonSchema);

StepVerifier.create(mcpAsyncServer.putTool(new McpServerFeatures.AsyncToolSpecification(toolV2,
(exchange, args) -> Mono.just(new CallToolResult(List.of(), false)))))
.verifyComplete();

assertThatCode(() -> mcpAsyncServer.closeGracefully().block(Duration.ofSeconds(10))).doesNotThrowAnyException();
}

@Test
void testRemoveTool() {
Tool too = new McpSchema.Tool(TEST_TOOL_NAME, "Duplicate tool", emptyJsonSchema);
Expand Down Expand Up @@ -201,7 +223,7 @@ void testAddResource() {
.capabilities(ServerCapabilities.builder().resources(true, false).build())
.build();

Resource resource = new Resource(TEST_RESOURCE_URI, "Test Resource", "text/plain", "Test resource description",
Resource resource = new Resource(TEST_RESOURCE_URI, "Test Resource", "Test resource description", "text/plain",
null);
McpServerFeatures.AsyncResourceSpecification specification = new McpServerFeatures.AsyncResourceSpecification(
resource, (exchange, req) -> Mono.just(new ReadResourceResult(List.of())));
Expand Down Expand Up @@ -233,7 +255,7 @@ void testAddResourceWithoutCapability() {
.serverInfo("test-server", "1.0.0")
.build();

Resource resource = new Resource(TEST_RESOURCE_URI, "Test Resource", "text/plain", "Test resource description",
Resource resource = new Resource(TEST_RESOURCE_URI, "Test Resource", "Test resource description", "text/plain",
null);
McpServerFeatures.AsyncResourceSpecification specification = new McpServerFeatures.AsyncResourceSpecification(
resource, (exchange, req) -> Mono.just(new ReadResourceResult(List.of())));
Expand All @@ -244,6 +266,28 @@ void testAddResourceWithoutCapability() {
});
}

@Test
void testPutResource() {
var mcpAsyncServer = McpServer.async(createMcpTransportProvider())
.serverInfo("test-server", "1.0.0")
.capabilities(ServerCapabilities.builder().resources(true, false).build())
.build();

Resource resourceV1 = new Resource(TEST_RESOURCE_URI, "Test Resource", "Resource with version 1.0.0",
"text/plain", null);
McpServerFeatures.AsyncResourceSpecification specificationV1 = new McpServerFeatures.AsyncResourceSpecification(
resourceV1, (exchange, req) -> Mono.just(new ReadResourceResult(List.of())));
StepVerifier.create(mcpAsyncServer.putResource(specificationV1)).verifyComplete();

Resource resourceV2 = new Resource(TEST_RESOURCE_URI, "Test Resource", "Resource with version 2.0.0",
"text/plain", null);
McpServerFeatures.AsyncResourceSpecification specificationV2 = new McpServerFeatures.AsyncResourceSpecification(
resourceV2, (exchange, req) -> Mono.just(new ReadResourceResult(List.of())));
StepVerifier.create(mcpAsyncServer.putResource(specificationV2)).verifyComplete();

assertThatCode(() -> mcpAsyncServer.closeGracefully().block(Duration.ofSeconds(10))).doesNotThrowAnyException();
}

@Test
void testRemoveResourceWithoutCapability() {
// Create a server without resource capabilities
Expand Down Expand Up @@ -301,6 +345,28 @@ void testAddPromptWithoutCapability() {
});
}

@Test
void testPutPrompt() {
var mcpAsyncServer = McpServer.async(createMcpTransportProvider())
.serverInfo("test-server", "1.0.0")
.capabilities(ServerCapabilities.builder().prompts(false).build())
.build();

Prompt promptV1 = new Prompt(TEST_PROMPT_NAME, "Prompt with version 1.0.0", List.of());
McpServerFeatures.AsyncPromptSpecification specificationV1 = new McpServerFeatures.AsyncPromptSpecification(
promptV1, (exchange, req) -> Mono.just(new GetPromptResult("Test prompt description", List
.of(new PromptMessage(McpSchema.Role.ASSISTANT, new McpSchema.TextContent("Test content"))))));

StepVerifier.create(mcpAsyncServer.putPrompt(specificationV1)).verifyComplete();

Prompt promptV2 = new Prompt(TEST_PROMPT_NAME, "Prompt with version 2.0.0", List.of());
McpServerFeatures.AsyncPromptSpecification specificationV2 = new McpServerFeatures.AsyncPromptSpecification(
promptV2, (exchange, req) -> Mono.just(new GetPromptResult("Test prompt description", List
.of(new PromptMessage(McpSchema.Role.ASSISTANT, new McpSchema.TextContent("Test content"))))));

StepVerifier.create(mcpAsyncServer.putPrompt(specificationV2)).verifyComplete();
}

@Test
void testRemovePromptWithoutCapability() {
// Create a server without prompt capabilities
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,28 @@ void testAddDuplicateTool() {
assertThatCode(() -> mcpSyncServer.closeGracefully()).doesNotThrowAnyException();
}

@Test
void testPutTool() {
var mcpSyncServer = McpServer.sync(createMcpTransportProvider())
.serverInfo("test-server", "1.0.0")
.capabilities(ServerCapabilities.builder().tools(true).build())
.build();

Tool toolV1 = new McpSchema.Tool(TEST_TOOL_NAME, "Tool with version 1.0.0", emptyJsonSchema);

assertThatCode(() -> mcpSyncServer.putTool(new McpServerFeatures.SyncToolSpecification(toolV1,
(exchange, args) -> new CallToolResult(List.of(), false))))
.doesNotThrowAnyException();

Tool toolV2 = new McpSchema.Tool(TEST_TOOL_NAME, "Tool with version 2.0.0", emptyJsonSchema);

assertThatCode(() -> mcpSyncServer.putTool(new McpServerFeatures.SyncToolSpecification(toolV2,
(exchange, args) -> new CallToolResult(List.of(), false))))
.doesNotThrowAnyException();

assertThatCode(() -> mcpSyncServer.closeGracefully()).doesNotThrowAnyException();
}

@Test
void testRemoveTool() {
Tool tool = new McpSchema.Tool(TEST_TOOL_NAME, "Test tool", emptyJsonSchema);
Expand Down Expand Up @@ -198,7 +220,7 @@ void testAddResource() {
.capabilities(ServerCapabilities.builder().resources(true, false).build())
.build();

Resource resource = new Resource(TEST_RESOURCE_URI, "Test Resource", "text/plain", "Test resource description",
Resource resource = new Resource(TEST_RESOURCE_URI, "Test Resource", "Test resource description", "text/plain",
null);
McpServerFeatures.SyncResourceSpecification specification = new McpServerFeatures.SyncResourceSpecification(
resource, (exchange, req) -> new ReadResourceResult(List.of()));
Expand Down Expand Up @@ -228,7 +250,7 @@ void testAddResourceWithoutCapability() {
.serverInfo("test-server", "1.0.0")
.build();

Resource resource = new Resource(TEST_RESOURCE_URI, "Test Resource", "text/plain", "Test resource description",
Resource resource = new Resource(TEST_RESOURCE_URI, "Test Resource", "Test resource description", "text/plain",
null);
McpServerFeatures.SyncResourceSpecification specification = new McpServerFeatures.SyncResourceSpecification(
resource, (exchange, req) -> new ReadResourceResult(List.of()));
Expand All @@ -237,6 +259,28 @@ void testAddResourceWithoutCapability() {
.hasMessage("Server must be configured with resource capabilities");
}

@Test
void testPutResource() {
var mcpSyncServer = McpServer.sync(createMcpTransportProvider())
.serverInfo("test-server", "1.0.0")
.capabilities(ServerCapabilities.builder().resources(true, false).build())
.build();

Resource resourceV1 = new Resource(TEST_RESOURCE_URI, "Test Resource", "Resource with version 1.0.0",
"text/plain", null);
McpServerFeatures.SyncResourceSpecification specificationV1 = new McpServerFeatures.SyncResourceSpecification(
resourceV1, (exchange, req) -> new ReadResourceResult(List.of()));
assertThatCode(() -> mcpSyncServer.putResource(specificationV1)).doesNotThrowAnyException();

Resource resourceV2 = new Resource(TEST_RESOURCE_URI, "Test Resource", "Resource with version 2.0.0",
"text/plain", null);
McpServerFeatures.SyncResourceSpecification specificationV2 = new McpServerFeatures.SyncResourceSpecification(
resourceV2, (exchange, req) -> new ReadResourceResult(List.of()));
assertThatCode(() -> mcpSyncServer.putResource(specificationV2)).doesNotThrowAnyException();

assertThatCode(() -> mcpSyncServer.closeGracefully()).doesNotThrowAnyException();
}

@Test
void testRemoveResourceWithoutCapability() {
var serverWithoutResources = McpServer.sync(createMcpTransportProvider())
Expand Down Expand Up @@ -287,6 +331,26 @@ void testAddPromptWithoutCapability() {
.hasMessage("Server must be configured with prompt capabilities");
}

@Test
void testPutPrompt() {
var mcpSyncServer = McpServer.sync(createMcpTransportProvider())
.serverInfo("test-server", "1.0.0")
.capabilities(ServerCapabilities.builder().prompts(false).build())
.build();

Prompt promptV1 = new Prompt(TEST_PROMPT_NAME, "Prompt with version 1.0.0", List.of());
McpServerFeatures.SyncPromptSpecification specificationV1 = new McpServerFeatures.SyncPromptSpecification(
promptV1, (exchange, req) -> new GetPromptResult("Test prompt description", List
.of(new PromptMessage(McpSchema.Role.ASSISTANT, new McpSchema.TextContent("Test content")))));
assertThatCode(() -> mcpSyncServer.putPrompt(specificationV1)).doesNotThrowAnyException();

Prompt promptV2 = new Prompt(TEST_PROMPT_NAME, "Prompt with version 2.0.0", List.of());
McpServerFeatures.SyncPromptSpecification specificationV2 = new McpServerFeatures.SyncPromptSpecification(
promptV2, (exchange, req) -> new GetPromptResult("Test prompt description", List
.of(new PromptMessage(McpSchema.Role.ASSISTANT, new McpSchema.TextContent("Test content")))));
assertThatCode(() -> mcpSyncServer.putPrompt(specificationV2)).doesNotThrowAnyException();
}

@Test
void testRemovePromptWithoutCapability() {
var serverWithoutPrompts = McpServer.sync(createMcpTransportProvider())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -303,6 +303,44 @@ public Mono<Void> addTool(McpServerFeatures.AsyncToolSpecification toolSpecifica
});
}

/**
* Replaces an existing tool handler or adds a new one if it doesn't exist.
* @param toolSpecification The tool specification to put
* @return Mono that completes when clients have been notified of the change
*/
public Mono<Void> putTool(McpServerFeatures.AsyncToolSpecification toolSpecification) {
if (toolSpecification == null) {
return Mono.error(new McpError("Tool specification must not be null"));
}
if (toolSpecification.tool() == null) {
return Mono.error(new McpError("Tool must not be null"));
}
if (toolSpecification.call() == null) {
return Mono.error(new McpError("Tool call handler must not be null"));
}
if (this.serverCapabilities.tools() == null) {
return Mono.error(new McpError("Server must be configured with tool capabilities"));
}

return Mono.defer(() -> {
this.tools.replaceAll(asyncToolSpecification -> {
if (!asyncToolSpecification.tool().name().equals(toolSpecification.tool().name())) {
return asyncToolSpecification;
}
logger.debug("Replaced tool handler: {}", toolSpecification.tool().name());
return toolSpecification;
});
if (this.tools.addIfAbsent(toolSpecification)) {
logger.debug("Added tool handler: {}", toolSpecification.tool().name());
}

if (this.serverCapabilities.tools().listChanged()) {
return notifyToolsListChanged();
}
return Mono.empty();
});
}

/**
* Remove a tool handler at runtime.
* @param toolName The name of the tool handler to remove
Expand Down Expand Up @@ -396,6 +434,34 @@ public Mono<Void> addResource(McpServerFeatures.AsyncResourceSpecification resou
});
}

/**
* Replaces an existing resource handler or adds a new one if it doesn't exist.
* @param resourceSpecification The resource handler to put
* @return Mono that completes when clients have been notified of the change
*/
public Mono<Void> putResource(McpServerFeatures.AsyncResourceSpecification resourceSpecification) {
if (resourceSpecification == null || resourceSpecification.resource() == null) {
return Mono.error(new McpError("Resource must not be null"));
}

if (this.serverCapabilities.resources() == null) {
return Mono.error(new McpError("Server must be configured with resource capabilities"));
}

return Mono.defer(() -> {
if (this.resources.put(resourceSpecification.resource().uri(), resourceSpecification) != null) {
logger.debug("Replaced resource handler: {}", resourceSpecification.resource().uri());
}
else {
logger.debug("Added resource handler: {}", resourceSpecification.resource().uri());
}
if (this.serverCapabilities.resources().listChanged()) {
return notifyResourcesListChanged();
}
return Mono.empty();
});
}

/**
* Remove a resource handler at runtime.
* @param resourceUri The URI of the resource handler to remove
Expand Down Expand Up @@ -520,6 +586,36 @@ public Mono<Void> addPrompt(McpServerFeatures.AsyncPromptSpecification promptSpe
});
}

/**
* Replaces an existing prompt handler or adds a new one if it doesn't exist.
* @param promptSpecification The prompt handler to put
* @return Mono that completes when clients have been notified of the change
*/
public Mono<Void> putPrompt(McpServerFeatures.AsyncPromptSpecification promptSpecification) {
if (promptSpecification == null) {
return Mono.error(new McpError("Prompt specification must not be null"));
}
if (this.serverCapabilities.prompts() == null) {
return Mono.error(new McpError("Server must be configured with prompt capabilities"));
}

return Mono.defer(() -> {
if (this.prompts.put(promptSpecification.prompt().name(), promptSpecification) != null) {
logger.debug("Replaced prompt handler: {}", promptSpecification.prompt().name());
}
else {
logger.debug("Added prompt handler: {}", promptSpecification.prompt().name());
}

// Servers that declared the listChanged capability SHOULD send a
// notification, when the list of available prompts changes
if (this.serverCapabilities.prompts().listChanged()) {
return notifyPromptsListChanged();
}
return Mono.empty();
});
}

/**
* Remove a prompt handler at runtime.
* @param promptName The name of the prompt handler to remove
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,14 @@ public void addTool(McpServerFeatures.SyncToolSpecification toolHandler) {
this.asyncServer.addTool(McpServerFeatures.AsyncToolSpecification.fromSync(toolHandler)).block();
}

/**
* Put a new tool handler or update an existing one.
* @param toolHandler The tool handler to put
*/
public void putTool(McpServerFeatures.SyncToolSpecification toolHandler) {
this.asyncServer.putTool(McpServerFeatures.AsyncToolSpecification.fromSync(toolHandler)).block();
}

/**
* Remove a tool handler.
* @param toolName The name of the tool handler to remove
Expand All @@ -87,6 +95,14 @@ public void addResource(McpServerFeatures.SyncResourceSpecification resourceHand
this.asyncServer.addResource(McpServerFeatures.AsyncResourceSpecification.fromSync(resourceHandler)).block();
}

/**
* Put a new resource handler or update an existing one.
* @param resourceHandler The resource handler to put
*/
public void putResource(McpServerFeatures.SyncResourceSpecification resourceHandler) {
this.asyncServer.putResource(McpServerFeatures.AsyncResourceSpecification.fromSync(resourceHandler)).block();
}

/**
* Remove a resource handler.
* @param resourceUri The URI of the resource handler to remove
Expand All @@ -103,6 +119,14 @@ public void addPrompt(McpServerFeatures.SyncPromptSpecification promptSpecificat
this.asyncServer.addPrompt(McpServerFeatures.AsyncPromptSpecification.fromSync(promptSpecification)).block();
}

/**
* Put a new prompt handler or update an existing one.
* @param promptSpecification The prompt specification to put
*/
public void putPrompt(McpServerFeatures.SyncPromptSpecification promptSpecification) {
this.asyncServer.putPrompt(McpServerFeatures.AsyncPromptSpecification.fromSync(promptSpecification)).block();
}

/**
* Remove a prompt handler.
* @param promptName The name of the prompt handler to remove
Expand Down
Loading