Add dynamic tool update example for Model Context Protocol

Implement a new example demonstrating how MCP servers can dynamically update available tools
at runtime and how clients can detect these changes.

- Add Server implementation that starts with weather forecast tools and dynamically adds math operation tools
- Add Client implementation that detects tool changes via MCP notifications
- Complete client/server architecture with proper tool registration and discovery
- Add detailed README explaining the dynamic tool update process and implementation

Signed-off-by: Christian Tzolov <christian.tzolov@broadcom.com>
This commit is contained in:
Christian Tzolov
2025-05-02 11:26:13 +03:00
parent f79eef4a2a
commit 2c9fa4d8fd
22 changed files with 1653 additions and 241 deletions

View File

@@ -0,0 +1,39 @@
/*
* Copyright 2024 - 2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.ai.mcp.sample.server;
import org.springframework.ai.tool.annotation.Tool;
public class MathTools {
public MathTools() {
}
@Tool(description = "Adds two numbers")
public int sumNumbers(int number1, int number2) {
return number1 + number2;
}
@Tool(description = "Multiplies two numbers")
public int multiplyNumbers(int number1, int number2) {
return number1 * number2;
}
@Tool(description = "Divide two numbers")
public double divideNumbers(double number1, double number2) {
return number1 / number2;
}
}

View File

@@ -0,0 +1,64 @@
package org.springframework.ai.mcp.sample.server;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import io.modelcontextprotocol.server.McpServerFeatures.SyncToolSpecification;
import io.modelcontextprotocol.server.McpSyncServer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.ai.mcp.McpToolUtils;
import org.springframework.ai.tool.ToolCallbackProvider;
import org.springframework.ai.tool.ToolCallbacks;
import org.springframework.ai.tool.method.MethodToolCallbackProvider;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
@SpringBootApplication
@RestController
public class ServerApplication {
private static final Logger logger = LoggerFactory.getLogger(ServerApplication.class);
CountDownLatch latch = new CountDownLatch(1);
public static void main(String[] args) {
SpringApplication.run(ServerApplication.class, args);
}
@GetMapping("/updateTools")
public String greeting() {
latch.countDown();
return "Update signal received!";
}
@Bean
public ToolCallbackProvider weatherTools(WeatherService weatherService) {
return MethodToolCallbackProvider.builder().toolObjects(weatherService).build();
}
@Bean
public CommandLineRunner predefinedQuestions(McpSyncServer mcpSyncServer) {
return args -> {
logger.info("Server: " + mcpSyncServer.getServerInfo());
latch.await();
List<SyncToolSpecification> newTools = McpToolUtils
.toSyncToolSpecifications(ToolCallbacks.from(new MathTools()));
mcpSyncServer.addTool(newTools.iterator().next());
logger.info("Tools updated: ");
};
}
}

View File

@@ -0,0 +1,65 @@
/*
* Copyright 2025 - 2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.ai.mcp.sample.server;
import java.time.LocalDateTime;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
import org.springframework.ai.chat.model.ToolContext;
import org.springframework.ai.tool.annotation.Tool;
import org.springframework.ai.tool.annotation.ToolParam;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestClient;
@Service
public class WeatherService {
private final RestClient restClient;
public WeatherService() {
this.restClient = RestClient.builder()
.defaultHeader("Accept", "application/geo+json")
.defaultHeader("User-Agent", "WeatherApiClient/1.0 (your@email.com)")
.build();
}
@JsonIgnoreProperties(ignoreUnknown = true)
public record WeatherResponse(@JsonProperty("current") Current current) { // @formatter:off
public record Current(
@JsonProperty("time") LocalDateTime time,
@JsonProperty("interval") int interval,
@JsonProperty("temperature_2m") double temperature_2m) {
}
} // @formatter:on
@Tool(description = "Get the temperature (in celsius) for a specific location") // @formatter:off
public WeatherResponse weatherForecast(
@ToolParam(description = "The location latitude") double latitude,
@ToolParam(description = "The location longitude") double longitude,
ToolContext toolContext) { // @formatter:on
WeatherResponse weatherResponse = restClient
.get()
.uri("https://api.open-meteo.com/v1/forecast?latitude={latitude}&longitude={longitude}&current=temperature_2m",
latitude, longitude)
.retrieve()
.body(WeatherResponse.class);
return weatherResponse;
}
}

View File

@@ -0,0 +1,10 @@
# spring.main.web-application-type=none
# NOTE: You must disable the banner and the console logging
# to allow the STDIO transport to work !!!
spring.main.banner-mode=off
# logging.pattern.console=
spring.ai.mcp.server.name=my-mcp-server
spring.ai.mcp.server.version=0.0.1