Skip to content

feat: add Streamable HTTP transport for MCP server - #1921

Open
sappusaketh wants to merge 1 commit into
kafbat:mainfrom
sappusaketh:feat/mcp-streamable-http
Open

feat: add Streamable HTTP transport for MCP server#1921
sappusaketh wants to merge 1 commit into
kafbat:mainfrom
sappusaketh:feat/mcp-streamable-http

Conversation

@sappusaketh

@sappusaketh sappusaketh commented Jul 29, 2026

Copy link
Copy Markdown

Replace legacy SSE transport with Streamable HTTP transport for the MCP server. This upgrades the MCP SDK from 0.10.0 to 1.1.0 and exposes a single /mcp endpoint instead of separate /mcp/sse + /mcp/message paths.

The WebFluxStreamableServerTransportProvider is vendored from Spring AI 2.0.0-M4 with a compatibility fix for Spring Framework 6.2 (replaces HttpHeaders.asMultiValueMap() which requires Spring 7.0).

Closes #1137

Summary by CodeRabbit

  • New Features

    • Added support for MCP streamable HTTP transport.
    • Enabled session-based MCP communication with streaming responses, notifications, reconnection, and graceful session termination.
    • Added support for client keep-alive messages and broadcasting notifications across active sessions.
  • Bug Fixes

    • Improved MCP tool request and response handling, including clearer structured error results.

Replace legacy SSE transport with Streamable HTTP transport for the MCP
server. This upgrades the MCP SDK from 0.10.0 to 1.1.0 and exposes a
single /mcp endpoint instead of separate /mcp/sse + /mcp/message paths.

The WebFluxStreamableServerTransportProvider is vendored from Spring AI
2.0.0-M4 with a compatibility fix for Spring Framework 6.2 (replaces
HttpHeaders.asMultiValueMap() which requires Spring 7.0).

Closes kafbat#1137
@sappusaketh
sappusaketh requested a review from a team as a code owner July 29, 2026 20:34
Copilot AI review requested due to automatic review settings July 29, 2026 20:34
@sappusaketh
sappusaketh requested a review from a team as a code owner July 29, 2026 20:34
@kapybro kapybro Bot added status/triage/manual Manual triage in progress and removed status/triage/manual Manual triage in progress labels Jul 29, 2026
@kapybro

kapybro Bot commented Jul 29, 2026

Copy link
Copy Markdown

AI Summary

The issue proposes replacing the legacy Server-Sent Events (SSE) transport in the MCP server with a Streamable HTTP transport, upgrading the MCP SDK from 0.10.0 to 1.1.0. This change consolidates endpoints into a single /mcp path and integrates a vendored Spring AI 2.0.0-M4 component with a compatibility fix for Spring Framework 6.2. The solution addresses the need for modernization and endpoint simplification while ensuring compatibility.

@kapybro kapybro Bot changed the title feat: add Streamable HTTP transport for MCP server Replace SSE transport with Streamable HTTP for MCP server Jul 29, 2026
@kapybro kapybro Bot added area/internal Internal app components. Will be excluded from the changelog. impact/api A PR with changes which affect API type/enhancement En enhancement/improvement to an already existing feature labels Jul 29, 2026
@coderabbitai

coderabbitai Bot commented Jul 29, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The MCP server migrates from the legacy SSE transport to Streamable HTTP, adds session-aware WebFlux GET/POST/DELETE handling, updates MCP SDK APIs and dependencies, and configures analysis exclusions for the new transport provider.

Changes

MCP Streamable HTTP migration

Layer / File(s) Summary
SDK upgrade and MCP wiring
gradle/libs.versions.toml, api/build.gradle, api/src/main/java/io/kafbat/ui/config/McpConfig.java, api/src/main/java/io/kafbat/ui/service/mcp/McpSpecificationGenerator.java, api/src/test/java/io/kafbat/ui/service/mcp/McpSpecificationGeneratorTest.java
MCP dependencies and Spring beans use the Streamable HTTP provider, while tool schemas, call handlers, results, and tests use the MCP 1.1.0 request and builder APIs.
Streamable transport provider
api/src/main/java/io/kafbat/ui/transport/WebFluxStreamableServerTransportProvider.java, api/build.gradle, etc/checkstyle/checkstyle-suppressions.xml, build.gradle, sonar-project.properties
Adds session-based WebFlux GET/POST/DELETE routes, SSE response streaming, JSON-RPC handling, notifications, keep-alive, deletion, graceful shutdown, builder configuration, and related analysis configuration.

