Menu

AI

How to Build an MCP Server in Kotlin (Kotlin SDK 0.16, 2026)

Build an MCP server in Kotlin with the official Kotlin SDK 0.16.0. Gradle setup, a stdio server, adding tools, Ktor Streamable HTTP, Inspector tests and pitfalls.

By: Irina ShvayaNovember 2, 2026

How to Build an MCP Server in Kotlin (Kotlin SDK 0.16, 2026)

This tutorial is part of Build an MCP Server, within The MCP Guide. Want it built for you? See MCP development.

You can build an MCP server in Kotlin with the official Kotlin SDK, published on Maven Central as io.modelcontextprotocol:kotlin-sdk. You create a Server, register tools with addTool, and serve it over stdio or over HTTP with Ktor. The same code can target the JVM, Native, JS and Wasm.

This guide builds a small stdio server, adds a tool, tests it with MCP Inspector, connects it to Claude, then moves it to Streamable HTTP. It also flags what the SDK's Tier 3 status and protocol version mean for production use.

Checked against: the Kotlin SDK README, the 0.16.0 release notes and the Kotlin tab of the official Build an MCP server tutorial, on 2 November 2026, for io.modelcontextprotocol:kotlin-sdk 0.16.0. eSEOspace did not compile or run this Kotlin code; no JDK was available.

Key Takeaways

  • The official Kotlin SDK is Tier 3 on the official SDK list as of November 2026, so plan for API changes between releases.
  • Version 0.16.0 (27 October 2026) is the latest on GitHub and Maven Central; its newest protocol version is 2025-11-25.
  • Clients that support only the 2026-07-28 spec cannot connect to it yet, per the official tutorial.
  • Use StdioServerTransport for local servers and the Ktor mcpStreamableHttp() helper for remote ones; SSE is deprecated.
  • Never use println() in a stdio server; log to stderr or a file.
  • 0.16.0 has breaking changes, including idle session expiry and renamed stdio arguments.

Prerequisites

  • JDK 11 or higher. The tutorial lists it, and the README badge says "JVM 11+".
  • Gradle, or IntelliJ IDEA's Kotlin project wizard.
  • Node.js, to run MCP Inspector with npx.
  • A client such as Claude Desktop or Claude Code.

New to MCP servers? Read how to build your first MCP server for the concepts, then return here.

Step 1: Set up Gradle

Create a project with gradle init and choose Application and Kotlin. Then use a build.gradle.kts like the one in the official tutorial:

val mcpVersion = "0.16.0"
val ktorVersion = "3.2.3"
val slf4jVersion = "2.0.17"

plugins {
    kotlin("jvm") version "2.3.20"
    kotlin("plugin.serialization") version "2.3.20"
    id("com.gradleup.shadow") version "8.3.9"
    application
}

repositories {
    mavenCentral()
}

application {
    mainClass.set("MainKt")
}

dependencies {
    implementation("io.modelcontextprotocol:kotlin-sdk:$mcpVersion")
    implementation("io.ktor:ktor-client-content-negotiation:$ktorVersion")
    implementation("io.ktor:ktor-serialization-kotlinx-json:$ktorVersion")
    implementation("io.ktor:ktor-client-cio:$ktorVersion")
    implementation("org.slf4j:slf4j-simple:$slf4jVersion")
}

The tutorial's weather server needs the Ktor client lines to call an outside API. A server with no outbound HTTP can drop them.

The README lists three artifacts. kotlin-sdk is the umbrella package with client and server APIs. kotlin-sdk-server and kotlin-sdk-client hold one side each. It also notes that the SDK "does not add Ktor engine dependencies transitively," so you declare the engine you want, such as ktor-server-netty or CIO.

Step 2: Write a stdio server with one tool

This bookshop server follows the stdio setup in the official tutorial and the addTool pattern in the README:

import io.ktor.utils.io.streams.asInput
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.CallToolResult
import io.modelcontextprotocol.kotlin.sdk.types.Implementation
import io.modelcontextprotocol.kotlin.sdk.types.ServerCapabilities
import io.modelcontextprotocol.kotlin.sdk.types.TextContent
import io.modelcontextprotocol.kotlin.sdk.types.ToolSchema
import kotlinx.coroutines.Job
import kotlinx.coroutines.runBlocking
import kotlinx.io.asSink
import kotlinx.io.buffered
import kotlinx.serialization.json.buildJsonObject
import kotlinx.serialization.json.jsonPrimitive
import kotlinx.serialization.json.put
import kotlinx.serialization.json.putJsonObject

val catalog = mapOf("Dune" to "Frank Herbert", "Neuromancer" to "William Gibson")

fun main() {
    val server = Server(
        Implementation(name = "bookshop", version = "1.0.0"),
        ServerOptions(
            capabilities = ServerCapabilities(tools = ServerCapabilities.Tools(listChanged = true)),
        ),
    )

    server.addTool(
        name = "search_books",
        description = "Search the catalog by title or author. Returns matching titles.",
        inputSchema = ToolSchema(
            properties = buildJsonObject {
                putJsonObject("query") {
                    put("type", "string")
                    put("description", "Part of a title or author name")
                }
            },
            required = listOf("query"),
        ),
    ) { request ->
        val query = request.arguments?.get("query")?.jsonPrimitive?.content?.lowercase()
            ?: return@addTool CallToolResult(content = listOf(TextContent("The 'query' argument is required.")))
        val hits = catalog.filter { (title, author) ->
            title.lowercase().contains(query) || author.lowercase().contains(query)
        }.keys
        CallToolResult(content = listOf(TextContent(hits.joinToString("\n").ifEmpty { "No matches." })))
    }

    val transport = StdioServerTransport(
        System.`in`.asInput(),
        System.out.asSink().buffered(),
    )

    runBlocking {
        val session = server.createSession(transport)
        val done = Job()
        session.onClose { done.complete() }
        done.join()
    }
}

What each part does:

  • Server(Implementation(...), ServerOptions(...)) creates the server and declares that it offers tools.
  • addTool takes a name, a description, a ToolSchema and a handler. The handler receives the request and returns a CallToolResult.
  • ToolSchema holds JSON Schema properties built with kotlinx.serialization's buildJsonObject. The description strings are all the model knows about each argument.
  • StdioServerTransport reads stdin and writes stdout. The tutorial passes the two streams positionally, as above.
  • createSession starts the session. The Job keeps main alive until the client closes the connection.

The README notes that listChanged = true should only be set "if your tool catalog can change at runtime and your server will emit notifications/tools/list_changed when it does." For a fixed set of tools, you can leave it out.

Step 3: Add more tools

Call addTool again for each tool. Keep each one focused on a single job with clear argument names. The README adds that long-running tools can report progress through the request context, and tools can trigger sampling when they need the client's model.

To report a problem the model should see, return a CallToolResult with a helpful text message, as the tutorial does for missing arguments. That gives the model a chance to fix its call and try again.

Step 4: Test with MCP Inspector

