diff --git a/auto-configurations/mcp/spring-ai-autoconfigure-mcp-client-common/src/main/java/org/springframework/ai/mcp/client/common/autoconfigure/properties/McpSseClientProperties.java b/auto-configurations/mcp/spring-ai-autoconfigure-mcp-client-common/src/main/java/org/springframework/ai/mcp/client/common/autoconfigure/properties/McpSseClientProperties.java index f23029ddd96..25d1a41c720 100644 --- a/auto-configurations/mcp/spring-ai-autoconfigure-mcp-client-common/src/main/java/org/springframework/ai/mcp/client/common/autoconfigure/properties/McpSseClientProperties.java +++ b/auto-configurations/mcp/spring-ai-autoconfigure-mcp-client-common/src/main/java/org/springframework/ai/mcp/client/common/autoconfigure/properties/McpSseClientProperties.java @@ -29,13 +29,27 @@ * Each connection is configured with a URL endpoint for SSE communication. * *

- * Example configuration:

+ * Example configurations: 
+ * # Simple configuration with default SSE endpoint (/sse)
  * spring.ai.mcp.client.sse:
  *   connections:
  *     server1:
- *       url: http://localhost:8080/events
- *     server2:
- *       url: http://otherserver:8081/events
+ *       url: http://localhost:8080
+ * 
+ * # Custom SSE endpoints - split complex URLs correctly
+ * spring.ai.mcp.client.sse:
+ *   connections:
+ *     mcp-hub:
+ *       url: http://localhost:3000
+ *       sse-endpoint: /mcp-hub/sse/cf9ec4527e3c4a2cbb149a85ea45ab01
+ *     custom-server:
+ *       url: http://api.example.com
+ *       sse-endpoint: /v1/mcp/events?token=abc123&format=json
+ * 
+ * # How to split a full URL:
+ * # Full URL: http://localhost:3000/mcp-hub/sse/token123
+ * # Split as:  url: http://localhost:3000
+ * #           sse-endpoint: /mcp-hub/sse/token123
  * 