Estimated code review effort: 4 (Complex) | ~60 minutes

Possibly related issues

Possibly related PRs

  • kafbat/kafka-ui#1830 — Directly overlaps the Streamable HTTP provider, MCP SDK upgrade, configuration, and analysis changes.

Suggested labels: scope/backend, type/feature

Suggested reviewers: iliax

Poem

A bunny hops through /mcp bright,
Carrying messages in streams of light.
Sessions bloom and errors flee,
Tools now speak MCP one-point-one happily.
Hop, hop—SSE gives way to HTTP!

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Linked Issues check ✅ Passed The PR implements Streamable HTTP support for the MCP server, matching issue #1137’s request to replace SSE-only transport.
Out of Scope Changes check ✅ Passed The extra build, test, and Sonar/checkstyle updates support the transport migration and do not appear unrelated.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely summarizes the main change: adding Streamable HTTP transport for the MCP server.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@sappusaketh sappusaketh changed the title Replace SSE transport with Streamable HTTP for MCP server feat: add Streamable HTTP transport for MCP server Jul 29, 2026

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR migrates the Kafka UI MCP server from the legacy SSE transport to the MCP “Streamable HTTP” transport, upgrading the MCP SDK to 1.1.0 and consolidating transport routing behind a single /mcp endpoint. It also vendors a Spring AI WebFlux transport provider (with a Spring Framework 6.2 compatibility tweak) and adjusts code style/static analysis settings to accommodate the vendored source.

Changes:

  • Replace the previous MCP Spring WebFlux SSE transport dependency with MCP SDK 1.1.0 core + Jackson JSON modules and update MCP tool invocation/signatures accordingly.
  • Introduce a vendored WebFluxStreamableServerTransportProvider and wire it via Spring configuration to expose /mcp with Streamable HTTP semantics.
  • Update Sonar + Checkstyle configuration to exclude/relax rules for the vendored transport provider file.

Reviewed changes

Copilot reviewed 8 out of 9 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
sonar-project.properties Adds Sonar project settings and CPD exclusion for the vendored transport provider.
gradle/libs.versions.toml Upgrades MCP dependencies by adding MCP SDK 1.1.0 core + Jackson modules.
etc/checkstyle/checkstyle-suppressions.xml Suppresses Checkstyle checks for the vendored transport provider file.
build.gradle Adds Sonar CPD exclusion for the vendored transport provider.
api/build.gradle Switches dependencies to MCP SDK 1.1.0 artifacts and wires Checkstyle suppression config.
api/src/test/java/io/kafbat/ui/service/mcp/McpSpecificationGeneratorTest.java Updates tests for MCP SDK API changes (tool builder + call handler signature).
api/src/main/java/io/kafbat/ui/transport/WebFluxStreamableServerTransportProvider.java Adds vendored Streamable HTTP WebFlux transport provider implementation.
api/src/main/java/io/kafbat/ui/service/mcp/McpSpecificationGenerator.java Adapts tool schema generation + call handling to MCP SDK 1.1.0 APIs.
api/src/main/java/io/kafbat/ui/config/McpConfig.java Rewires MCP server beans to use the new Streamable HTTP transport provider and unified /mcp endpoint.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +262 to +266
List<MediaType> acceptHeaders = request.headers().asHttpHeaders().getAccept();
if (!(acceptHeaders.contains(MediaType.APPLICATION_JSON)
&& acceptHeaders.contains(MediaType.TEXT_EVENT_STREAM))) {
return ServerResponse.badRequest().build();
}
Comment on lines +384 to +392
String sessionId = request.headers().asHttpHeaders().getFirst(HttpHeaders.MCP_SESSION_ID);

McpStreamableServerSession session = this.sessions.get(sessionId);

if (session == null) {
return ServerResponse.notFound().build();
}

