| name | kotlin-mcp-server-expert |
| description | Expert guidance for building production-grade MCP (Model Context Protocol) servers in Kotlin using the official Kotlin Multiplatform SDK. Use this skill whenever the user is building, designing, debugging, or improving an MCP server in Kotlin — including tool registration, resource templates, prompt handling, transport selection (stdio vs Streamable HTTP), coroutine patterns, structured output, error handling, token efficiency, security, testing, and deployment. Trigger even if the user only mentions "MCP", "kotlin sdk mcp", "addTool", "CallToolResult", "ServerCapabilities", or is wiring up a Ktor-based MCP server. Also trigger for questions like "how do I build an AI tool server in Kotlin", "expose my API as MCP tools", or "wire up Claude to my Kotlin backend". |
Kotlin MCP Server Expert
Build production-grade MCP servers in Kotlin — idiomatic, coroutine-native, efficient.
Decision Tree: Where to Start
Quick Start
Minimal stdio server
import io.ktor.utils.io.streams.asSource
import io.ktor.utils.io.streams.asSink
import io.modelcontextprotocol.kotlin.sdk.server.Server
import io.modelcontextprotocol.kotlin.sdk.server.ServerOptions
import io.modelcontextprotocol.kotlin.sdk.server.StdioServerTransport
import io.modelcontextprotocol.kotlin.sdk.types.*
fun main() {
val server = Server(
serverInfo = Implementation(name = "my-server", version = "1.0.0"),
options = ServerOptions(
capabilities = ServerCapabilities(
tools = ServerCapabilities.Tools(listChanged = true)
)
)
)
server.addTool(
name = "greet",
description = "Greet a user by name. Returns a personalised greeting.",
inputSchema = ToolSchema(
properties = buildJsonObject {
put("name", buildJsonObject {
put("type", "string")
put("description", "The user's name")
})
},
required = listOf("name")
)
) { request ->
val name = request.arguments?.get("name")?.jsonPrimitive?.content
?: return@addTool CallToolResult(
content = listOf(TextContent("Missing required argument: name")),
isError = true
)
CallToolResult(content = listOf(TextContent("Hello, $name!")))
}
val transport = StdioServerTransport(
inputStream = System.`in`.asSource().buffered(),
outputStream = System.out.asSink().buffered()
)
runBlocking { server.createSession(transport) }
}
Minimal Streamable HTTP server (Ktor)
import io.ktor.server.cio.CIO
import io.ktor.server.engine.embeddedServer
import io.modelcontextprotocol.kotlin.sdk.server.mcpStreamableHttp
fun main() {
embeddedServer(CIO, host = "127.0.0.1", port = 3000) {
mcpStreamableHttp {
buildMyServer()
}
}.start(wait = true)
}
Transport Selection
| Criterion | stdio | Streamable HTTP |
|---|
| Deployment | Local subprocess | Remote / cloud |
| Sessions | Single-tenant | Multi-tenant |
| Auth | Process-level | Bearer / OAuth 2.1 |
| Scaling | Single instance | Horizontal (stateless) |
| Cold start | Microseconds | Milliseconds |
| Preferred for | CLI, IDE plugins | Cloud APIs, SaaS |
Rule: use stdio for local dev and tooling. Use Streamable HTTP (via Ktor mcpStreamableHttp) for anything deployed remotely. Avoid the legacy SSE transport — it's deprecated.
ServerCapabilities: Declare What You Support
Only declare capabilities you actually register handlers for — the SDK enforces this at runtime.
ServerCapabilities(
tools = ServerCapabilities.Tools(listChanged = true),
resources = ServerCapabilities.Resources(
subscribe = true,
listChanged = true
),
prompts = ServerCapabilities.Prompts(listChanged = true),
logging = ServerCapabilities.Logging,
)
Core API Cheatsheet
server.addTool(name, description, inputSchema, outputSchema?) { req -> CallToolResult(...) }
server.addResource(uri, name, description, mimeType) { req -> ReadResourceResult(...) }
server.addResourceTemplate(uriTemplate, name, description, mimeType) { req -> ReadResourceResult(...) }
server.addPrompt(name, description, arguments?) { req -> GetPromptResult(...) }
sendToolListChanged()
sendResourceListChanged()
sendLoggingMessage(LoggingMessageNotification(...))
server.onConnect { }
All handlers are suspend lambdas with ClientConnection as the receiver — giving direct access to sendLoggingMessage, sendToolListChanged, etc. from inside a handler.
Five Rules You Must Not Break
- Never write to stdout in stdio mode. stdout is the JSON-RPC wire. Use
System.err or sendLoggingMessage for all diagnostics.
- Always validate arguments before use. Return
CallToolResult(isError = true) for bad input — never throw from a handler for expected user errors.
- Rethrow
CancellationException. Catching Exception in a coroutine handler swallows cancellation. Catch only specific types, or catch then rethrow CancellationException.
- Declare capabilities before adding primitives.
addTool throws IllegalStateException if tools capability is absent from ServerOptions.
- One server instance per transport connection. Never share mutable server state across concurrent HTTP connections — the SDK handles session isolation, but you must not use shared mutable state in handlers.
Reference Index
- primitives.md — Deep guide: tool design, inputSchema, outputSchema, structured output, resource templates, prompts, tool annotations, response content types.
- coroutines.md — Dispatcher selection, structured concurrency in handlers, Flow integration, progress notifications, avoiding deadlocks.
- error-handling.md —
isError vs exceptions, retry patterns, partial failures, business errors vs protocol errors.
- token-efficiency.md — Response verbosity, pagination, ResourceLink, field projection, capping output.
- security.md — Input validation, SSRF prevention, command injection, OAuth 2.1, secret management, sandboxing.
- testing.md — ChannelTransport unit tests, runTest patterns, integration tests, assertion strategies.
- deployment.md — Ktor integration, health checks, observability, horizontal scaling, containerisation.
- web-dashboard.md — MCP Inspector, SSE live event feed, browser-editable runtime config (
MutableStateFlow<ServerConfig>).
- niche-edge-cases.md — Dynamic tool registration, elicitation, sampling, Tasks API, cancellation, protocol version, multi-tenant isolation.