modelcontextprotocol / modelcontextprotocol/kotlin-sdk

Ktor SSE / StreamableHttp extensions build their transports internally, with no hook to wrap them

Open
#921 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

api enhancement P2 ready for work
Dominant language
Kotlin
Stars
1.5k
Forks
248
Avg merge
1d 20h
Merged PRs (30d)
23

Description

Affected version / component: io.modelcontextprotocol:kotlin-sdk 0.14.0 — server/KtorServer.kt

The Application.mcp { } (SSE) and Application.mcpStreamableHttp { } extensions construct the transport and create the session entirely inside the extension. The user block only returns a Server, so there is no point at which caller code can wrap, decorate, or intercept the Transport before it is used. That rules out any cross-cutting transport behavior — logging, metrics, back-pressure, custom framing — on HTTP sessions, even though it is straightforward for transports you build yourself.

The SSE endpoint is representative:

private suspend fun ServerSSESession.mcpSseEndpoint(..., block: ServerSSESession.() -> Server) {
    val transport = mcpSseTransport(postEndpoint, transportManager, maxRequestBodySize) // SseServerTransport(...)
    val server = block()
    server.onClose { ... }
    server.createSession(transport)
    awaitCancellation()
}

mcpStreamableHttp has the same shape, with StreamableHttpServerTransport(configuration) followed by server.createSession(transport). Because the block returns a Server, the caller never sees the Transport and cannot substitute a delegating wrapper. mcpStreamableHttp { } does accept a serverFactory parameter, but it only supplies the Server; it does not expose or allow wrapping the transport, so it does not close this gap.

(Surfacing the JSON-RPC request id to handlers, which might otherwise motivate a transport hook, is being addressed separately upstream, so it is out of scope here.)

Suggested fix

Expose a transport factory/interceptor hook — let the mcp / mcpStreamableHttp setup wrap each Transport before createSession, or route these sessions through a user-controllable connect() path so a subclassed server can intercept them.

Workaround

The HTTP/SSE transport classes (SseServerTransport, StreamableHttpServerTransport) and their handleRequest/handlePostMessage methods are public, so you can stand up your own Ktor routes, construct the transport, wrap it, and call Server.createSession(wrappedTransport) directly. The cost is real: the extension wiring you have to replicate — TransportManager, the session-id lifecycle (setOnSessionInitialized/setOnSessionClosed), DNS-rebinding protection, and content negotiation — is private, roughly a hundred lines that then have to track SDK internals across upgrades.

Impact

A transport hook would bring logging, metrics, back-pressure, and custom framing to HTTP/SSE without reimplementing the extension. Today, parity between stdio and HTTP transports means duplicating that internal wiring.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start in server/KtorServer.kt with the Application.mcp and Application.mcpStreamableHttp extensions, then inspect SseServerTransport, StreamableHttpServerTransport, and Server.createSession. Determine how a transport factory or interceptor can be exposed while preserving TransportManager, session lifecycle callbacks, DNS-rebinding protection, and content negotiation. Done means callers can wrap each HTTP/SSE transport without recreating the extension wiring.

Written by the indexing model from the issue text.

Assessment

Tech stack
kotlin
Domain
api, backend
Issue type
Feature
Difficulty
5/5
Estimated time
Over a week
Activity status
Quiet
Clarity
Mostly clear
Newbie friendliness
42/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.