return session.delete().then(ServerResponse.ok().build());

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

🧹 Nitpick comments (1)
api/src/main/java/io/kafbat/ui/transport/WebFluxStreamableServerTransportProvider.java (1)

192-192: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Extract the duplicated shutdown response text.

Sonar reports this duplication as a blocking failure. Introduce one SERVER_SHUTTING_DOWN constant and use it in all three handlers.

Also applies to: 248-248, 361-361

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@api/src/main/java/io/kafbat/ui/transport/WebFluxStreamableServerTransportProvider.java`
at line 192, Extract the repeated "Server is shutting down" text into a single
SERVER_SHUTTING_DOWN constant in WebFluxStreamableServerTransportProvider, then
replace the literal in all three shutdown response handlers, including the
locations corresponding to the referenced lines.

Source: Linters/SAST tools

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@api/src/main/java/io/kafbat/ui/config/McpConfig.java`:
- Around line 28-31: Update McpConfig.streamableServerTransport() to configure a
non-NOOP Origin security validator on
WebFluxStreamableServerTransportProvider.builder(), using the transport’s
supported security-validator configuration so the /mcp endpoint retains
DNS-rebinding protection.

In
`@api/src/main/java/io/kafbat/ui/transport/WebFluxStreamableServerTransportProvider.java`:
- Around line 283-300: Update the initialization flow around
sessionFactory.startSession, sessions.put, and init.initResult() to remove
init.session() from sessions whenever initialization, mapping, serialization, or
response creation fails. Preserve the existing successful response behavior and
ensure cleanup occurs for the session inserted before initResult completes.
- Around line 246-356: The handlePost method exceeds the cognitive-complexity
threshold; split its message-specific logic into focused private handlers.
Extract initialization handling, session ID validation and lookup, and JSON-RPC
request/notification/response dispatch from handlePost while preserving all
existing responses, session behavior, streaming, and error handling.
- Around line 384-392: Update the session deletion flow in the request handler
using the sessions registry and McpStreamableServerSession lookup: atomically
remove the session associated with sessionId from this.sessions before invoking
session.delete(). Preserve the existing not-found response when no session is
registered and return the successful response after deletion completes.
- Around line 170-183: Update closeGracefully() to perform session clearing and
keepAliveScheduler shutdown in a doFinally callback instead of doOnSuccess,
ensuring cleanup runs after successful completion, errors, and cancellation
while preserving the existing shutdown flow.
- Around line 216-218: The shared session-request handling around sessionId
lookup must also validate HttpHeaders.MCP_PROTOCOL_VERSION before retrieving or
using the session. Accept only the requested, supported, or negotiated protocol
value; return HTTP 400 for missing, invalid, or unsupported values, and apply
this consistently to GET, non-initialize POST, and DELETE paths.

---

Nitpick comments:
In
`@api/src/main/java/io/kafbat/ui/transport/WebFluxStreamableServerTransportProvider.java`:
- Line 192: Extract the repeated "Server is shutting down" text into a single
SERVER_SHUTTING_DOWN constant in WebFluxStreamableServerTransportProvider, then
replace the literal in all three shutdown response handlers, including the
locations corresponding to the referenced lines.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: c4bd728f-797a-4648-b133-56d89c92ae0d

📥 Commits

Reviewing files that changed from the base of the PR and between bc2d7ca and 5fbf0e5.

📒 Files selected for processing (9)
  • api/build.gradle
  • api/src/main/java/io/kafbat/ui/config/McpConfig.java
  • api/src/main/java/io/kafbat/ui/service/mcp/McpSpecificationGenerator.java
  • api/src/main/java/io/kafbat/ui/transport/WebFluxStreamableServerTransportProvider.java
  • api/src/test/java/io/kafbat/ui/service/mcp/McpSpecificationGeneratorTest.java
  • build.gradle
  • etc/checkstyle/checkstyle-suppressions.xml
  • gradle/libs.versions.toml
  • sonar-project.properties

