聊聊spring ai的mcp server

聊聊spring ai的mcp server

本文主要研究一下spring ai的mcp server

mcp java sdk

mcp提供了java sdk,同时还提供了spring webflux及mvc的sse实现

<dependencyManagement> <dependencies> <dependency> <groupId>io.modelcontextprotocol.sdk</groupId> <artifactId>mcp-bom</artifactId> <version>0.8.0</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement> <dependency> <groupId>io.modelcontextprotocol.sdk</groupId> <artifactId>mcp</artifactId> </dependency> <!-- Spring WebFlux-based SSE client and server transport --> <dependency> <groupId>io.modelcontextprotocol.sdk</groupId> <artifactId>mcp-spring-webflux</artifactId> </dependency> <!-- Spring WebMVC-based SSE server transport --> <dependency> <groupId>io.modelcontextprotocol.sdk</groupId> <artifactId>mcp-spring-webmvc</artifactId> </dependency> 

spring ai mcp

Spring AI MCP扩展了mcp java sdk,提供了spring boot的集成

 <dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-starter-mcp-client</artifactId> </dependency> <dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-starter-mcp-client-webflux</artifactId> </dependency> <dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-starter-mcp-server</artifactId> </dependency> <dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-starter-mcp-server-webmvc</artifactId> </dependency> <dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-starter-mcp-server-webflux</artifactId> </dependency> 
其中mcp-server提供了webmvc及webflux两个版本

示例

pom.xml

 <dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-starter-mcp-server-webmvc</artifactId> </dependency> 

config

 @Bean public ToolCallbackProvider weatherTools(WeatherService weatherService) { return MethodToolCallbackProvider.builder().toolObjects(weatherService).build(); } @Bean public ToolCallback toUpperCase() { return FunctionToolCallback.builder("toUpperCase", (TextInput input) -> input.input().toUpperCase()) .inputType(TextInput.class) .description("Put the text to upper case") .build(); } 

weatherService

@Service public class WeatherService { private static final String BASE_URL = "https://api.weather.gov"; private final RestClient restClient; public WeatherService() { this.restClient = RestClient.builder() .baseUrl(BASE_URL) .defaultHeader("Accept", "application/geo+json") .defaultHeader("User-Agent", "WeatherApiClient/1.0 ([email protected])") .build(); } @JsonIgnoreProperties(ignoreUnknown = true) public record Points(@JsonProperty("properties") Props properties) { @JsonIgnoreProperties(ignoreUnknown = true) public record Props(@JsonProperty("forecast") String forecast) { } } @JsonIgnoreProperties(ignoreUnknown = true) public record Forecast(@JsonProperty("properties") Props properties) { @JsonIgnoreProperties(ignoreUnknown = true) public record Props(@JsonProperty("periods") List<Period> periods) { } @JsonIgnoreProperties(ignoreUnknown = true) public record Period(@JsonProperty("number") Integer number, @JsonProperty("name") String name, @JsonProperty("startTime") String startTime, @JsonProperty("endTime") String endTime, @JsonProperty("isDaytime") Boolean isDayTime, @JsonProperty("temperature") Integer temperature, @JsonProperty("temperatureUnit") String temperatureUnit, @JsonProperty("temperatureTrend") String temperatureTrend, @JsonProperty("probabilityOfPrecipitation") Map probabilityOfPrecipitation, @JsonProperty("windSpeed") String windSpeed, @JsonProperty("windDirection") String windDirection, @JsonProperty("icon") String icon, @JsonProperty("shortForecast") String shortForecast, @JsonProperty("detailedForecast") String detailedForecast) { } } @JsonIgnoreProperties(ignoreUnknown = true) public record Alert(@JsonProperty("features") List<Feature> features) { @JsonIgnoreProperties(ignoreUnknown = true) public record Feature(@JsonProperty("properties") Properties properties) { } @JsonIgnoreProperties(ignoreUnknown = true) public record Properties(@JsonProperty("event") String event, @JsonProperty("areaDesc") String areaDesc, @JsonProperty("severity") String severity, @JsonProperty("description") String description, @JsonProperty("instruction") String instruction) { } } /** * Get forecast for a specific latitude/longitude * @param latitude Latitude * @param longitude Longitude * @return The forecast for the given location * @throws RestClientException if the request fails */ @Tool(description = "Get weather forecast for a specific latitude/longitude") public String getWeatherForecastByLocation(double latitude, double longitude) { var points = restClient.get() .uri("/points/{latitude},{longitude}", latitude, longitude) .retrieve() .body(Points.class); var forecast = restClient.get().uri(points.properties().forecast()).retrieve().body(Forecast.class); String forecastText = forecast.properties().periods().stream().map(p -> { return String.format(""" %s: Temperature: %s %s Wind: %s %s Forecast: %s """, p.name(), p.temperature(), p.temperatureUnit(), p.windSpeed(), p.windDirection(), p.detailedForecast()); }).collect(Collectors.joining()); return forecastText; } /** * Get alerts for a specific area * @param state Area code. Two-letter US state code (e.g. CA, NY) * @return Human readable alert information * @throws RestClientException if the request fails */ @Tool(description = "Get weather alerts for a US state. Input is Two-letter US state code (e.g. CA, NY)") public String getAlerts(String state) { Alert alert = restClient.get().uri("/alerts/active/area/{state}", state).retrieve().body(Alert.class); return alert.features() .stream() .map(f -> String.format(""" Event: %s Area: %s Severity: %s Description: %s Instructions: %s """, f.properties().event(), f.properties.areaDesc(), f.properties.severity(), f.properties.description(), f.properties.instruction())) .collect(Collectors.joining("\n")); } public static void main(String[] args) { WeatherService client = new WeatherService(); System.out.println(client.getWeatherForecastByLocation(47.6062, -122.3321)); System.out.println(client.getAlerts("NY")); } } 
WeatherService使用RestClient去请求api.weather.gov,它使用@Tool注解了getWeatherForecastByLocation、getAlerts方法

