feat: Add Spring WebMVC streamable server transport provider (#425)

- Add WebMvcStreamableServerTransportProvider with SSE support for streamable sessions
- Support GET, POST, DELETE endpoints for MCP protocol operations
- Implement thread-safe SSE operations using ReentrantLock in WebMvcSseServerTransportProvider
- Add test infrastructure with AbstractMcpClientServerIntegrationTests
  - Refactor WebMvcStreamableIntegrationTests to use parameterized tests
  - Support testing with both HttpClient and WebFlux transports
- Add streamable transport tests for both async and sync server modes
- Refactor existing WebMVC SSE integration tests to use shared test base
- Add error handling improvements in McpStreamableServerSession
- Update dependencies: add json-unit-assertj for enhanced JSON testing
- Reorganize POM dependencies and add mcp-spring-webflux test dependency
- Wrap handler invocations with Mono.defer for lazy evaluation
   - Wrap consumer, tool, resource, prompt, and completion handler calls with Mono.defer()
   - Ensures proper lazy evaluation and error handling in reactive streams

Related to #72

Signed-off-by: Christian Tzolov <christian.tzolov@broadcom.com>
Signed-off-by: Dariusz Jędrzejczyk <dariusz.jedrzejczyk@broadcom.com>
Co-authored-by: Dariusz Jędrzejczyk <dariusz.jedrzejczyk@broadcom.com>
This commit is contained in:
Christian Tzolov 2025-07-30 14:38:16 +02:00 • committed by GitHub
commit 73515346ca
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -296,7 +296,7 @@ public class McpAsyncServer {
List<BiFunction<McpAsyncServerExchange, List<McpSchema.Root>, Mono<Void>>> rootsChangeConsumers) {
return (exchange, params) -> exchange.listRoots()
.flatMap(listRootsResult -> Flux.fromIterable(rootsChangeConsumers)
.flatMap(consumer -> consumer.apply(exchange, listRootsResult.roots()))
.flatMap(consumer -> Mono.defer(() -> consumer.apply(exchange, listRootsResult.roots())))
.onErrorResume(error -> {
logger.error("Error handling roots list change notification", error);
return Mono.empty();
@ -506,7 +506,7 @@ public class McpAsyncServer {
return Mono.error(new McpError("Tool not found: " + callToolRequest.name()));
}
return toolSpecification.map(tool -> tool.callHandler().apply(exchange, callToolRequest))
return toolSpecification.map(tool -> Mono.defer(() -> tool.callHandler().apply(exchange, callToolRequest)))
.orElse(Mono.error(new McpError("Tool not found: " + callToolRequest.name())));
};
}
@ -634,7 +634,7 @@ public class McpAsyncServer {
.findFirst()
.orElseThrow(() -> new McpError("Resource not found: " + resourceUri));
return specification.readHandler().apply(exchange, resourceRequest);
return Mono.defer(() -> specification.readHandler().apply(exchange, resourceRequest));
};
}
@ -740,7 +740,7 @@ public class McpAsyncServer {
return Mono.error(new McpError("Prompt not found: " + promptRequest.name()));
}
return specification.promptHandler().apply(exchange, promptRequest);
return Mono.defer(() -> specification.promptHandler().apply(exchange, promptRequest));
};
}
@ -845,7 +845,7 @@ public class McpAsyncServer {
return Mono.error(new McpError("AsyncCompletionSpecification not found: " + request.ref()));
}
return specification.completionHandler().apply(exchange, request);
return Mono.defer(() -> specification.completionHandler().apply(exchange, request));
};
}