feat(agents): upgrade sdk to 0.11.1, add streamable transport #1870
feat(agents): upgrade sdk to 0.11.1, add streamable transport #1870
Conversation
- Serialize non-string JSON Schema enum entries (numbers, booleans,
null, arrays, objects) to their canonical JSON form so
DefaultMcpToolDescriptorParser no longer throws on mixed-type enums
like ["red", "amber", null, 42].
- Treat `additionalProperties: {}` as `true` with no type constraint
instead of recursing into an empty schema and failing with
"Parameter must have type property".
Closes #307
Closes #1676
There was a problem hiding this comment.
Pull request overview
Upgrades the Model Context Protocol (MCP) Kotlin SDK to 0.11.1 and makes Streamable HTTP the primary transport across Koog’s MCP client/server integrations, adding parser support for newer JSON Schema constructs and expanding test coverage.
Changes:
- Bump MCP Kotlin SDK from
0.8.1to0.11.1(including examples). - Add Streamable HTTP client/server transport support and deprecate SSE server entrypoints in favor of
startMcpServer(...). - Extend MCP tool schema parsing to support JSON Schema
typearrays and local$refvia$defs/definitions, with new tests.
Reviewed changes
Copilot reviewed 14 out of 14 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
| koog-ktor/src/jvmMain/kotlin/ai/koog/ktor/KoogKtorServerPluginJvm.kt | Adds Streamable HTTP registration API for MCP tools in the Ktor plugin. |
| gradle/libs.versions.toml | Updates MCP SDK version to 0.11.1 (and related catalog cleanup). |
| examples/simple-examples/gradle/libs.versions.toml | Updates MCP SDK version for examples to 0.11.1. |
| agents/agents-mcp/src/jvmTest/kotlin/ai/koog/agents/mcp/TestMcpServer.kt | Adds transport-mode support + dynamic port resolution for test server. |
| agents/agents-mcp/src/jvmTest/kotlin/ai/koog/agents/mcp/StreamableHttpMcpToolTest.kt | New tests validating Streamable HTTP tool discovery/execution. |
| agents/agents-mcp/src/jvmTest/kotlin/ai/koog/agents/mcp/McpToolTest.kt | Switches to dynamic ports and adds error-encoding tests. |
| agents/agents-mcp/src/commonTest/kotlin/ai/koog/agents/mcp/DefaultMcpToolDescriptorParserTest.kt | Adds extensive tests for type-arrays and $ref/$defs parsing. |
| agents/agents-mcp/src/commonMain/kotlin/ai/koog/agents/mcp/McpToolRegistryProvider.kt | Adds Streamable HTTP registry builder and transport metadata mapping. |
| agents/agents-mcp/src/commonMain/kotlin/ai/koog/agents/mcp/McpToolDefinitionParser.kt | Implements JSON Schema type arrays + $ref resolution in tool parsing. |
| agents/agents-mcp/src/commonMain/kotlin/ai/koog/agents/mcp/McpTool.kt | Adjusts string encoding for error results. |
| agents/agents-mcp-server/src/jvmTest/kotlin/ai/koog/agents/mcp/server/KoogToolAsMcpToolTest.kt | Adds Streamable HTTP server/client test path. |
| agents/agents-mcp-server/src/jvmMain/kotlin/ai/koog/agents/mcp/server/McpServer.jvm.kt | Updates stdio server startup for new SDK session API. |
| agents/agents-mcp-server/src/commonMain/kotlin/ai/koog/agents/mcp/server/McpServer.kt | Introduces Streamable HTTP as default server transport and deprecates SSE starters. |
| agents/agents-mcp-metadata/src/commonMain/kotlin/ai/koog/agents/mcp/metadata/McpToolSupport.kt | Adds StreamableHttp transport metadata enum. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| fun stop() { | ||
| if (!isRunning) return | ||
|
|
||
| serverJob?.cancel() | ||
| serverJob = null | ||
| embeddedServer = null | ||
| isRunning = false | ||
| println("Test MCP server stopped") |
There was a problem hiding this comment.
stop() only cancels the coroutine and nulls references, but it never stops the underlying Ktor EmbeddedServer. Since start() calls emb.start(wait = true) (blocking), cancelling serverJob may not actually stop the server, which can leak a running server and make tests hang/flaky. Consider storing emb and calling embeddedServer?.stop(gracePeriodMillis, timeoutMillis) (or using start(wait = false) and stopping explicitly) before clearing fields.
| level = DeprecationLevel.WARNING, | ||
| ) | ||
| public suspend fun startSseMcpServer( | ||
| factory: ApplicationEngineFactory<*, *>, | ||
| host: String = "localhost", | ||
| tools: ToolRegistry, |
There was a problem hiding this comment.
doStartMcpServer now always calls emb.connectors() even for callers that only need the Server (e.g. the fixed-port overload). connectors() currently busy-spins in a tight loop until resolvedConnectors() is non-empty, which can waste CPU and potentially loop forever if startup fails. Consider restoring conditional connector collection (only when the caller needs it), and/or adding a small delay/yield + timeout in connectors() to avoid a tight spin.
| * Defaults to Streamable HTTP transport. | ||
| * | ||
| * @param factory The Ktor application engine factory to use (e.g., CIO, Netty). | ||
| * @param tools The tools to expose via the MCP server. | ||
| * @param host The host to bind to. |
There was a problem hiding this comment.
The KDoc for startSseMcpServer(..., port, host, tools): Server says the port can be obtained from a returned list of EngineConnectorConfig, but this overload returns only Server (no connectors). Please update the comment to avoid implying data is returned here (or point to the other overload that returns connectors).
| val (server, connectors) = startMcpServer( | ||
| factory = CIO, | ||
| tools = ToolRegistry { | ||
| tool(tool) | ||
| }, | ||
| ) | ||
|
|
||
| val port = connectors.firstOrNull()?.port ?: 0 |
There was a problem hiding this comment.
This Streamable HTTP test hard-codes port = 3003, which can collide with other processes/parallel test runs and cause flaky failures. Prefer allocating an available port (e.g. NetUtil.findAvailablePort() exists in this repo) and using that value when starting the server.
| */ | ||
| override fun encodeResultToString(result: CallToolResult?, serializer: JSONSerializer): String { | ||
| if (result?.isError == true) { | ||
| return "Error: ${result.content.filterIsInstance<TextContent>().joinToString("\n") { it.text }}" |
There was a problem hiding this comment.
When result.isError == true, this returns only concatenated TextContent blocks. If the server returns an error with non-text content (or an empty content list), the encoded string becomes "Error: " with no useful details. Consider falling back to encoding the whole CallToolResult (or at least result.content.toString()/JSON) when no TextContent is present, so error information isn't silently dropped.
| return "Error: ${result.content.filterIsInstance<TextContent>().joinToString("\n") { it.text }}" | |
| val errorText = result.content.filterIsInstance<TextContent>().joinToString("\n") { it.text } | |
| if (errorText.isNotBlank()) { | |
| return "Error: $errorText" | |
| } | |
| val fallbackErrorJson = json.encodeToJsonElement(resultSerializer, result).toKoogJSONElement() | |
| return "Error: ${serializer.encodeJSONElementToString(fallbackErrorJson)}" |
Upgrades MCP kotlin-sdk from 0.8.1 to 0.11.1 and makes Streamable HTTP the primary MCP transport for both client and server
closes #1674
closes KG-792
closes KG-756
closes KG-49
closes KG-755
DEPRECATED:
startSseMcpServer(factory, port, host, tools)-- usestartMcpServer(factory, tools, port, host)startSseMcpServer(factory, host, tools)-- usestartMcpServer(factory, tools, host)