Skip to content

feat: Support separate endpoint path router in WebFluxStreamableServerTransportProvider #621

Description

@lihuagang03

Please do a quick search on GitHub issues first, the feature you are about to request might have already been requested.
#79
#80
#425
#432

Expected Behavior

The user can customize the endpoint routing functions.
We hope to wrap HTTP-APIs to MCP-Server-Tools.

  • Uses Spring WebFlux's RouterFunction for endpoint handling (GET, POST, DELETE)

We hope to support the follow MCP-Servers in one application process:

  • /mcp
  • /mcp/mcp-server-app-name-A -> some MCP-Tools
  • /mcp/mcp-server-app-name-B -> some MCP-Tools
  • /mcp/mcp-server-app-name-C -> some MCP-Tools

Current Behavior

The RouterFunction is private initialization in WebFluxStreamableServerTransportProvider, and its constructor is private.

publicclassWebFluxStreamableServerTransportProviderimplementsMcpStreamableServerTransportProvider {
privatefinalStringmcpEndpoint;
privatefinalRouterFunction<?> routerFunction;
privateWebFluxStreamableServerTransportProvider(ObjectMapperobjectMapper, StringmcpEndpoint,
McpTransportContextExtractor<ServerRequest> contextExtractor, booleandisallowDelete,
DurationkeepAliveInterval) {
this.mcpEndpoint = mcpEndpoint;
this.routerFunction = RouterFunctions.route()
.GET(this.mcpEndpoint, this::handleGet)
.POST(this.mcpEndpoint, this::handlePost)
.DELETE(this.mcpEndpoint, this::handleDelete)
.build();
}
publicRouterFunction<?> getRouterFunction() {
returnthis.routerFunction;
}
}
publicabstractclassRouterFunctions {
publicstaticBuilderroute() {
returnnewRouterFunctionBuilder();
}
}
classRouterFunctionBuilderimplementsRouterFunctions.Builder {
privatefinalList<RouterFunction<ServerResponse>> routerFunctions = newArrayList<>();
@OverridepublicRouterFunctions.Builderadd(RouterFunction<ServerResponse> routerFunction) {
Assert.notNull(routerFunction, "RouterFunction must not be null");
this.routerFunctions.add(routerFunction);
returnthis;
}
@OverridepublicRouterFunction<ServerResponse> build() {
if (this.routerFunctions.isEmpty()) {
thrownewIllegalStateException("No routes registered. Register a route with GET(), POST(), etc.");
}
RouterFunction<ServerResponse> result = newBuiltRouterFunction(this.routerFunctions);
if (this.filterFunctions.isEmpty() && this.errorHandlers.isEmpty()) {
returnresult;
}
else {
HandlerFilterFunction<ServerResponse, ServerResponse> filter =
Stream.concat(this.filterFunctions.stream(), this.errorHandlers.stream())
.reduce(HandlerFilterFunction::andThen)
.orElseThrow(IllegalStateException::new);
returnresult.filter(filter);
}
}
/** * Router function returned by {@link #build()} that simply iterates over the registered routes. */privatestaticclassBuiltRouterFunctionextendsRouterFunctions.AbstractRouterFunction<ServerResponse> {
privatefinalList<RouterFunction<ServerResponse>> routerFunctions;
publicBuiltRouterFunction(List<RouterFunction<ServerResponse>> routerFunctions) {
Assert.notEmpty(routerFunctions, "RouterFunctions must not be empty");
this.routerFunctions = newArrayList<>(routerFunctions);
}
@OverridepublicMono<HandlerFunction<ServerResponse>> route(ServerRequestrequest) {
returnFlux.fromIterable(this.routerFunctions)
.concatMap(routerFunction -> routerFunction.route(request))
.next();
}
@Overridepublicvoidaccept(RouterFunctions.Visitorvisitor) {
this.routerFunctions.forEach(routerFunction -> routerFunction.accept(visitor));
}
}
}

Context

API is MCP, allowing AI to connect to the real world with lower cost, speed, and security. The existing APIs can be instantly converted into a Remote MCP Server, laying out the shortest connection path between AI and the real world.

We need to start multiple WebFluxStreamableServerTransportProvider, McpAsyncServer instances in one application process. Please to see the follow code in McpServerConfiguration, that is reference to McpServerStreamableHttpWebFluxAutoConfiguration.

It can support the follow MCP-Servers:

  • /mcp
  • /mcp/mcp-server-app-name-A -> some MCP-Tools
  • /mcp/mcp-server-app-name-B -> some MCP-Tools

But the RouterFunction can not dynamic update when the database update for some new app-name MCP-Server.

  • /mcp/mcp-server-app-name-C
@Slf4j@EnableConfigurationProperties({ McpServerStreamableHttpProperties.class })
@Configuration(proxyBeanMethods = false)
publicclassMcpServerConfiguration {
publicMcpServerConfiguration() {
log.info("create McpServerConfiguration");
}
@BeanpublicMap<String, List<McpTool>> mcpToolListMap() {
List<String> yamlFiles = List.of(
"mcp-server-user-apis.yml",
"mcp-server-travel-apis.yml"
);
returnyamlFiles.stream()
.map(YamlUtil::load)
.collect(Collectors.toMap(
mcpServerRule -> mcpServerRule.getServer().getName(),
McpServerRule::getTools
));
}
@Bean@ConditionalOnProperty(prefix = McpServerProperties.CONFIG_PREFIX, name = "type", havingValue = "ASYNC")
@Conditional({ McpServerAutoConfiguration.EnabledStreamableServerCondition.class })
publicMap<String, WebFluxStreamableServerTransportProvider> transportProviderMap(
Map<String, List<McpTool>> mcpToolListMap) {
log.info("init transportProviderMap");
Map<String, WebFluxStreamableServerTransportProvider> transportProviderMap =
newConcurrentHashMap<>(mcpToolListMap.size());
transportProviderMap.putAll(McpServerTransportManager.transportProviderMap(mcpToolListMap.keySet()));
returntransportProviderMap;
}
/** * @see McpServerAutoConfiguration#capabilitiesBuilder() */@BeanpublicMcpSchema.ServerCapabilities.BuildercapabilitiesBuilder() {
log.info("init capabilitiesBuilder");
returnMcpSchema.ServerCapabilities.builder()
.tools(true);
}
@Bean@ConditionalOnProperty(prefix = McpServerProperties.CONFIG_PREFIX, name = "type", havingValue = "ASYNC")
publicMap<String, McpAsyncServer> mcpAsyncServerMap(
Map<String, List<McpTool>> mcpToolListMap,
Map<String, WebFluxStreamableServerTransportProvider> transportProviderMap,
McpSchema.ServerCapabilities.BuildercapabilitiesBuilder) {
log.info("init mcpAsyncServerMap");
returnMcpServerManager.mcpAsyncServerMap(mcpToolListMap, transportProviderMap, capabilitiesBuilder);
}
/** * @see McpServerStreamableHttpWebFluxAutoConfiguration#webFluxStreamableServerTransportProvider */@Bean@ConditionalOnProperty(prefix = McpServerProperties.CONFIG_PREFIX, name = "type", havingValue = "ASYNC")
@Conditional({ McpServerAutoConfiguration.EnabledStreamableServerCondition.class })
publicWebFluxStreamableServerTransportProviderwebFluxStreamableServerTransportProvider(
ObjectProvider<ObjectMapper> objectMapperProvider, McpServerStreamableHttpPropertiesserverProperties) {
log.info("init webFluxStreamableServerTransportProvider");
ObjectMapperobjectMapper = objectMapperProvider.getIfAvailable(ObjectMapper::new);
returnWebFluxStreamableServerTransportProvider.builder()
.objectMapper(objectMapper)
.messageEndpoint(serverProperties.getMcpEndpoint())
.keepAliveInterval(serverProperties.getKeepAliveInterval())
.disallowDelete(serverProperties.isDisallowDelete())
.build();
}
/** * @see McpServerStreamableHttpWebFluxAutoConfiguration#webFluxStreamableServerRouterFunction */// Router function for streamable http transport used by Spring WebFlux to start an// HTTP server.@Bean@ConditionalOnProperty(prefix = McpServerProperties.CONFIG_PREFIX, name = "type", havingValue = "ASYNC")
@Conditional({ McpServerAutoConfiguration.EnabledStreamableServerCondition.class })
publicRouterFunction<?> webFluxStreamableServerRouterFunction(
WebFluxStreamableServerTransportProviderwebFluxProvider,
Map<String, WebFluxStreamableServerTransportProvider> transportProviderMap) {
log.info("init webFluxStreamableServerRouterFunction");
RouterFunctions.BuilderrouterFunctionBuilder = RouterFunctions.route();
routerFunctionBuilder.add((RouterFunction<ServerResponse>) webFluxProvider.getRouterFunction());
for (WebFluxStreamableServerTransportProvidertransportProvider : transportProviderMap.values()) {
routerFunctionBuilder.add((RouterFunction<ServerResponse>) transportProvider.getRouterFunction());
}
returnrouterFunctionBuilder.build();
}
}

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions