Ktor extensions for streamable http (#504)

## Motivation and Context
Adds Ktor extensions for streaming data over the server-side HTTP
transport

## How Has This Been Tested?
Integration test

## Breaking Changes
-

## Types of changes
- [ ] Bug fix (non-breaking change which fixes an issue)
- [x] New feature (non-breaking change which adds functionality)
- [ ] Breaking change (fix or feature that would cause existing
functionality to change)
- [ ] Documentation update

## Checklist
- [x] I have read the [MCP
Documentation](https://modelcontextprotocol.io)
- [x] My code follows the repository's style guidelines
- [x] New and existing tests pass locally
- [x] I have added appropriate error handling
- [x] I have added or updated documentation as needed
This commit is contained in:
Pavel Gorgulov 2026-02-12 19:28:16 +01:00 • committed by GitHub
commit da971aaee5
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 356 additions and 16 deletions

View file

@ -0,0 +1,82 @@
package io.modelcontextprotocol.kotlin.sdk.integration.streamablehttp
import io.ktor.serialization.kotlinx.json.json
import io.ktor.server.application.install
import io.ktor.server.cio.CIOApplicationEngine
import io.ktor.server.engine.EmbeddedServer
import io.ktor.server.engine.embeddedServer
import io.ktor.server.plugins.contentnegotiation.ContentNegotiation
import io.modelcontextprotocol.kotlin.sdk.server.Server
import io.modelcontextprotocol.kotlin.sdk.server.ServerOptions
import io.modelcontextprotocol.kotlin.sdk.server.mcpStreamableHttp
import io.modelcontextprotocol.kotlin.sdk.types.GetPromptResult
import io.modelcontextprotocol.kotlin.sdk.types.Implementation
import io.modelcontextprotocol.kotlin.sdk.types.McpJson
import io.modelcontextprotocol.kotlin.sdk.types.PromptArgument
import io.modelcontextprotocol.kotlin.sdk.types.PromptMessage
import io.modelcontextprotocol.kotlin.sdk.types.Role
import io.modelcontextprotocol.kotlin.sdk.types.ServerCapabilities
import io.modelcontextprotocol.kotlin.sdk.types.TextContent
import io.ktor.server.cio.CIO as ServerCIO
open class AbstractStreamableHttpIntegrationTest {
suspend fun initTestServer(name: String? = null): StreamableHttpTestServer {
val mcpServer = Server(
Implementation(name = name ?: DEFAULT_SERVER_NAME, version = VERSION),
ServerOptions(
capabilities = ServerCapabilities(prompts = ServerCapabilities.Prompts(listChanged = true)),
),
) {
addPrompt(
name = "prompt",
description = "Prompt description",
arguments = listOf(
PromptArgument(
name = "client",
description = "Client name who requested a prompt",
required = true,
),
),
) { request ->
GetPromptResult(
messages = listOf(
PromptMessage(
role = Role.User,
content = TextContent("Prompt for client ${request.params.arguments?.get("client")}"),
),
),
description = "Prompt for ${request.params.name}",
)
}
}
val ktorServer = embeddedServer(
ServerCIO,
host = URL,
port = PORT,
) {
install(ContentNegotiation) {
json(McpJson)
}
mcpStreamableHttp { mcpServer }
}
return StreamableHttpTestServer(
mcpServer = mcpServer,
ktorServer = ktorServer.startSuspend(wait = false),
)
}
data class StreamableHttpTestServer(
val mcpServer: Server,
val ktorServer: EmbeddedServer<CIOApplicationEngine, CIOApplicationEngine.Configuration>,
)
companion object {
internal const val DEFAULT_SERVER_NAME = "streamable-http-test-server"
internal const val VERSION = "1.0.0"
internal const val URL = "127.0.0.1"
internal const val PORT = 0
}
}

View file

@ -0,0 +1,65 @@
package io.modelcontextprotocol.kotlin.sdk.integration.streamablehttp
import io.ktor.client.HttpClient
import io.ktor.client.plugins.sse.SSE
import io.modelcontextprotocol.kotlin.sdk.client.Client
import io.modelcontextprotocol.kotlin.sdk.client.StreamableHttpClientTransport
import io.modelcontextprotocol.kotlin.sdk.types.GetPromptRequest
import io.modelcontextprotocol.kotlin.sdk.types.GetPromptRequestParams
import io.modelcontextprotocol.kotlin.sdk.types.Implementation
import io.modelcontextprotocol.kotlin.sdk.types.LATEST_PROTOCOL_VERSION
import io.modelcontextprotocol.kotlin.sdk.types.TextContent
import io.modelcontextprotocol.kotlin.test.utils.actualPort
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.runBlocking
import kotlin.test.Test
import kotlin.test.assertTrue
import io.ktor.client.engine.cio.CIO as ClientCIO
class StreamableHttpIntegrationTest : AbstractStreamableHttpIntegrationTest() {
@Test
fun `client should receive responses via POST json and server notifications via GET sse`() =
runBlocking(Dispatchers.IO) {
var server: StreamableHttpTestServer? = null
var client: Client? = null
var httpClient: HttpClient? = null
try {
server = initTestServer()
val port = server.ktorServer.actualPort()
httpClient = HttpClient(ClientCIO) { install(SSE) }
val transport = StreamableHttpClientTransport(
client = httpClient,
url = "http://$URL:$port/mcp",
).apply {
protocolVersion = LATEST_PROTOCOL_VERSION
}
client = Client(Implementation("streamable-http-client", VERSION))
client.connect(transport)
val prompt = getPrompt(client, "Client A")
assertTrue { "Client A" in prompt }
} finally {
client?.close()
httpClient?.close()
server?.ktorServer?.stopSuspend(1000, 2000)
}
}
private suspend fun getPrompt(client: Client, clientName: String): String {
val response = client.getPrompt(
GetPromptRequest(
GetPromptRequestParams(
name = "prompt",
arguments = mapOf("client" to clientName),
),
),
)
return (response.messages.first().content as? TextContent)?.text
?: error("Failed to receive prompt for Client $clientName")
}
}