Skip to content

feat: OAuth 2.0 Protected Resource + Streamable HTTP for MCP endpoint #1841

Description

@lrudolph333

Summary

The MCP endpoint currently has no authentication/authorization and only supports SSE transport. As agentic development becomes mainstream, the MCP server needs to be a first-class citizen in kafbat's security model — not an afterthought that bypasses existing controls (see #1751).

This proposal outlines adding OAuth 2.0 Protected Resource semantics and Streamable HTTP transport to the MCP endpoint, making it compatible with enterprise authorization servers and the latest MCP specification.

Motivation

Key Finding: Streamable HTTP Already Available

WebFluxStreamableServerTransportProvider — the reactive/WebFlux-native streamable HTTP transport — already exists in mcp-spring-webflux v0.18.x. Kafbat is on v0.10.0 which only has WebFluxSseServerTransportProvider. This is a version bump, not new development.

SDK version WebFlux transports available
0.10.0 (kafbat today) WebFluxSseServerTransportProvider only
0.18.2 WebFluxSseServerTransportProvider + WebFluxStreamableServerTransportProvider + WebFluxStatelessServerTransport

As of MCP Java SDK 1.0, the Spring transport modules moved to Spring AI:

Before (0.x) After (1.0+)
io.modelcontextprotocol.sdk:mcp-spring-webflux:0.18.2 org.springframework.ai:mcp-spring-webflux (Spring AI 2.0)

Either coordinate works. The 0.18.x path is simpler since it doesn't require adopting the full Spring AI dependency tree.

Why Streamable HTTP Matters for OAuth

SSE (legacy) Streamable HTTP
Spec status Deprecated (2025-06-18) Current
Transport Two endpoints (/sse + /message) Single endpoint (/mcp)
Auth Awkward — EventSource in browsers can't set Authorization header Clean — Bearer token on every HTTP POST
Resumability Not built-in Built-in via session tokens
Proxy-friendly SSE connections problematic behind LBs Standard HTTP POST/GET

The key point: SSE makes OAuth difficult because the initial EventSource connection cannot set custom headers. Streamable HTTP is standard HTTP with Bearer tokens on every request — exactly what OAuth resource servers expect.

Proposed Changes

Phase 1: Upgrade MCP SDK + Add Streamable HTTP Transport

Bump mcp-spring-webflux from 0.10.0 → 0.18.2. In McpConfig.java, replace:

// Current (SSE only, v0.10.0)
@Bean
public WebFluxSseServerTransportProvider sseServerTransport(ObjectMapper mapper) {
    return new WebFluxSseServerTransportProvider(mapper, "/mcp/message", "/mcp/sse");
}

With:

// Streamable HTTP (v0.18.x)
@Bean
public WebFluxStreamableServerTransportProvider streamableServerTransport(ObjectMapper mapper) {
    return WebFluxStreamableServerTransportProvider.builder()
        .objectMapper(mapper)
        .messageEndpoint("/mcp")
        .build();
}

Keep SSE available for backwards compatibility (both transports can coexist). The RouterFunction bean and McpAsyncServer wiring remain structurally the same.

Phase 2: OAuth 2.0 Resource Server for MCP (Security)

Wire Spring Security's existing reactive resource server support into the MCP endpoint paths.

2a. Protected Resource Discovery Endpoint

Expose GET /.well-known/oauth-protected-resource (RFC 9728) that tells MCP clients where to authenticate:

{
  "resource": "https://your-kafbat-instance.com/mcp",
  "authorization_servers": ["https://your-oauth-server.com"],
  "jwks_uri": "https://your-oauth-server.com/.well-known/jwks.json",
  "scopes_supported": ["mcp.read", "mcp.write"]
}

Configurable via application properties:

mcp:
  enabled: true
  transport: streamable-http  # or "sse" for backwards compat
  auth:
    enabled: true
    resource-url: ${MCP_RESOURCE_URL:}
    authorization-server: ${MCP_AUTH_SERVER:}
    jwks-uri: ${MCP_JWKS_URI:}
    audience: ${MCP_AUDIENCE:}
    scopes-supported:
      - mcp.read
      - mcp.write

2b. JWT Validation on MCP Endpoints

Apply the existing oauth2ResourceServer JWT filter chain to /mcp/** paths. Since kafbat already supports resource server configuration (auth.oauth2.resource-server), this means ensuring the MCP router function participates in the same SecurityWebFilterChain.

The existing RBAC role extraction already works for REST API endpoints and can be reused for MCP sessions directly. (and is being improved upon)

2c. Scope-Based Tool Gating

Respect OAuth scopes to control which MCP tools are available:

  • mcp.read — read-only tools (list topics, describe configs, get messages)
  • mcp.write — mutating tools (create topics, produce messages, alter configs)

This also fixes #1751 more robustly: scope enforcement at the security layer prevents unauthorized mutations before tool dispatch runs.

Configuration Design

mcp:
  enabled: true
  transport: streamable-http  # "sse" | "streamable-http" | "both"
  auth:
    enabled: true  # default false for backwards compat
    resource-url: ${MCP_RESOURCE_URL:}
    authorization-server: ${MCP_AUTH_SERVER:}
    jwks-uri: ${MCP_JWKS_URI:}
    audience: ${MCP_AUDIENCE:}
    scopes-supported:
      - mcp.read
      - mcp.write
    scope-enforcement:
      read-tools: mcp.read
      write-tools: mcp.write

When mcp.auth.enabled=false, the MCP endpoint behaves as it does today (no auth). This preserves backwards compatibility for local/dev usage.

Implementation Considerations

  • Reactive stack: Kafbat is WebFlux-based (Spring Boot 3.5, Spring Security 6.x). The WebFluxStreamableServerTransportProvider is purpose-built for this — it uses RouterFunction<ServerResponse>, Mono/Flux, and Netty.
  • Existing RBAC integration: JWT validation + configurable claim extraction already works for REST endpoints. Extending to MCP paths reuses the same RbacReactiveJwtAuthenticationConverter.
  • Backwards compatibility: SSE transport should remain available (with deprecation notice) while streamable HTTP is added. A mcp.transport config property can control which transports are active.

Prior Art

Related Issues

Questions for the Community

  1. Should the SDK upgrade target 0.18.x (minimal change) or Spring AI 2.0 coordinates (future-proof)?
  2. Should mcp.auth.enabled default to true or false? (Backwards compat vs. secure-by-default)
  3. Should scope enforcement be configurable per-tool, or is the read/write binary sufficient?

Proposed Implementation Order

  1. Phase 1: Bump SDK version + add WebFluxStreamableServerTransportProvider alongside SSE
  2. Phase 2a: /.well-known/oauth-protected-resource endpoint
  3. Phase 2b: JWT validation on /mcp/** paths (reuse existing resource server config)
  4. Phase 2c: Scope-based tool gating

Happy to contribute implementation for all phases. Would appreciate maintainer input on the SDK version strategy (0.18.x vs Spring AI 2.0 coordinates).

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions