Skip to content

feat: Support separate endpoint path router in WebFluxStreamableServerTransportProvider #621

@lihuagang03

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.

public class WebFluxStreamableServerTransportProvider implements McpStreamableServerTransportProvider {

	private final String mcpEndpoint;

	private final RouterFunction<?> routerFunction;

	private WebFluxStreamableServerTransportProvider(ObjectMapper objectMapper, String mcpEndpoint,
			McpTransportContextExtractor<ServerRequest> contextExtractor, boolean disallowDelete,
			Duration keepAliveInterval) {
		
		this.mcpEndpoint = mcpEndpoint;
		
		this.routerFunction = RouterFunctions.route()
			.GET(this.mcpEndpoint, this::handleGet)
			.POST(this.mcpEndpoint, this::handlePost)
			.DELETE(this.mcpEndpoint, this::handleDelete)
			.build();
	}

	public RouterFunction<?> getRouterFunction() {
		return this.routerFunction;
	}

}
public abstract class RouterFunctions {

	public static Builder route() {
		return new RouterFunctionBuilder();
	}

}
class RouterFunctionBuilder implements RouterFunctions.Builder {

	private final List<RouterFunction<ServerResponse>> routerFunctions = new ArrayList<>();


	@Override
	public RouterFunctions.Builder add(RouterFunction<ServerResponse> routerFunction) {
		Assert.notNull(routerFunction, "RouterFunction must not be null");
		this.routerFunctions.add(routerFunction);
		return this;
	}

	@Override
	public RouterFunction<ServerResponse> build() {
		if (this.routerFunctions.isEmpty()) {
			throw new IllegalStateException("No routes registered. Register a route with GET(), POST(), etc.");
		}
		RouterFunction<ServerResponse> result = new BuiltRouterFunction(this.routerFunctions);

		if (this.filterFunctions.isEmpty() && this.errorHandlers.isEmpty()) {
			return result;
		}
		else {
			HandlerFilterFunction<ServerResponse, ServerResponse> filter =
					Stream.concat(this.filterFunctions.stream(), this.errorHandlers.stream())
							.reduce(HandlerFilterFunction::andThen)
							.orElseThrow(IllegalStateException::new);

			return result.filter(filter);
		}
	}


	/**
	 * Router function returned by {@link #build()} that simply iterates over the registered routes.
	 */
	private static class BuiltRouterFunction extends RouterFunctions.AbstractRouterFunction<ServerResponse> {

		private final List<RouterFunction<ServerResponse>> routerFunctions;

		public BuiltRouterFunction(List<RouterFunction<ServerResponse>> routerFunctions) {
			Assert.notEmpty(routerFunctions, "RouterFunctions must not be empty");
			this.routerFunctions = new ArrayList<>(routerFunctions);
		}

		@Override
		public Mono<HandlerFunction<ServerResponse>> route(ServerRequest request) {
			return Flux.fromIterable(this.routerFunctions)
					.concatMap(routerFunction -> routerFunction.route(request))
					.next();
		}

		@Override
		public void accept(RouterFunctions.Visitor visitor) {
			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)
public class McpServerConfiguration {

    public McpServerConfiguration() {
        log.info("create McpServerConfiguration");
    }

    @Bean
    public Map<String, List<McpTool>> mcpToolListMap() {
        List<String> yamlFiles = List.of(
                "mcp-server-user-apis.yml",
                "mcp-server-travel-apis.yml"
        );

        return yamlFiles.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 })
    public Map<String, WebFluxStreamableServerTransportProvider> transportProviderMap(
            Map<String, List<McpTool>> mcpToolListMap) {
        log.info("init transportProviderMap");

        Map<String, WebFluxStreamableServerTransportProvider> transportProviderMap =
                new ConcurrentHashMap<>(mcpToolListMap.size());
        transportProviderMap.putAll(McpServerTransportManager.transportProviderMap(mcpToolListMap.keySet()));
        return transportProviderMap;
    }

    /**
     * @see McpServerAutoConfiguration#capabilitiesBuilder()
     */
    @Bean
    public McpSchema.ServerCapabilities.Builder capabilitiesBuilder() {
        log.info("init capabilitiesBuilder");

        return McpSchema.ServerCapabilities.builder()
                .tools(true);
    }

    @Bean
    @ConditionalOnProperty(prefix = McpServerProperties.CONFIG_PREFIX, name = "type", havingValue = "ASYNC")
    public Map<String, McpAsyncServer> mcpAsyncServerMap(
            Map<String, List<McpTool>> mcpToolListMap,
            Map<String, WebFluxStreamableServerTransportProvider> transportProviderMap,
            McpSchema.ServerCapabilities.Builder capabilitiesBuilder) {
        log.info("init mcpAsyncServerMap");

        return McpServerManager.mcpAsyncServerMap(mcpToolListMap, transportProviderMap, capabilitiesBuilder);
    }

    /**
     * @see McpServerStreamableHttpWebFluxAutoConfiguration#webFluxStreamableServerTransportProvider
     */
    @Bean
    @ConditionalOnProperty(prefix = McpServerProperties.CONFIG_PREFIX, name = "type", havingValue = "ASYNC")
    @Conditional({ McpServerAutoConfiguration.EnabledStreamableServerCondition.class })
    public WebFluxStreamableServerTransportProvider webFluxStreamableServerTransportProvider(
            ObjectProvider<ObjectMapper> objectMapperProvider, McpServerStreamableHttpProperties serverProperties) {
        log.info("init webFluxStreamableServerTransportProvider");

        ObjectMapper objectMapper = objectMapperProvider.getIfAvailable(ObjectMapper::new);

        return WebFluxStreamableServerTransportProvider.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 })
    public RouterFunction<?> webFluxStreamableServerRouterFunction(
            WebFluxStreamableServerTransportProvider webFluxProvider,
            Map<String, WebFluxStreamableServerTransportProvider> transportProviderMap) {
        log.info("init webFluxStreamableServerRouterFunction");

        RouterFunctions.Builder routerFunctionBuilder = RouterFunctions.route();
        routerFunctionBuilder.add((RouterFunction<ServerResponse>) webFluxProvider.getRouterFunction());

        for (WebFluxStreamableServerTransportProvider transportProvider : transportProviderMap.values()) {
            routerFunctionBuilder.add((RouterFunction<ServerResponse>) transportProvider.getRouterFunction());
        }

        return routerFunctionBuilder.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