client调用

public class SampleClient { public static void main(String[] args) { var transport = new HttpClientSseClientTransport("http://localhost:8080"); var client = McpClient.sync(transport).build(); client.initialize(); client.ping(); // List and demonstrate tools ListToolsResult toolsList = client.listTools(); System.out.println("Available Tools = " + toolsList); CallToolResult weatherForcastResult = client.callTool(new CallToolRequest("getWeatherForecastByLocation", Map.of("latitude", "47.6062", "longitude", "-122.3321"))); System.out.println("Weather Forcast: " + weatherForcastResult); CallToolResult alertResult = client.callTool(new CallToolRequest("getAlerts", Map.of("state", "NY"))); System.out.println("Alert Response = " + alertResult); client.closeGracefully(); } } 
这里构建了HttpClientSseClientTransport,然后通过McpClient.sync(transport).build()创建了McpSyncClient;示例先调用listTools查看有哪些tool,之后构建CallToolRequest去请求getWeatherForecastByLocation、getAlerts方法。

输出示例:

16:18:34.707 [HttpClient-1-Worker-0] INFO io.modelcontextprotocol.client.McpAsyncClient -- Server response with Protocol: 2024-11-05, Capabilities: ServerCapabilities[experimental=null, logging=LoggingCapabilities[], prompts=null, resources=null, tools=ToolCapabilities[listChanged=true]], Info: Implementation[name=my-weather-server, version=0.0.1] and Instructions null Available Tools = ListToolsResult[tools=[Tool[name=toUpperCase, description=Put the text to upper case, inputSchema=JsonSchema[type=object, properties={input={type=string}}, required=[input], additionalProperties=false]], Tool[name=getAlerts, description=Get weather alerts for a US state. Input is Two-letter US state code (e.g. CA, NY), inputSchema=JsonSchema[type=object, properties={state={type=string}}, required=[state], additionalProperties=false]], Tool[name=getWeatherForecastByLocation, description=Get weather forecast for a specific latitude/longitude, inputSchema=JsonSchema[type=object, properties={latitude={type=number, format=double}, longitude={type=number, format=double}}, required=[latitude, longitude], additionalProperties=false]]], nextCursor=null] Weather Forcast: CallToolResult[content=[TextContent[audience=null, priority=null, text="Overnight:\nTemperature: 50 F\nWind: 5 mph N\nForecast: Mostly cloudy, with a low around 50. North wind around 5 mph.\nWednesday:\nTemperature: 70 F\nWind: 2 to 7 mph NW\nForecast: A chance of rain showers between 9am and 5pm, then showers and thunderstorms. Some of the storms could be severe. Mostly cloudy. High near 70, with temperatures falling to around 68 in the afternoon. Northwest wind 2 to 7 mph. Chance of precipitation is 100%. New rainfall amounts between a quarter and half of an inch possible.\nWednesday Night:\nTemperature: 48 F\nWind: 7 to 10 mph SSW\nForecast: Showers and thunderstorms before 5am, then rain. Some of the storms could be severe. Cloudy. Low around 48, with temperatures rising to around 50 overnight. South southwest wind 7 to 10 mph, with gusts as high as 21 mph. Chance of precipitation is 100%. New rainfall amounts between a quarter and half of an inch possible.\nThursday:\nTemperature: 59 F\nWind: 9 mph S\nForecast: Rain. Cloudy, with a high near 59. South wind around 9 mph, with gusts as high as 21 mph. Chance of precipitation is 90%. New rainfall amounts between a tenth and quarter of an inch possible.\nThursday Night:\nTemperature: 47 F\nWind: 9 mph S\nForecast: Rain. Cloudy, with a low around 47. South wind around 9 mph, with gusts as high as 22 mph. Chance of precipitation is 90%. New rainfall amounts between a quarter and half of an inch possible.\nFriday:\nTemperature: 55 F\nWind: 9 to 13 mph S\nForecast: Rain. Cloudy, with a high near 55. Chance of precipitation is 100%. New rainfall amounts between a tenth and quarter of an inch possible.\nFriday Night:\nTemperature: 44 F\nWind: 6 to 10 mph S\nForecast: Rain. Mostly cloudy, with a low around 44. Chance of precipitation is 80%.\nSaturday:\nTemperature: 55 F\nWind: 7 mph S\nForecast: Rain likely. Partly sunny, with a high near 55.\nSaturday Night:\nTemperature: 41 F\nWind: 2 to 6 mph SE\nForecast: A chance of rain before 11pm. Mostly cloudy, with a low around 41.\nSunday:\nTemperature: 59 F\nWind: 2 to 6 mph NNE\nForecast: A chance of rain after 11am. Mostly cloudy, with a high near 59.\nSunday Night:\nTemperature: 45 F\nWind: 6 mph ESE\nForecast: Rain likely. Mostly cloudy, with a low around 45.\nMonday:\nTemperature: 54 F\nWind: 5 to 8 mph S\nForecast: Rain. Mostly cloudy, with a high near 54.\nMonday Night:\nTemperature: 42 F\nWind: 5 to 8 mph S\nForecast: Rain likely. Mostly cloudy, with a low around 42.\nTuesday:\nTemperature: 54 F\nWind: 7 mph SSW\nForecast: Rain likely. Mostly cloudy, with a high near 54.\n"]], isError=false] Alert Response = CallToolResult[content=[TextContent[audience=null, priority=null,]], isError=false] 

源码

McpSchema

io/modelcontextprotocol/spec/McpSchema.java

public final class McpSchema { private static final Logger logger = LoggerFactory.getLogger(McpSchema.class); private McpSchema() { } public static final String LATEST_PROTOCOL_VERSION = "2024-11-05"; public static final String JSONRPC_VERSION = "2.0"; // --------------------------- // Method Names // --------------------------- // Lifecycle Methods public static final String METHOD_INITIALIZE = "initialize"; public static final String METHOD_NOTIFICATION_INITIALIZED = "notifications/initialized"; public static final String METHOD_PING = "ping"; // Tool Methods public static final String METHOD_TOOLS_LIST = "tools/list"; public static final String METHOD_TOOLS_CALL = "tools/call"; public static final String METHOD_NOTIFICATION_TOOLS_LIST_CHANGED = "notifications/tools/list_changed"; // Resources Methods public static final String METHOD_RESOURCES_LIST = "resources/list"; public static final String METHOD_RESOURCES_READ = "resources/read"; public static final String METHOD_NOTIFICATION_RESOURCES_LIST_CHANGED = "notifications/resources/list_changed"; public static final String METHOD_RESOURCES_TEMPLATES_LIST = "resources/templates/list"; public static final String METHOD_RESOURCES_SUBSCRIBE = "resources/subscribe"; public static final String METHOD_RESOURCES_UNSUBSCRIBE = "resources/unsubscribe"; // Prompt Methods public static final String METHOD_PROMPT_LIST = "prompts/list"; public static final String METHOD_PROMPT_GET = "prompts/get"; public static final String METHOD_NOTIFICATION_PROMPTS_LIST_CHANGED = "notifications/prompts/list_changed"; // Logging Methods public static final String METHOD_LOGGING_SET_LEVEL = "logging/setLevel"; public static final String METHOD_NOTIFICATION_MESSAGE = "notifications/message"; // Roots Methods public static final String METHOD_ROOTS_LIST = "roots/list"; public static final String METHOD_NOTIFICATION_ROOTS_LIST_CHANGED = "notifications/roots/list_changed"; // Sampling Methods public static final String METHOD_SAMPLING_CREATE_MESSAGE = "sampling/createMessage"; private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); //...... } 
McpSchema定义了Lifecycle方法(initialize,notifications/initialized,ping),Tool方法(tools/list,tools/call,notifications/tools/list_changed),Resources方法(resources/list,resources/read,notifications/resources/list_changed,resources/templates/list,resources/subscribe,resources/unsubscribe),Prompt方法(prompts/list,prompts/get,notifications/prompts/list_changed);Logging方法(logging/setLevel,notifications/message),Roots方法(roots/list,notifications/roots/list_changed),Sampling方法(sampling/createMessage)

McpSyncClient

io/modelcontextprotocol/client/McpSyncClient.java

public class McpSyncClient implements AutoCloseable { private static final Logger logger = LoggerFactory.getLogger(McpSyncClient.class); // TODO: Consider providing a client config to set this properly // this is currently a concern only because AutoCloseable is used - perhaps it // is not a requirement? private static final long DEFAULT_CLOSE_TIMEOUT_MS = 10_000L; private final McpAsyncClient delegate; /** * Create a new McpSyncClient with the given delegate. * @param delegate the asynchronous kernel on top of which this synchronous client * provides a blocking API. * @deprecated This method will be removed in 0.9.0. Use * {@link McpClient#sync(McpClientTransport)} to obtain an instance. */ @Deprecated // TODO make the constructor package private post-deprecation public McpSyncClient(McpAsyncClient delegate) { Assert.notNull(delegate, "The delegate can not be null"); this.delegate = delegate; } /** * Get the server capabilities that define the supported features and functionality. * @return The server capabilities */ public McpSchema.ServerCapabilities getServerCapabilities() { return this.delegate.getServerCapabilities(); } /** * Get the server implementation information. * @return The server implementation details */ public McpSchema.Implementation getServerInfo() { return this.delegate.getServerInfo(); } /** * Get the client capabilities that define the supported features and functionality. * @return The client capabilities */ public ClientCapabilities getClientCapabilities() { return this.delegate.getClientCapabilities(); } /** * Get the client implementation information. * @return The client implementation details */ public McpSchema.Implementation getClientInfo() { return this.delegate.getClientInfo(); } @Override public void close() { this.delegate.close(); } public boolean closeGracefully() { try { this.delegate.closeGracefully().block(Duration.ofMillis(DEFAULT_CLOSE_TIMEOUT_MS)); } catch (RuntimeException e) { logger.warn("Client didn't close within timeout of {} ms.", DEFAULT_CLOSE_TIMEOUT_MS, e); return false; } return true; } /** * The initialization phase MUST be the first interaction between client and server. * During this phase, the client and server: * <ul> * <li>Establish protocol version compatibility</li> * <li>Exchange and negotiate capabilities</li> * <li>Share implementation details</li> * </ul> * <br/> * The client MUST initiate this phase by sending an initialize request containing: * <ul> * <li>The protocol version the client supports</li> * <li>The client's capabilities</li> * <li>Client implementation information</li> * </ul> * * The server MUST respond with its own capabilities and information: * {@link McpSchema.ServerCapabilities}. <br/> * After successful initialization, the client MUST send an initialized notification * to indicate it is ready to begin normal operations. * * <br/> * * <a href= * "https://github.com/modelcontextprotocol/specification/blob/main/docs/specification/basic/lifecycle.md#initialization">Initialization * Spec</a> * @return the initialize result. */ public McpSchema.InitializeResult initialize() { // TODO: block takes no argument here as we assume the async client is // configured with a requestTimeout at all times return this.delegate.initialize().block(); } /** * Send a roots/list_changed notification. */ public void rootsListChangedNotification() { this.delegate.rootsListChangedNotification().block(); } /** * Add a roots dynamically. */ public void addRoot(McpSchema.Root root) { this.delegate.addRoot(root).block(); } /** * Remove a root dynamically. */ public void removeRoot(String rootUri) { this.delegate.removeRoot(rootUri).block(); } /** * Send a synchronous ping request. * @return */ public Object ping() { return this.delegate.ping().block(); } // -------------------------- // Tools // -------------------------- /** * Calls a tool provided by the server. Tools enable servers to expose executable * functionality that can interact with external systems, perform computations, and * take actions in the real world. * @param callToolRequest The request containing: - name: The name of the tool to call * (must match a tool name from tools/list) - arguments: Arguments that conform to the * tool's input schema * @return The tool execution result containing: - content: List of content items * (text, images, or embedded resources) representing the tool's output - isError: * Boolean indicating if the execution failed (true) or succeeded (false/absent) */ public McpSchema.CallToolResult callTool(McpSchema.CallToolRequest callToolRequest) { return this.delegate.callTool(callToolRequest).block(); } /** * Retrieves the list of all tools provided by the server. * @return The list of tools result containing: - tools: List of available tools, each * with a name, description, and input schema - nextCursor: Optional cursor for * pagination if more tools are available */ public McpSchema.ListToolsResult listTools() { return this.delegate.listTools().block(); } /** * Retrieves a paginated list of tools provided by the server. * @param cursor Optional pagination cursor from a previous list request * @return The list of tools result containing: - tools: List of available tools, each * with a name, description, and input schema - nextCursor: Optional cursor for * pagination if more tools are available */ public McpSchema.ListToolsResult listTools(String cursor) { return this.delegate.listTools(cursor).block(); } // -------------------------- // Resources // -------------------------- /** * Send a resources/list request. * @param cursor the cursor * @return the list of resources result. */ public McpSchema.ListResourcesResult listResources(String cursor) { return this.delegate.listResources(cursor).block(); } /** * Send a resources/list request. * @return the list of resources result. */ public McpSchema.ListResourcesResult listResources() { return this.delegate.listResources().block(); } /** * Send a resources/read request. * @param resource the resource to read * @return the resource content. */ public McpSchema.ReadResourceResult readResource(McpSchema.Resource resource) { return this.delegate.readResource(resource).block(); } /** * Send a resources/read request. * @param readResourceRequest the read resource request. * @return the resource content. */ public McpSchema.ReadResourceResult readResource(McpSchema.ReadResourceRequest readResourceRequest) { return this.delegate.readResource(readResourceRequest).block(); } /** * Resource templates allow servers to expose parameterized resources using URI * templates. Arguments may be auto-completed through the completion API. * * Request a list of resource templates the server has. * @param cursor the cursor * @return the list of resource templates result. */ public McpSchema.ListResourceTemplatesResult listResourceTemplates(String cursor) { return this.delegate.listResourceTemplates(cursor).block(); } /** * Request a list of resource templates the server has. * @return the list of resource templates result. */ public McpSchema.ListResourceTemplatesResult listResourceTemplates() { return this.delegate.listResourceTemplates().block(); } /** * Subscriptions. The protocol supports optional subscriptions to resource changes. * Clients can subscribe to specific resources and receive notifications when they * change. * * Send a resources/subscribe request. * @param subscribeRequest the subscribe request contains the uri of the resource to * subscribe to. */ public void subscribeResource(McpSchema.SubscribeRequest subscribeRequest) { this.delegate.subscribeResource(subscribeRequest).block(); } /** * Send a resources/unsubscribe request. * @param unsubscribeRequest the unsubscribe request contains the uri of the resource * to unsubscribe from. */ public void unsubscribeResource(McpSchema.UnsubscribeRequest unsubscribeRequest) { this.delegate.unsubscribeResource(unsubscribeRequest).block(); } // -------------------------- // Prompts // -------------------------- public ListPromptsResult listPrompts(String cursor) { return this.delegate.listPrompts(cursor).block(); } public ListPromptsResult listPrompts() { return this.delegate.listPrompts().block(); } public GetPromptResult getPrompt(GetPromptRequest getPromptRequest) { return this.delegate.getPrompt(getPromptRequest).block(); } /** * Client can set the minimum logging level it wants to receive from the server. * @param loggingLevel the min logging level */ public void setLoggingLevel(McpSchema.LoggingLevel loggingLevel) { this.delegate.setLoggingLevel(loggingLevel).block(); } } 
McpSyncClient实现了AutoCloseable接口,它主要是使用McpAsyncClient进行了包装,包装为同步方法;它提供了getServerCapabilities、getServerInfo、getClientCapabilities、getClientInfo、initialize、rootsListChangedNotification、addRoot、removeRoot、ping、callTool、listTools、listResources、readResource、listResourceTemplates、subscribeResource、unsubscribeResource、listPrompts、getPrompt、setLoggingLevel

McpServerAutoConfiguration

org/springframework/ai/mcp/server/autoconfigure/McpServerAutoConfiguration.java

@AutoConfiguration(after = { McpWebMvcServerAutoConfiguration.class, McpWebFluxServerAutoConfiguration.class }) @ConditionalOnClass({ McpSchema.class, McpSyncServer.class }) @EnableConfigurationProperties(McpServerProperties.class) @Import(McpBackwardCompatibility.class) @ConditionalOnProperty(prefix = McpServerProperties.CONFIG_PREFIX, name = "enabled", havingValue = "true", matchIfMissing = true) public class McpServerAutoConfiguration { private static final LogAccessor logger = new LogAccessor(McpServerAutoConfiguration.class); @Bean @ConditionalOnMissingBean public McpServerTransportProvider stdioServerTransport() { return new StdioServerTransportProvider(); } @Bean @ConditionalOnMissingBean public McpSchema.ServerCapabilities.Builder capabilitiesBuilder() { return McpSchema.ServerCapabilities.builder(); } @Bean @ConditionalOnProperty(prefix = McpServerProperties.CONFIG_PREFIX, name = "type", havingValue = "SYNC", matchIfMissing = true) public List<McpServerFeatures.SyncToolSpecification> syncTools(ObjectProvider<List<ToolCallback>> toolCalls, List<ToolCallback> toolCallbacksList, McpServerProperties serverProperties) { List<ToolCallback> tools = new ArrayList<>(toolCalls.stream().flatMap(List::stream).toList()); if (!CollectionUtils.isEmpty(toolCallbacksList)) { tools.addAll(toolCallbacksList); } return this.toSyncToolSpecifications(tools, serverProperties); } private List<McpServerFeatures.SyncToolSpecification> toSyncToolSpecifications(List<ToolCallback> tools, McpServerProperties serverProperties) { return tools.stream().map(tool -> { String toolName = tool.getToolDefinition().name(); MimeType mimeType = (serverProperties.getToolResponseMimeType().containsKey(toolName)) ? MimeType.valueOf(serverProperties.getToolResponseMimeType().get(toolName)) : null; return McpToolUtils.toSyncToolSpecification(tool, mimeType); }).toList(); } @Bean @ConditionalOnProperty(prefix = McpServerProperties.CONFIG_PREFIX, name = "type", havingValue = "SYNC", matchIfMissing = true) public McpSyncServer mcpSyncServer(McpServerTransportProvider transportProvider, McpSchema.ServerCapabilities.Builder capabilitiesBuilder, McpServerProperties serverProperties, ObjectProvider<List<SyncToolSpecification>> tools, ObjectProvider<List<SyncResourceSpecification>> resources, ObjectProvider<List<SyncPromptSpecification>> prompts, ObjectProvider<BiConsumer<McpSyncServerExchange, List<McpSchema.Root>>> rootsChangeConsumers, List<ToolCallbackProvider> toolCallbackProvider) { McpSchema.Implementation serverInfo = new Implementation(serverProperties.getName(), serverProperties.getVersion()); // Create the server with both tool and resource capabilities SyncSpecification serverBuilder = McpServer.sync(transportProvider).serverInfo(serverInfo); List<SyncToolSpecification> toolSpecifications = new ArrayList<>(tools.stream().flatMap(List::stream).toList()); List<ToolCallback> providerToolCallbacks = toolCallbackProvider.stream() .map(pr -> List.of(pr.getToolCallbacks())) .flatMap(List::stream) .filter(fc -> fc instanceof ToolCallback) .map(fc -> (ToolCallback) fc) .toList(); toolSpecifications.addAll(this.toSyncToolSpecifications(providerToolCallbacks, serverProperties)); if (!CollectionUtils.isEmpty(toolSpecifications)) { serverBuilder.tools(toolSpecifications); capabilitiesBuilder.tools(serverProperties.isToolChangeNotification()); logger.info("Registered tools: " + toolSpecifications.size() + ", notification: " + serverProperties.isToolChangeNotification()); } List<SyncResourceSpecification> resourceSpecifications = resources.stream().flatMap(List::stream).toList(); if (!CollectionUtils.isEmpty(resourceSpecifications)) { serverBuilder.resources(resourceSpecifications); capabilitiesBuilder.resources(false, serverProperties.isResourceChangeNotification()); logger.info("Registered resources: " + resourceSpecifications.size() + ", notification: " + serverProperties.isResourceChangeNotification()); } List<SyncPromptSpecification> promptSpecifications = prompts.stream().flatMap(List::stream).toList(); if (!CollectionUtils.isEmpty(promptSpecifications)) { serverBuilder.prompts(promptSpecifications); capabilitiesBuilder.prompts(serverProperties.isPromptChangeNotification()); logger.info("Registered prompts: " + promptSpecifications.size() + ", notification: " + serverProperties.isPromptChangeNotification()); } rootsChangeConsumers.ifAvailable(consumer -> { serverBuilder.rootsChangeHandler((exchange, roots) -> { consumer.accept(exchange, roots); }); logger.info("Registered roots change consumer"); }); serverBuilder.capabilities(capabilitiesBuilder.build()); return serverBuilder.build(); } @Bean @ConditionalOnProperty(prefix = McpServerProperties.CONFIG_PREFIX, name = "type", havingValue = "ASYNC") public List<McpServerFeatures.AsyncToolSpecification> asyncTools(ObjectProvider<List<ToolCallback>> toolCalls, List<ToolCallback> toolCallbackList, McpServerProperties serverProperties) { List<ToolCallback> tools = new ArrayList<>(toolCalls.stream().flatMap(List::stream).toList()); if (!CollectionUtils.isEmpty(toolCallbackList)) { tools.addAll(toolCallbackList); } return this.toAsyncToolSpecification(tools, serverProperties); } private List<McpServerFeatures.AsyncToolSpecification> toAsyncToolSpecification(List<ToolCallback> tools, McpServerProperties serverProperties) { return tools.stream().map(tool -> { String toolName = tool.getToolDefinition().name(); MimeType mimeType = (serverProperties.getToolResponseMimeType().containsKey(toolName)) ? MimeType.valueOf(serverProperties.getToolResponseMimeType().get(toolName)) : null; return McpToolUtils.toAsyncToolSpecification(tool, mimeType); }).toList(); } @Bean @ConditionalOnProperty(prefix = McpServerProperties.CONFIG_PREFIX, name = "type", havingValue = "ASYNC") public McpAsyncServer mcpAsyncServer(McpServerTransportProvider transportProvider, McpSchema.ServerCapabilities.Builder capabilitiesBuilder, McpServerProperties serverProperties, ObjectProvider<List<AsyncToolSpecification>> tools, ObjectProvider<List<AsyncResourceSpecification>> resources, ObjectProvider<List<AsyncPromptSpecification>> prompts, ObjectProvider<BiConsumer<McpAsyncServerExchange, List<McpSchema.Root>>> rootsChangeConsumer, List<ToolCallbackProvider> toolCallbackProvider) { McpSchema.Implementation serverInfo = new Implementation(serverProperties.getName(), serverProperties.getVersion()); // Create the server with both tool and resource capabilities AsyncSpecification serverBuilder = McpServer.async(transportProvider).serverInfo(serverInfo); List<AsyncToolSpecification> toolSpecifications = new ArrayList<>( tools.stream().flatMap(List::stream).toList()); List<ToolCallback> providerToolCallbacks = toolCallbackProvider.stream() .map(pr -> List.of(pr.getToolCallbacks())) .flatMap(List::stream) .filter(fc -> fc instanceof ToolCallback) .map(fc -> (ToolCallback) fc) .toList(); toolSpecifications.addAll(this.toAsyncToolSpecification(providerToolCallbacks, serverProperties)); if (!CollectionUtils.isEmpty(toolSpecifications)) { serverBuilder.tools(toolSpecifications); capabilitiesBuilder.tools(serverProperties.isToolChangeNotification()); logger.info("Registered tools: " + toolSpecifications.size() + ", notification: " + serverProperties.isToolChangeNotification()); } List<AsyncResourceSpecification> resourceSpecifications = resources.stream().flatMap(List::stream).toList(); if (!CollectionUtils.isEmpty(resourceSpecifications)) { serverBuilder.resources(resourceSpecifications); capabilitiesBuilder.resources(false, serverProperties.isResourceChangeNotification()); logger.info("Registered resources: " + resourceSpecifications.size() + ", notification: " + serverProperties.isResourceChangeNotification()); } List<AsyncPromptSpecification> promptSpecifications = prompts.stream().flatMap(List::stream).toList(); if (!CollectionUtils.isEmpty(promptSpecifications)) { serverBuilder.prompts(promptSpecifications); capabilitiesBuilder.prompts(serverProperties.isPromptChangeNotification()); logger.info("Registered prompts: " + promptSpecifications.size() + ", notification: " + serverProperties.isPromptChangeNotification()); } rootsChangeConsumer.ifAvailable(consumer -> { BiFunction<McpAsyncServerExchange, List<McpSchema.Root>, Mono<Void>> asyncConsumer = (exchange, roots) -> { consumer.accept(exchange, roots); return Mono.empty(); }; serverBuilder.rootsChangeHandler(asyncConsumer); logger.info("Registered roots change consumer"); }); serverBuilder.capabilities(capabilitiesBuilder.build()); return serverBuilder.build(); } } 
McpServerAutoConfiguration主要是提供McpSyncServer亦或是McpAsyncServer,它依赖于McpServerTransportProvider、McpSchema.ServerCapabilities.Builder、McpServerProperties、List<SyncToolSpecification>List<SyncResourceSpecification>List<SyncPromptSpecification>BiConsumer<McpSyncServerExchange, List<McpSchema.Root>>List<ToolCallbackProvider>;McpSyncServer内部是包装McpAsyncServer来实现的。

McpWebMvcServerAutoConfiguration

org/springframework/ai/mcp/server/autoconfigure/McpWebMvcServerAutoConfiguration.java

@AutoConfiguration @ConditionalOnClass({ WebMvcSseServerTransportProvider.class }) @ConditionalOnMissingBean(McpServerTransportProvider.class) @ConditionalOnProperty(prefix = McpServerProperties.CONFIG_PREFIX, name = "stdio", havingValue = "false", matchIfMissing = true) public class McpWebMvcServerAutoConfiguration { @Bean @ConditionalOnMissingBean public WebMvcSseServerTransportProvider webMvcSseServerTransportProvider(ObjectMapper objectMapper, McpServerProperties serverProperties) { return new WebMvcSseServerTransportProvider(objectMapper, serverProperties.getSseMessageEndpoint()); } @Bean public RouterFunction<ServerResponse> mvcMcpRouterFunction(WebMvcSseServerTransportProvider transportProvider) { return transportProvider.getRouterFunction(); } } 
McpWebMvcServerAutoConfiguration自动配置了WebMvcSseServerTransportProvider、mvcMcpRouterFunction

小结

mcp提供了java sdk,同时还提供了spring webflux及mvc的sse实现;Spring AI MCP扩展了mcp java sdk,提供了spring boot的集成,其中mcp-server提供了webmvc及webflux两个版本。
McpAsyncServer内部实现了mcp协议定义的方法的requestHandlers,之后创建McpServerSession.Factory,设置给McpServerTransportProvider,与spring提供的WebMvcSseServerTransportProvider衔接起来。WebMvcSseServerTransportProvider通过McpServerSession.Factory来集成mcp,其构造器定义了routerFunction(注册到spring中),其handleSseConnection方法主要是构建McpServerSession,其handleMessage方法获取McpServerSession,执行其handle方法。

doc

Read more

Agent-Reach:给你的AI装上天眼,零API费用看透整个互联网

Agent-Reach:给你的AI装上天眼,零API费用看透整个互联网

Agent-Reach:给你的AI装上"天眼",零API费用看透整个互联网 当你的AI Agent还在为无法读取最新的技术推文而报错,或者被昂贵的官方API账单拒之门外时,Agent-Reach已经悄悄用你的本地Cookie,帮它把Reddit的热帖和B站的视频字幕完整扒了下来。 一、一场打破"AI断网"魔咒的开源风暴 2026年的科技圈,AI Agent 已经能帮你写代码、改文档、管项目——但你一旦让它去网上找点"活生生"的东西,它就瞬间变成了"瞎子": * 📺 “帮我看看这个YouTube教程讲了什么” → 看不了,拿不到字幕。 * 🐦 “帮我搜一下推特上大家怎么评价这个产品” → 搜不了,Twitter API太贵了。 * 📕 “帮我看看小红书上这个品的口碑” → 打不开,必须登录才能看。 直到GitHub上出现了一个名为 Agent-Reach 的开源项目。它以一种极客且纯粹的方式,彻底终结了AI的"断网时代"

By Ne0inhk

让“小爱音箱PRO”智能起来:接入豆包AI

下面提供从 0 到 1 的完整流程,包含豆包 API 申请、MiGPT 部署、配置与使用,确保你能顺利让小爱音箱 Pro 用上豆包的强大能力。 一、准备工作(必做) 二、申请豆包 API 密钥(关键步骤) 豆包 API 需通过火山引擎方舟平台申请,以下是详细步骤: 1. 注册与实名认证 1. 访问火山引擎官网:https://www.volcengine.com/ 2. 注册账号并完成实名认证(个人 / 企业均可) 3. 登录后进入方舟大模型平台 2. 创建 API 密钥与推理接入点 1. 进入控制台 → API 密钥管理 → 点击 "

By Ne0inhk
医疗AI场景下算法编程的深度解析(2026新生培训讲稿)(五)

医疗AI场景下算法编程的深度解析(2026新生培训讲稿)(五)

第9章 朴素贝叶斯算法:医学文本分类利器 在医疗信息化进程中,非结构化的文本数据占据了医疗信息的很大比重——从电子病历的病程记录、出院小结,到医学文献、临床指南,乃至患者的在线咨询记录。如何从这些海量文本中高效、准确地提取关键信息,是医疗AI面临的重要挑战。朴素贝叶斯(Naïve Bayes)算法以其简单、高效、对高维稀疏数据适应性强的特点,成为医学文本分类任务中的经典利器。本章将从算法原理出发,深入解析朴素贝叶斯在医疗场景中的应用,并通过实战案例展示从文本预处理、特征提取到模型训练与评估的完整流程。 9.1 算法原理 朴素贝叶斯是一系列基于贝叶斯定理的分类算法,其核心是“朴素”的条件独立性假设——在给定类别的情况下,各个特征之间相互独立。尽管这一假设在现实中很少完全成立,但朴素贝叶斯在众多实际任务中仍表现出色,尤其是在文本分类领域。 9.1.1 贝叶斯定理 贝叶斯定理描述了在已知某些条件下,事件发生的概率如何更新。对于分类问题,我们希望计算给定样本 (x) 的条件下,其属于类别 (C_k) 的概率,

By Ne0inhk
打造你的家庭 AI 助手(三):QQ 机器人接入你的 OpenClaw

打造你的家庭 AI 助手(三):QQ 机器人接入你的 OpenClaw

不得不承认腾讯进步的速度太快了,几条命令就可以接入Openclaw,也不用设置IP白名单了,在 QQ开放平台还增加了专门的Openclaw入口: 没啥好说的,很简单,安装完Openclaw之后,执行如下命令(命令也是生成好的): openclaw plugins install @tencent-connect/openclaw-qqbot@latest openclaw channels add--channel qqbot --token"" openclaw gateway restart 以下内容已经过时了,留作纪念 以下内容已经过时了,留作纪念 以下内容已经过时了,留作纪念 ⚠️ 重要提示:如果是家用宽带,没有申请固定 IP 地址的话,大可以放弃这种方式。由于 QQ 开发平台的白名单限制,机器人会非常不稳定,频繁掉线。建议使用云服务器或有固定 IP 的环境部署。 前言 在完成 OpenClaw 安装后,

By Ne0inhk