Comment on lines +28 to +31
public WebFluxStreamableServerTransportProvider streamableServerTransport() {
return WebFluxStreamableServerTransportProvider.builder()
.messageEndpoint("/mcp")
.build();

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Locate McpConfig.java and MCP deps ="
git ls-files | rg 'McpConfig.java|pom.xml|build.gradle|gradle.properties' || true

echo
echo "== McpConfig contents =="
cat -n api/src/main/java/io/kafbat/ui/config/McpConfig.java

echo
echo "== Search WebFluxStreamableServerTransportProvider usages/imports/settings =="
rg -n "WebFluxStreamableServerTransportProvider|ServerTransportSecurityValidator|securityValidator|/mcp|ServerTransportSecurityValidator" .

Repository: kafbat/kafka-ui

Length of output: 7849


🌐 Web query:

Model Context Protocol streamable HTTP transport origin validation modelcontextprotocol.io

💡 Result:

In the Model Context Protocol (MCP) Streamable HTTP transport, origin validation is a mandatory security requirement designed to prevent DNS rebinding attacks [1][2][3]. The specification mandates the following regarding Origin header validation: 1. Mandatory Validation: Servers must validate the Origin header on all incoming connections if it is present [1][2][3]. 2. Rejection Criteria: If the Origin header is present and deemed invalid, the server must reject the request with an HTTP 403 Forbidden status [1][2][3]. The response body may optionally contain a JSON-RPC error response [1][2]. 3. Handling Missing Headers: Requests that do not include an Origin header are generally permitted to pass, as non-browser MCP clients do not typically send this header, and they are not considered relevant to the specific DNS rebinding threats that this validation defends against [4][5]. Implementation details for developers: - Official SDKs provide middleware and transport options (e.g., allowedOrigins in the TypeScript SDK) to facilitate this validation [6][7][4]. - Validation is strictly "deny-on-failure": if a header is present but cannot be parsed (or matches no entry in an allowlist), the request must be rejected [4]. - For local development, servers are also strongly encouraged to bind only to localhost (127.0.0.1) and implement authentication as additional layers of security [1][3]. These requirements ensure that local MCP servers are protected from unauthorized cross-site requests while remaining compatible with non-browser-based clients [4][5].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== MCP dependency declarations =="
rg -n "modelcontextprotocol|io.modelcontextprotocol|mcp-enabled|mcp" build.gradle api/build.gradle gradle.properties README.md || true

echo
echo "== Builder and validator implementation slice =="
sed -n '430,515p' api/src/main/java/io/kafbat/ui/transport/WebFluxStreamableServerTransportProvider.java

echo
echo "== Security validator implementation declaration and calls =="
rg -n "class ServerTransportSecurityValidator|interface ServerTransportSecurityValidator|impl|validateHeaders|Origin|Origin" api/src/main/java/io/kafbat/ui/transport/WebFluxStreamableServerTransportProvider.java

Repository: kafbat/kafka-ui

Length of output: 3357


Configure an Origin security validator for the MCP transport.

WebFluxStreamableServerTransportProvider.builder() defaults to ServerTransportSecurityValidator.NOOP, so the /mcp route performs no Origin validation. Set a non-noop validator (for example via .securityValidator(...)) to preserve the Streamable HTTP transport’s DNS-rebinding protection.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@api/src/main/java/io/kafbat/ui/config/McpConfig.java` around lines 28 - 31,
Update McpConfig.streamableServerTransport() to configure a non-NOOP Origin
security validator on WebFluxStreamableServerTransportProvider.builder(), using
the transport’s supported security-validator configuration so the /mcp endpoint
retains DNS-rebinding protection.

Comment on lines +170 to +183
public Mono<Void> closeGracefully() {
return Mono.defer(() -> {
this.isClosing = true;
return Flux.fromIterable(this.sessions.values())
.doFirst(() -> logger.debug("Initiating graceful shutdown with {} active sessions",
this.sessions.size()))
.flatMap(McpStreamableServerSession::closeGracefully)
.then();
}).then().doOnSuccess(v -> {
this.sessions.clear();
if (this.keepAliveScheduler != null) {
this.keepAliveScheduler.shutdown();
}
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Run shutdown cleanup on errors and cancellation too.

doOnSuccess is skipped if any session fails to close, leaving the keep-alive scheduler running and sessions retained. Move unconditional cleanup to doFinally.

Proposed fix
-		}).then().doOnSuccess(v -> {
+		}).then().doFinally(signalType -> {
 			this.sessions.clear();
 			if (this.keepAliveScheduler != null) {
 				this.keepAliveScheduler.shutdown();
 			}
 		});
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
public Mono<Void> closeGracefully() {
return Mono.defer(() -> {
this.isClosing = true;
return Flux.fromIterable(this.sessions.values())
.doFirst(() -> logger.debug("Initiating graceful shutdown with {} active sessions",
this.sessions.size()))
.flatMap(McpStreamableServerSession::closeGracefully)
.then();
}).then().doOnSuccess(v -> {
this.sessions.clear();
if (this.keepAliveScheduler != null) {
this.keepAliveScheduler.shutdown();
}
});
public Mono<Void> closeGracefully() {
return Mono.defer(() -> {
this.isClosing = true;
return Flux.fromIterable(this.sessions.values())
.doFirst(() -> logger.debug("Initiating graceful shutdown with {} active sessions",
this.sessions.size()))
.flatMap(McpStreamableServerSession::closeGracefully)
.then();
}).then().doFinally(signalType -> {
this.sessions.clear();
if (this.keepAliveScheduler != null) {
this.keepAliveScheduler.shutdown();
}
});
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@api/src/main/java/io/kafbat/ui/transport/WebFluxStreamableServerTransportProvider.java`
around lines 170 - 183, Update closeGracefully() to perform session clearing and
keepAliveScheduler shutdown in a doFinally callback instead of doOnSuccess,
ensuring cleanup runs after successful completion, errors, and cancellation
while preserving the existing shutdown flow.

Comment on lines +216 to +218
String sessionId = request.headers().asHttpHeaders().getFirst(HttpHeaders.MCP_SESSION_ID);

McpStreamableServerSession session = this.sessions.get(sessionId);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Locate and inspect the referenced file and related symbol usages.
file="api/src/main/java/io/kafbat/ui/transport/WebFluxStreamableServerTransportProvider.java"
if [ ! -f "$file" ]; then
  fd -i 'WebFluxStreamableServerTransportProvider.java'
  exit 0
fi

wc -l "$file"
echo '--- outline ---'
ast-grep outline "$file" --view compact || true

echo '--- relevant sections 180-405 ---'
sed -n '180,405p' "$file" | nl -ba -v180

echo '--- protocol header references ---'
rg -n "MCP-Protocol-Version|MCP_SESSION_ID|StreamableServerSession|GET|DELETE|post" "$file"

Repository: kafbat/kafka-ui

Length of output: 486


🏁 Script executed:

#!/bin/bash
set -euo pipefail

file="api/src/main/java/io/kafbat/ui/transport/WebFluxStreamableServerTransportProvider.java"

echo '--- relevant sections 1-430 ---'
awk '{printf "%d\t%s\n", NR, $0}' "$file" | sed -n '1,430p'

echo '--- protocol/header references ---'
grep -nE "MCP-Protocol-Version|PROTOCOL|SESSION_ID|McpStreamableServerSession|GET|DELETE|Post|post" "$file" || true

Repository: kafbat/kafka-ui

Length of output: 19941


🌐 Web query:

MCP specification 2025-06-18 basic transports MCP-Protocol-Version HTTP requests subsequent requests required unsupported values 400

💡 Result:

Under the Model Context Protocol (MCP) specification, particularly when using the Streamable HTTP transport, clients are required to include the MCP-Protocol-Version HTTP header on all requests [1][2][3]. This header allows the MCP server to respond according to the specific protocol version requested [1][2]. If a server receives a request with an invalid or unsupported MCP-Protocol-Version header, it is required to respond with a 400 Bad Request status code [2][3]. In modern versions of the specification, the server should also provide an UnsupportedProtocolVersionError in the response body, which lists the protocol versions that the server does support [3][4]. Key aspects of this requirement include: 1. Consistency: The value in the MCP-Protocol-Version header must match the version declared in the io.modelcontextprotocol/protocolVersion field within the request body's _meta object [3][5]. If these values mismatch, the server must reject the request with a 400 Bad Request and a HeaderMismatch error [3]. 2. Negotiation: The protocol version used by the client should typically align with the version negotiated during the initialization phase [2][3]. 3. Backwards Compatibility: While older versions of the specification (pre-2025-06-18) did not always define this header, servers that do not support legacy behavior must reject requests missing this header [3]. Servers that do support legacy clients may optionally assume a default version (such as 2025-03-26) if the header is omitted, depending on the specific implementation requirements [2][3]. For detailed implementation, consult the specific version of the MCP specification you are targeting, as newer drafts have formalized the UnsupportedProtocolVersionError and header-body validation requirements [3][4].

Citations:


Validate MCP-Protocol-Version on session requests.

GET, non-initialize POST, and DELETE retrieve a session only after checking MCP_SESSION_ID; they should also check HttpHeaders.MCP_PROTOCOL_VERSION for the requested/unsupported/negotiated value and return HTTP 400 when it is invalid or unsupported. Add this validation in shared handling so all session-dependent request paths enforce it.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@api/src/main/java/io/kafbat/ui/transport/WebFluxStreamableServerTransportProvider.java`
around lines 216 - 218, The shared session-request handling around sessionId
lookup must also validate HttpHeaders.MCP_PROTOCOL_VERSION before retrieving or
using the session. Accept only the requested, supported, or negotiated protocol
value; return HTTP 400 for missing, invalid, or unsupported values, and apply
this consistently to GET, non-initialize POST, and DELETE paths.

Comment on lines +246 to +356
private Mono<ServerResponse> handlePost(ServerRequest request) {
if (this.isClosing) {
return ServerResponse.status(HttpStatus.SERVICE_UNAVAILABLE).bodyValue("Server is shutting down");
}

try {
Map<String, List<String>> headers = toHeaderMap(request.headers().asHttpHeaders());
this.securityValidator.validateHeaders(headers);
}
catch (ServerTransportSecurityException e) {
String errorMessage = e.getMessage();
return ServerResponse.status(e.getStatusCode()).bodyValue(errorMessage != null ? errorMessage : "");
}

McpTransportContext transportContext = this.contextExtractor.extract(request);

List<MediaType> acceptHeaders = request.headers().asHttpHeaders().getAccept();
if (!(acceptHeaders.contains(MediaType.APPLICATION_JSON)
&& acceptHeaders.contains(MediaType.TEXT_EVENT_STREAM))) {
return ServerResponse.badRequest().build();
}

return request.bodyToMono(String.class).<ServerResponse>flatMap(body -> {
try {
McpSchema.JSONRPCMessage message = McpSchema.deserializeJsonRpcMessage(this.jsonMapper, body);
if (message instanceof McpSchema.JSONRPCRequest jsonrpcRequest
&& jsonrpcRequest.method().equals(McpSchema.METHOD_INITIALIZE)) {
if (this.sessionFactory == null) {
return ServerResponse.status(HttpStatus.INTERNAL_SERVER_ERROR)
.bodyValue(McpError.builder(McpSchema.ErrorCodes.INTERNAL_ERROR)
.message("Session factory not initialized")
.build());
}
var typeReference = new TypeRef<McpSchema.InitializeRequest>() {
};
McpSchema.InitializeRequest initializeRequest = this.jsonMapper
.convertValue(jsonrpcRequest.params(), typeReference);
McpStreamableServerSession.McpStreamableServerSessionInit init = this.sessionFactory
.startSession(initializeRequest);
this.sessions.put(init.session().getId(), init.session());
return init.initResult().map(initializeResult -> {
McpSchema.JSONRPCResponse jsonrpcResponse = new McpSchema.JSONRPCResponse(
McpSchema.JSONRPC_VERSION, jsonrpcRequest.id(), initializeResult, null);
try {
return this.jsonMapper.writeValueAsString(jsonrpcResponse);
}
catch (IOException e) {
logger.warn("Failed to serialize initResponse", e);
throw Exceptions.propagate(e);
}
})
.flatMap(initResult -> ServerResponse.ok()
.contentType(MediaType.APPLICATION_JSON)
.header(HttpHeaders.MCP_SESSION_ID, init.session().getId())
.bodyValue(initResult));
}

if (request.headers().header(HttpHeaders.MCP_SESSION_ID).isEmpty()) {
return ServerResponse.badRequest()
.bodyValue(McpError.builder(McpSchema.ErrorCodes.METHOD_NOT_FOUND)
.message("Session ID missing")
.build());
}

String sessionId = request.headers().asHttpHeaders().getFirst(HttpHeaders.MCP_SESSION_ID);
McpStreamableServerSession session = this.sessions.get(sessionId);

if (session == null) {
return ServerResponse.status(HttpStatus.NOT_FOUND)
.bodyValue(McpError.builder(McpSchema.ErrorCodes.INTERNAL_ERROR)
.message("Session not found: " + sessionId)
.build());
}

if (message instanceof McpSchema.JSONRPCResponse jsonrpcResponse) {
return session.accept(jsonrpcResponse).then(ServerResponse.accepted().build());
}
else if (message instanceof McpSchema.JSONRPCNotification jsonrpcNotification) {
return session.accept(jsonrpcNotification).then(ServerResponse.accepted().build());
}
else if (message instanceof McpSchema.JSONRPCRequest jsonrpcRequest) {
return ServerResponse.ok()
.contentType(MediaType.TEXT_EVENT_STREAM)
.body(Flux.<ServerSentEvent<?>>create(sink -> {
WebFluxStreamableMcpSessionTransport st = new WebFluxStreamableMcpSessionTransport(sink);
Mono<Void> stream = session.responseStream(jsonrpcRequest, st);
Disposable streamSubscription = stream.onErrorComplete(err -> {
sink.error(err);
return true;
}).contextWrite(sink.contextView()).subscribe();
sink.onCancel(streamSubscription);
}).contextWrite(ctx -> ctx.put(McpTransportContext.KEY, transportContext)),
ServerSentEvent.class);
}
else {
return ServerResponse.badRequest()
.bodyValue(McpError.builder(McpSchema.ErrorCodes.INVALID_REQUEST)
.message("Unknown message type")
.build());
}
}
catch (IllegalArgumentException | IOException e) {
logger.error("Failed to deserialize message: {}", e.getMessage());
return ServerResponse.badRequest()
.bodyValue(McpError.builder(McpSchema.ErrorCodes.INVALID_REQUEST)
.message("Invalid message format")
.build());
}
})
.switchIfEmpty(ServerResponse.badRequest().build())
.contextWrite(ctx -> ctx.put(McpTransportContext.KEY, transportContext));

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟠 Major | 🏗️ Heavy lift

Split handlePost into message-specific handlers.

Sonar reports cognitive complexity 27 versus the allowed 15. Extract initialization, session lookup, and request/notification/response dispatch into focused methods to unblock the quality gate.

🧰 Tools
🪛 GitHub Check: SonarCloud Code Analysis

[warning] 347-347: Replace "e" with an unnamed pattern.

See more on https://sonarcloud.io/project/issues?id=kafbat_kafka-ui&issues=AZ-vlmumaxAiTHmpQq30&open=AZ-vlmumaxAiTHmpQq30&pullRequest=1921


[failure] 246-246: Refactor this method to reduce its Cognitive Complexity from 27 to the 15 allowed.

See more on https://sonarcloud.io/project/issues?id=kafbat_kafka-ui&issues=AZ-vlmumaxAiTHmpQq3x&open=AZ-vlmumaxAiTHmpQq3x&pullRequest=1921


[warning] 320-320: Replace the chain of if/else with a switch expression.

See more on https://sonarcloud.io/project/issues?id=kafbat_kafka-ui&issues=AZ-vlmumaxAiTHmpQq3z&open=AZ-vlmumaxAiTHmpQq3z&pullRequest=1921


[warning] 292-292: Replace "e" with an unnamed pattern.

See more on https://sonarcloud.io/project/issues?id=kafbat_kafka-ui&issues=AZ-vlmumaxAiTHmpQq3y&open=AZ-vlmumaxAiTHmpQq3y&pullRequest=1921

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@api/src/main/java/io/kafbat/ui/transport/WebFluxStreamableServerTransportProvider.java`
around lines 246 - 356, The handlePost method exceeds the cognitive-complexity
threshold; split its message-specific logic into focused private handlers.
Extract initialization handling, session ID validation and lookup, and JSON-RPC
request/notification/response dispatch from handlePost while preserving all
existing responses, session behavior, streaming, and error handling.

Source: Linters/SAST tools

Comment on lines +283 to +300
McpStreamableServerSession.McpStreamableServerSessionInit init = this.sessionFactory
.startSession(initializeRequest);
this.sessions.put(init.session().getId(), init.session());
return init.initResult().map(initializeResult -> {
McpSchema.JSONRPCResponse jsonrpcResponse = new McpSchema.JSONRPCResponse(
McpSchema.JSONRPC_VERSION, jsonrpcRequest.id(), initializeResult, null);
try {
return this.jsonMapper.writeValueAsString(jsonrpcResponse);
}
catch (IOException e) {
logger.warn("Failed to serialize initResponse", e);
throw Exceptions.propagate(e);
}
})
.flatMap(initResult -> ServerResponse.ok()
.contentType(MediaType.APPLICATION_JSON)
.header(HttpHeaders.MCP_SESSION_ID, init.session().getId())
.bodyValue(initResult));

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Remove the session if initialization fails.

The session enters sessions before initResult() completes. Serialization or initialization errors therefore leave an unreachable session retained indefinitely. Remove it in the error path.

🧰 Tools
🪛 GitHub Check: SonarCloud Code Analysis

[warning] 292-292: Replace "e" with an unnamed pattern.

See more on https://sonarcloud.io/project/issues?id=kafbat_kafka-ui&issues=AZ-vlmumaxAiTHmpQq3y&open=AZ-vlmumaxAiTHmpQq3y&pullRequest=1921

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@api/src/main/java/io/kafbat/ui/transport/WebFluxStreamableServerTransportProvider.java`
around lines 283 - 300, Update the initialization flow around
sessionFactory.startSession, sessions.put, and init.initResult() to remove
init.session() from sessions whenever initialization, mapping, serialization, or
response creation fails. Preserve the existing successful response behavior and
ensure cleanup occurs for the session inserted before initResult completes.

Comment on lines +384 to +392
String sessionId = request.headers().asHttpHeaders().getFirst(HttpHeaders.MCP_SESSION_ID);

McpStreamableServerSession session = this.sessions.get(sessionId);

if (session == null) {
return ServerResponse.notFound().build();
}

return session.delete().then(ServerResponse.ok().build());

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Remove deleted sessions from the registry.

session.delete() terminates the session object, but the map retains it. Future requests still find the deleted session, while notifications and keep-alives continue targeting it.

Atomically remove it before deletion:

Proposed fix
-			McpStreamableServerSession session = this.sessions.get(sessionId);
+			McpStreamableServerSession session = this.sessions.remove(sessionId);

 			if (session == null) {
 				return ServerResponse.notFound().build();
 			}

 			return session.delete().then(ServerResponse.ok().build());
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
String sessionId = request.headers().asHttpHeaders().getFirst(HttpHeaders.MCP_SESSION_ID);
McpStreamableServerSession session = this.sessions.get(sessionId);
if (session == null) {
return ServerResponse.notFound().build();
}
return session.delete().then(ServerResponse.ok().build());
String sessionId = request.headers().asHttpHeaders().getFirst(HttpHeaders.MCP_SESSION_ID);
McpStreamableServerSession session = this.sessions.remove(sessionId);
if (session == null) {
return ServerResponse.notFound().build();
}
return session.delete().then(ServerResponse.ok().build());
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@api/src/main/java/io/kafbat/ui/transport/WebFluxStreamableServerTransportProvider.java`
around lines 384 - 392, Update the session deletion flow in the request handler
using the sessions registry and McpStreamableServerSession lookup: atomically
remove the session associated with sessionId from this.sessions before invoking
session.delete(). Preserve the existing not-found response when no session is
registered and return the successful response after deletion completes.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/internal Internal app components. Will be excluded from the changelog. impact/api A PR with changes which affect API type/enhancement En enhancement/improvement to an already existing feature

Projects

None yet

Development

Successfully merging this pull request may close these issues.

streamable-http for MCP server

2 participants