Build a runnable JAR with ./gradlew build (the tutorial's shadow plugin produces a -all.jar). Then use the MCP Inspector CLI:

npx @modelcontextprotocol/inspector --cli java -jar build/libs/bookshop-all.jar \
  --protocol-era legacy --method tools/list

npx @modelcontextprotocol/inspector --cli java -jar build/libs/bookshop-all.jar \
  --protocol-era legacy --method tools/call \
  --tool-name search_books --tool-arg query=dune

The --protocol-era legacy flag tells the Inspector to use the initialize-based handshake this SDK speaks. Run npx @modelcontextprotocol/inspector alone for the web UI. Our lesson on testing with MCP Inspector walks through it.

Step 5: Connect it to a client

For Claude Desktop, add the server under mcpServers in claude_desktop_config.json (macOS: ~/Library/Application Support/Claude/; Windows: %AppData%\Claude\):

{
  "mcpServers": {
    "bookshop": {
      "command": "java",
      "args": ["-jar", "/ABSOLUTE/PATH/TO/build/libs/bookshop-all.jar"]
    }
  }
}

Restart Claude Desktop after saving. The MCP config generator produces the same block for other clients.

Step 6: Serve it over Streamable HTTP with Ktor

For a remote server, the README's quickstart mounts the same Server on an embedded Ktor server:

import io.ktor.server.cio.CIO
import io.ktor.server.engine.embeddedServer
import io.modelcontextprotocol.kotlin.sdk.server.mcpStreamableHttp

fun main() {
    val server = createBookshopServer() // the Server and addTool calls from step 2
    embeddedServer(CIO, host = "127.0.0.1", port = 3000) {
        mcpStreamableHttp {
            server
        }
    }.start(wait = true)
}

The endpoint defaults to /mcp. Add a Ktor server engine to your dependencies first; this example imports CIO, so it needs io.ktor:ktor-server-cio. The README lists two helpers: mcpStreamableHttp() keeps sessions, and mcpStatelessStreamableHttp() does not. Both install ContentNegotiation for you, so do not install it yourself.

Connect from Claude Code with the form the README shows, claude mcp add --transport http <name> <url>, using your server's full URL, for example http://localhost:3000/mcp. If you connect from a browser-based client such as the Inspector web UI, the README says to install Ktor's CORS plugin and allow the Mcp-Session-Id and Mcp-Protocol-Version headers.

For production hosting choices, see our lesson on deploying a remote MCP server.

What changed in 0.16.0

The 0.16.0 release notes list breaking changes worth knowing before you copy older examples:

  • mcpStreamableHttp sessions now expire after sessionIdleTimeout (30 minutes by default) and are capped by maxSessions (10,000 by default). Clients of an expired session get 404 Not Found.
  • A GET without an Mcp-Session-Id header now gets 400 Bad Request, so point health checks at a separate route.
  • APIs deprecated in 0.9.0 to 0.13.0 no longer compile.
  • The named arguments of StdioServerTransport changed from inputStream/outputStream to input/output. Positional calls still compile.
  • The SSE transport is deprecated in favor of Streamable HTTP.
  • New experimental types for the 2026-07-28 server/discover request arrived, but the notes say "the client and server do not use them yet."

Common pitfalls

  • Printing to stdout. The official tutorial says never use println() in a stdio server. Use a logger that writes to stderr or a file.
  • Expecting 2026-07-28 support. The SDK's newest protocol version is 2025-11-25 in 0.16.0. The tutorial says clients that support only 2026-07-28 cannot connect.
  • Missing Ktor engine. The SDK does not pull in a Ktor engine. Add one explicitly.
  • Old SSE examples. Application.mcp and SseServerTransport are deprecated. Use mcpStreamableHttp.
  • Pinning loosely on a Tier 3 SDK. Tier 3 carries no stability promise. Pin an exact version and read release notes before upgrading.
  • Relative paths in client config. Use the absolute path to your JAR.

Next step

The free MCP course covers resources, prompts and OAuth step by step. If you need a Kotlin or JVM server built for your product, our MCP development team can help. To compare Kotlin with Java and the other languages, see the official MCP SDKs overview and our Java MCP server guide.

More in the MCP Guide

Free tools: server.json Validator · MCP Error Code Lookup

Latest research: State of MCP: October 2026

Frequently asked questions

Is the Kotlin MCP SDK official?

Yes. It lives in the modelcontextprotocol GitHub organization and is listed on the official SDK page. As of November 2026 it is Tier 3, which means no minimum conformance score and no response-time commitments.

Can I use the Kotlin SDK outside the JVM?

Yes. The README says it targets JVM, Native, JS and Wasm, and you can add it to commonMain in a Kotlin Multiplatform project. A client or server still needs a transport that works on that platform.

Does the Kotlin SDK support the 2026-07-28 spec?

Not yet as of 0.16.0. Its newest supported protocol version is 2025-11-25. The release added experimental types for the new server/discover request, but the server does not use them.

Should I use the Kotlin SDK or the Java SDK on the JVM?

The Java SDK is Tier 2 and has a preview 2026-07-28 server; the Kotlin SDK is Tier 3 with coroutine-friendly APIs and Ktor helpers. Pick by your stack and how much API churn you can absorb.

Why does my Kotlin server work with ./gradlew run but not in Claude Desktop?

Gradle may print to stdout, and any stray output breaks the JSON-RPC stream. Point the client at the built JAR with java -jar and an absolute path instead.

You Might Also like to Read

Get a FREE Website Audit

Enter your website and get a 20–30 page SEO, GEO, AEO & CRO audit within 24 hours. Completely free.

Don't have a site yet? Get in touch →

Ready to speak with a marketing expert?
Give us a call! (916) 866-7893

Websites Launched
1407
Websites Launched
Marketing Experts
29
Marketing Experts
Hours of Dedicated Work
204533
Hours of Dedicated Work
Irina S.Benjamin G.Jeanette M.David G.