Koog 1.0 idioms, gotchas, and scaffolding skills for Kotlin agents on the JVM
87
88%
Does it follow best practices?
Impact
87%
1.85xAverage score across 45 eval scenarios
Advisory
Suggest reviewing before use
Process steps in order. Do not skip ahead.
Ask the user, one question at a time:
If the user doesn't know which transport the server supports, default to Streamable HTTP and plan a fallback to SSE if connection fails — the primary 1.0 transport is Streamable HTTP (MCP SDK 0.11.x), but a number of in-the-wild servers still only expose SSE.
Proceed immediately to Step 2.
Open build.gradle.kts and confirm the MCP client artifact is in the dependencies block. The umbrella ai.koog:koog-agents does not pull MCP — it must be added explicitly. If absent, add it:
implementation("ai.koog:agents-mcp-jvm:1.0.0-beta")Two gotchas the umbrella version hides:
agents-mcp (and agents-mcp-server) ship as 1.0.0-beta — Koog 1.0's stable release did not publish them at 1.0.0. Pin 1.0.0-beta (or later when stable).-jvm suffix (agents-mcp-jvm, not bare agents-mcp) — without it, Gradle KMP variant resolution can't pick the JVM artifact at this version and the build fails with "could not find" errors.Re-run ./gradlew --refresh-dependencies if the project is already imported into the IDE.
Proceed immediately to Step 3.
ToolRegistryIn the file that constructs the agent, build the MCP registry before the agent. Pick the form matching the transport.
Two-import rule for the registry builder:
McpToolRegistryProvider — the objectstreamableHttp, fromSseUrl, or fromProcessai.koog.agents.mcp, not members of the provider objectStreamable HTTP (preferred):
import ai.koog.agents.mcp.McpToolRegistryProvider
import ai.koog.agents.mcp.streamableHttp
val mcpRegistry = McpToolRegistryProvider.streamableHttp {
url = "https://<your-mcp-server>/mcp"
// headers, auth, timeouts — see McpToolRegistryProvider source
}SSE (fallback):
import ai.koog.agents.mcp.McpToolRegistryProvider
import ai.koog.agents.mcp.fromSseUrl
val mcpRegistry = McpToolRegistryProvider.fromSseUrl("https://<your-mcp-server>/sse")stdio (locally launched):
import ai.koog.agents.mcp.McpToolRegistryProvider
import ai.koog.agents.mcp.fromProcess
val process = ProcessBuilder("npx", "-y", "@playwright/mcp@latest").start()
val mcpRegistry = McpToolRegistryProvider.fromProcess(process)This is a suspending call. Run it inside the same runBlocking { ... } (or coroutine scope) that constructs the agent.
Proceed immediately to Step 4.
If the agent already has a ToolRegistry, merge with +; if not, pass the MCP registry as the toolRegistry:
// merge case
val combined = ToolRegistry { tools(<ExistingTools>().asTools()) } + mcpRegistry
val agent = AIAgent(
promptExecutor = ...,
llmModel = ...,
toolRegistry = combined,
systemPrompt = ...,
)Do not mutate the registry after constructing the agent — features wire up during installFeatures and read the registry then. Reorder the construction so the registry is final before AIAgent(...) runs.
Proceed immediately to Step 5.
./gradlew build to confirm the project still compiles. If it doesn't, the most common cause is a missing import — surface the exact errortools/list JSON-RPC method against the server (over its configured transport) to confirm what it actually advertises.Steps 1–5 covered the client side. If the user is consuming an existing MCP server (the common case), finish here. If the user is authoring an MCP server, proceed to Step 6.
Use when the user asks to "expose tools over MCP", "build an MCP server", or "publish a ToolSet as an MCP endpoint". Skip otherwise.
Add the server module:
implementation("ai.koog:agents-mcp-server-jvm:1.0.0-beta")Same 1.0.0-beta and -jvm-suffix gotchas from Step 2 apply.
The same @Tool / @LLMDescription / ToolSet you'd register on a Koog agent via Skill(skill: "add-tool") is what you expose. Wrap it in a ToolRegistry and hand to startStdioMcpServer:
import ai.koog.agents.core.tools.ToolRegistry
import ai.koog.agents.core.tools.reflect.asTools
import ai.koog.agents.mcp.server.startStdioMcpServer
import kotlinx.coroutines.awaitCancellation
import kotlinx.coroutines.runBlocking
fun main() = runBlocking {
val registry = ToolRegistry { tools(YourTools().asTools()) } // replace YourTools with the actual ToolSet class
startStdioMcpServer(registry)
awaitCancellation()
}The awaitCancellation() keeps the process alive while the parent (a Koog agent client via fromProcess, or any MCP-compliant client) drives requests over stdio. Write Path: src/main/kotlin/com/example/McpServer.kt for the entry point.
Finish here.
evals
scenario-1
scenario-2
scenario-3
scenario-4
scenario-5
scenario-6
scenario-7
scenario-8
scenario-9
scenario-10
scenario-11
scenario-12
scenario-13
scenario-14
scenario-15
scenario-16
scenario-17
scenario-18
scenario-19
scenario-20
scenario-21
scenario-22
scenario-23
scenario-24
scenario-25
scenario-26
scenario-27
scenario-28
scenario-29
scenario-30
scenario-31
scenario-32
scenario-33
scenario-34
scenario-35
scenario-36
scenario-37
scenario-38
scenario-39
scenario-40
scenario-41
scenario-42
scenario-43
scenario-44
scenario-45
skills
add-observability
add-persistence
add-rag
add-structured-output
add-token-budgeting
add-tool
cache-llm-calls
define-prompt
domain-model-subtask-pipeline
references
enable-prompt-caching
handle-agent-events
manage-state
migrate-from-0-x
model-planner-subtasks
persist-chat-history
query-sql-from-agent
scaffold-agent
snapshot-and-restore
test-koog-agents
trace-agent-internals
use-attachments
use-functional-agent
use-llm-node-variants
use-planner
wire-a2a
wire-acp-server
wire-ktor-server
wire-mcp-server
wire-spring-boot