* * @author Christian Tzolov diff --git a/auto-configurations/mcp/spring-ai-autoconfigure-mcp-client-common/src/test/java/org/springframework/ai/mcp/client/common/autoconfigure/properties/McpSseClientPropertiesTests.java b/auto-configurations/mcp/spring-ai-autoconfigure-mcp-client-common/src/test/java/org/springframework/ai/mcp/client/common/autoconfigure/properties/McpSseClientPropertiesTests.java index b3c72aa08b3..1bdfdae8e23 100644 --- a/auto-configurations/mcp/spring-ai-autoconfigure-mcp-client-common/src/test/java/org/springframework/ai/mcp/client/common/autoconfigure/properties/McpSseClientPropertiesTests.java +++ b/auto-configurations/mcp/spring-ai-autoconfigure-mcp-client-common/src/test/java/org/springframework/ai/mcp/client/common/autoconfigure/properties/McpSseClientPropertiesTests.java @@ -283,6 +283,20 @@ void specialCharactersInSseEndpoint() { }); } + @Test + void mcpHubStyleUrlWithTokenPath() { + this.contextRunner.withPropertyValues("spring.ai.mcp.client.sse.connections.mcp-hub.url=http://localhost:3000", + "spring.ai.mcp.client.sse.connections.mcp-hub.sse-endpoint=/mcp-hub/sse/cf9ec4527e3c4a2cbb149a85ea45ab01") + .run(context -> { + McpSseClientProperties properties = context.getBean(McpSseClientProperties.class); + assertThat(properties.getConnections()).hasSize(1); + assertThat(properties.getConnections()).containsKey("mcp-hub"); + assertThat(properties.getConnections().get("mcp-hub").url()).isEqualTo("http://localhost:3000"); + assertThat(properties.getConnections().get("mcp-hub").sseEndpoint()) + .isEqualTo("/mcp-hub/sse/cf9ec4527e3c4a2cbb149a85ea45ab01"); + }); + } + @Configuration @EnableConfigurationProperties(McpSseClientProperties.class) static class TestConfiguration { diff --git a/auto-configurations/mcp/spring-ai-autoconfigure-mcp-client-httpclient/src/main/java/org/springframework/ai/mcp/client/httpclient/autoconfigure/SseHttpClientTransportAutoConfiguration.java b/auto-configurations/mcp/spring-ai-autoconfigure-mcp-client-httpclient/src/main/java/org/springframework/ai/mcp/client/httpclient/autoconfigure/SseHttpClientTransportAutoConfiguration.java index 1f713e75e88..05692a809c2 100644 --- a/auto-configurations/mcp/spring-ai-autoconfigure-mcp-client-httpclient/src/main/java/org/springframework/ai/mcp/client/httpclient/autoconfigure/SseHttpClientTransportAutoConfiguration.java +++ b/auto-configurations/mcp/spring-ai-autoconfigure-mcp-client-httpclient/src/main/java/org/springframework/ai/mcp/client/httpclient/autoconfigure/SseHttpClientTransportAutoConfiguration.java @@ -92,16 +92,30 @@ public List sseHttpClientTransports(McpSseClientPropert List sseTransports = new ArrayList<>(); for (Map.Entry serverParameters : sseProperties.getConnections().entrySet()) { + String connectionName = serverParameters.getKey(); + SseParameters params = serverParameters.getValue(); - String baseUrl = serverParameters.getValue().url(); - String sseEndpoint = serverParameters.getValue().sseEndpoint() != null - ? serverParameters.getValue().sseEndpoint() : "/sse"; - var transport = HttpClientSseClientTransport.builder(baseUrl) - .sseEndpoint(sseEndpoint) - .clientBuilder(HttpClient.newBuilder()) - .objectMapper(objectMapper) - .build(); - sseTransports.add(new NamedClientMcpTransport(serverParameters.getKey(), transport)); + String baseUrl = params.url(); + String sseEndpoint = params.sseEndpoint() != null ? params.sseEndpoint() : "/sse"; + + if (baseUrl == null || baseUrl.trim().isEmpty()) { + throw new IllegalArgumentException("SSE connection '" + connectionName + + "' requires a 'url' property. Example: url: http://localhost:3000"); + } + + try { + var transport = HttpClientSseClientTransport.builder(baseUrl) + .sseEndpoint(sseEndpoint) + .clientBuilder(HttpClient.newBuilder()) + .objectMapper(objectMapper) + .build(); + sseTransports.add(new NamedClientMcpTransport(connectionName, transport)); + } + catch (Exception e) { + throw new IllegalArgumentException("Failed to create SSE transport for connection '" + connectionName + + "'. Check URL splitting: url='" + baseUrl + "', sse-endpoint='" + sseEndpoint + + "'. Full URL should be split as: url=http://host:port, sse-endpoint=/path/to/endpoint", e); + } } return sseTransports; diff --git a/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/mcp/mcp-client-boot-starter-docs.adoc b/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/mcp/mcp-client-boot-starter-docs.adoc index fd6974ad77c..7c966dc9e95 100644 --- a/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/mcp/mcp-client-boot-starter-docs.adoc +++ b/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/mcp/mcp-client-boot-starter-docs.adoc @@ -188,7 +188,7 @@ Properties for Server-Sent Events (SSE) transport are prefixed with `spring.ai.m |`/sse` |=== -Example configuration: +Example configurations: [source,yaml] ---- spring: @@ -197,13 +197,55 @@ spring: client: sse: connections: + # Simple configuration using default /sse endpoint server1: url: http://localhost:8080 + + # Custom SSE endpoint server2: url: http://otherserver:8081 sse-endpoint: /custom-sse + + # Complex URL with path and token (like MCP Hub) + mcp-hub: + url: http://localhost:3000 + sse-endpoint: /mcp-hub/sse/cf9ec4527e3c4a2cbb149a85ea45ab01 + + # SSE endpoint with query parameters + api-server: + url: https://api.example.com + sse-endpoint: /v1/mcp/events?token=abc123&format=json ---- +==== URL Splitting Guidelines + +When you have a full SSE URL, split it into base URL and endpoint path: + +[cols="2,2"] +|=== +|Full URL |Configuration + +|`\http://localhost:3000/mcp-hub/sse/token123` +|`url: http://localhost:3000` + +`sse-endpoint: /mcp-hub/sse/token123` + +|`\https://api.service.com/v2/events?key=secret` +|`url: https://api.service.com` + +`sse-endpoint: /v2/events?key=secret` + +|`\http://localhost:8080/sse` +|`url: http://localhost:8080` + +`sse-endpoint: /sse` (or omit for default) +|=== + +==== Troubleshooting SSE Connections + +*404 Not Found Errors:* + +* Verify URL splitting: ensure the base `url` contains only the scheme, host, and port +* Check the `sse-endpoint` starts with `/` and includes the full path and query parameters +* Test the full URL directly in a browser or curl to confirm it's accessible + === Streamable Http Transport Properties Properties for Streamable Http transport are prefixed with `spring.ai.mcp.client.streamable-http`: