Mirror of https://github.com/modelcontextprotocol/kotlin-sdk — The official Kotlin SDK for Model Context Protocol servers and clients. Maintained in collaboration with JetBrains
  • Kotlin 98%
  • TypeScript 1.7%
  • Shell 0.3%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
Amr Youssef a238fc0a2e
fix(server): evict stale GET SSE stream mapping on reconnect (#716)
## Motivation and Context

When a GET SSE stream disconnects and the client reconnects on the same
session, the new GET is silently rejected because the previous stream's
`STANDALONE_SSE_STREAM_ID` entry in `streamsMapping` hasn't been cleaned
up yet. Cleanup via `invokeOnCompletion` is asynchronous — there is a
race window where the stale entry persists and blocks reconnection.

Additionally, Ktor's `sse {}` handler commits 200 OK headers before
`handleGetRequest` runs, so the intended
`call.reject(HttpStatusCode.Conflict, ...)` cannot change the
already-committed HTTP status. On all tested engines (test host, CIO,
Netty), clients receive a 200 OK with an immediately-closed empty SSE
stream rather than a 409 Conflict. Server-side `CallLogging` reports 409
(from the response object, not the wire status), which is misleading.

### What this PR does

1. **Explicitly closes the old SSE session** before establishing a new
stream. If the old session is already dead, `close()` is a no-op. This
is the synchronous cleanup that the async `invokeOnCompletion` couldn't
provide.

2. **Replaces `invokeOnCompletion` with `try/finally`** around
`awaitCancellation()` — cleanup runs during cancellation propagation
(earlier in the lifecycle) rather than after job completion. An identity
check (`=== newContext`) ensures only this stream's entry is removed,
not a replacement's.

3. **Adds identity-based removal in `emitOnStream`** — a failed `send()`
on an old stream can no longer accidentally evict the new stream's
mapping.

4. **Rethrows `CancellationException`** instead of swallowing it,
preserving structured concurrency.

The TypeScript reference implementation avoids this race via synchronous
`ReadableStream.cancel()` cleanup. The Kotlin coroutine equivalent
(`invokeOnCompletion`) is inherently async. The Go SDK uses `defer
stream.release()` under a mutex for the same reason. The Java SDK uses
unconditional `AtomicReference.set()` replacement with
`compareAndExchange` on close.

Fixes #715

## How Has This Been Tested?

All tests run against latest `main` (`bf8dc6d`). ktlint and detekt pass
clean.

- 2 new unit tests (Ktor test host): reconnect after disconnect,
concurrent stream takeover
- 2 new integration tests (real CIO embedded server): same scenarios
over real HTTP
- Manual verification against a live Ktor+Netty MCP server (curl):
confirmed 200 OK on the wire with immediately-closed stream on reconnect
(pre-fix), live stream on reconnect (post-fix)
- All existing server unit tests pass (76 total)
- All existing integration tests pass (306 total)

## Breaking Changes

The server no longer returns 409 Conflict for a second GET SSE stream on
the same session. Instead, the old stream is explicitly closed and the
new stream takes over. The 409 was already unreachable as an HTTP status
— Ktor's `sse {}` handler commits 200 OK before the transport handler
runs on all tested engines (test host, CIO, Netty), so clients never
received 409 on the wire.

## Types of changes

- [x] Bug fix (non-breaking change which fixes an issue)
- [ ] 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

## Additional context

This bug likely became reachable after #681 (April 10) fixed the Netty
`appendSseHeaders` crash that previously prevented GET SSE from working
at all on Netty.

Cross-SDK comparison of how each SDK handles a second GET SSE on the
same session:

| SDK | Behavior | Cleanup mechanism | Stale mapping bug? |
|-----|----------|-------------------|-------------------|
| TypeScript (reference) | 409 Conflict | Synchronous
`ReadableStream.cancel()` | No |
| Go | 409 Conflict | `defer stream.release()` under mutex | No |
| Java | Replaces (no 409) | `AtomicReference.set()` +
`compareAndExchange` on close | No |
| C# | 400 Bad Request | Boolean flag, never reset (no reconnect
support) | No |
| Kotlin (before fix) | 409 (unreachable on wire) | Async
`invokeOnCompletion` | **Yes** |
| Kotlin (this PR) | Closes old + replaces | `try/finally` +
identity-based removal | No |

---------

Co-authored-by: devcrocod <devcrocod@gmail.com>
2026-04-29 12:09:05 +02:00
.claude/skills feat(docs): public api kdocs (#658) 2026-04-02 14:41:48 +02:00
.github chore(deps): bump actions/upload-pages-artifact from 4 to 5 in the all-actions group across 1 directory (#711) 2026-04-21 21:27:27 +00:00
buildSrc chore: remove redundant JUnit XML report configuration (#728) 2026-04-28 17:10:11 +02:00
config/detekt feat(docs): public api kdocs (#658) 2026-04-02 14:41:48 +02:00
conformance-test feat!: SEP-1577 sampling with tools (#718) 2026-04-28 17:51:48 +02:00
docs docs: streamable server sample (#595) 2026-03-10 12:45:21 +01:00
gradle chore(deps): bump the kotlin group across 1 directory with 2 updates (#723) 2026-04-27 17:03:48 +02:00
integration-test fix(server): evict stale GET SSE stream mapping on reconnect (#716) 2026-04-29 12:09:05 +02:00
kotlin-sdk Kotlin-MCP can't be used without ktor-bom starting from 0.7.5 (#390) (#392) 2025-11-12 15:32:54 +02:00
kotlin-sdk-client feat!: SEP-1577 sampling with tools (#718) 2026-04-28 17:51:48 +02:00
kotlin-sdk-core feat!: SEP-1577 sampling with tools (#718) 2026-04-28 17:51:48 +02:00
kotlin-sdk-server fix(server): evict stale GET SSE stream mapping on reconnect (#716) 2026-04-29 12:09:05 +02:00
kotlin-sdk-testing chore: drop deprecated watchosX64 and tvosX64 targets (#727) 2026-04-28 17:30:32 +02:00
samples chore(deps): bump kotlin from 2.2.21 to 2.3.21 in /samples/weather-stdio-server (#726) 2026-04-27 22:07:10 +02:00
test-utils chore: drop deprecated watchosX64 and tvosX64 targets (#727) 2026-04-28 17:30:32 +02:00
.editorconfig Detekt configuration (#501) 2026-02-04 17:02:58 +02:00
.gitignore feat: add mcp documentation skill for spec-aware development (#609) 2026-03-18 17:53:26 +01:00
AGENTS.md docs: update AGENTS.md (#612) 2026-03-20 15:01:58 +01:00
build.gradle.kts feat!: add mcp conformance test infra (#585); add SSE reconnection with retry support (#596) (#585) 2026-03-12 15:44:59 +02:00
CLAUDE.md docs: update AGENTS.md (#612) 2026-03-20 15:01:58 +01:00
CODE_OF_CONDUCT.md Update license 2024-12-16 21:20:35 +01:00
context7.json chore(ci): remove codecov (#691) 2026-04-14 13:14:30 +02:00
CONTRIBUTING.md Update package name across the project (#2) 2024-12-17 07:28:51 +01:00
gradle.properties release: bump mcp kotlin-sdk version to 0.11.1 (#682) 2026-04-10 15:30:20 +02:00
gradlew chore(deps): bump gradle-wrapper from 9.3.1 to 9.4.1 (#614) 2026-03-23 13:18:41 +02:00
gradlew.bat chore: #608 update kotlin and kotlinx versions (#611) 2026-03-27 19:37:26 +02:00
knit.properties docs: Update readme, enable Knit (#162) (#510) 2026-02-06 18:40:31 +02:00
LICENSE chore: update licensing to Apache 2.0 for new contributions (#481) 2026-01-13 14:21:52 +02:00
README.md feat(server): auto-install ContentNegotiation with McpJson (#664) (#665) 2026-04-02 11:01:13 +03:00
SECURITY.md Update SECURITY.md to use GitHub Security Advisories (#536) 2026-02-18 23:03:42 +02:00
settings.gradle.kts feat: Add kotlin-sdk-testing module with channel-based transport implementation (#505) 2026-02-16 14:10:56 +02:00

MCP Kotlin SDK

Maven Central Build

Kotlin Kotlin Multiplatform JVM License

Kotlin Multiplatform SDK for the Model Context Protocol. It enables Kotlin applications targeting JVM, Native, JS, and Wasm to implement MCP clients and servers using a standardized protocol interface.

Table of Contents

Overview

The Model Context Protocol allows applications to provide context for LLMs in a standardized way, separating the concerns of providing context from the actual LLM interaction. This Kotlin SDK implements the MCP specification, making it easy to:

  • Build MCP clients that can connect to any MCP server
  • Create MCP servers that expose resources, prompts, and tools
  • Target JVM, Native, JS, and Wasm from a single codebase
  • Use standard transports like stdio, SSE, Streamable HTTP, and WebSocket
  • Handle MCP protocol messages and lifecycle events with coroutine-friendly APIs

Installation

Artifacts

  • io.modelcontextprotocol:kotlin-sdk – umbrella SDK (client + server APIs)
  • io.modelcontextprotocol:kotlin-sdk-client – client-only APIs
  • io.modelcontextprotocol:kotlin-sdk-server – server-only APIs

Gradle setup (JVM)

Add the Maven Central repository and the SDK dependency:

repositories {
    mavenCentral()
}

dependencies {
    // See the badge above for the latest version
    implementation("io.modelcontextprotocol:kotlin-sdk:$mcpVersion")
}

Use kotlin-sdk-client or kotlin-sdk-server if you only need one side of the API:

dependencies {
    implementation("io.modelcontextprotocol:kotlin-sdk-client:$mcpVersion")
    implementation("io.modelcontextprotocol:kotlin-sdk-server:$mcpVersion")
}

Multiplatform

In a Kotlin Multiplatform project you can add the SDK to commonMain:

commonMain {
    dependencies {
        // Works as a common dependency as well as the platform one
        implementation("io.modelcontextprotocol:kotlin-sdk:$mcpVersion")
    }
}

Ktor dependencies

The Kotlin MCP SDK uses Ktor, but it does not add Ktor engine dependencies transitively. You need to declare Ktor client/server dependencies yourself (or reuse the ones already used in your project), for example:

dependencies {
    // MCP client with Ktor
    implementation("io.ktor:ktor-client-cio:$ktorVersion")
    implementation("io.modelcontextprotocol:kotlin-sdk-client:$mcpVersion")

    // MCP server with Ktor
    implementation("io.ktor:ktor-server-netty:$ktorVersion")
    implementation("io.modelcontextprotocol:kotlin-sdk-server:$mcpVersion")
}

Quickstart

Let's create a simple MCP client and server to demonstrate the basic usage of the Kotlin SDK.

Creating a Client

Create an MCP client that connects to a server via Streamable HTTP transport and lists available tools:

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.Implementation
import kotlinx.coroutines.runBlocking

fun main(args: Array<String>) = runBlocking {
    val url = args.firstOrNull() ?: "http://localhost:3000/mcp"

    val httpClient = HttpClient { install(SSE) }

    val client = Client(
        clientInfo = Implementation(
            name = "example-client",
            version = "1.0.0"
        )
    )

    val transport = StreamableHttpClientTransport(
        client = httpClient,
        url = url
    )

    // Connect to server
    client.connect(transport)

    // List available tools
    val tools = client.listTools().tools

    println(tools)
}

Creating a Server

Create an MCP server that exposes a simple tool and runs on an embedded Ktor server with Streamable HTTP transport. For a full working project with all required dependencies, see the simple-streamable-server sample.

import io.ktor.server.cio.CIO
import io.ktor.server.engine.embeddedServer
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.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.serialization.json.buildJsonObject
import kotlinx.serialization.json.put

fun main(args: Array<String>) {
    val port = args.firstOrNull()?.toIntOrNull() ?: 3000
    val mcpServer = Server(
        serverInfo = Implementation(
            name = "example-server",
            version = "1.0.0"
        ),
        options = ServerOptions(
            capabilities = ServerCapabilities(
                tools = ServerCapabilities.Tools(listChanged = true),
            ),
        )
    )

    mcpServer.addTool(
        name = "example-tool",
        description = "An example tool",
        inputSchema = ToolSchema(
            properties = buildJsonObject {
                put("input", buildJsonObject { put("type", "string") })
            }
        )
    ) { request ->
        CallToolResult(content = listOf(TextContent("Hello, world!")))
    }
    
    embeddedServer(CIO, host = "127.0.0.1", port = port) {
        mcpStreamableHttp {
            mcpServer
        }
    }.start(wait = true)
}

You can run the server and then connect to it using the client or test with the MCP Inspector:

npx -y @modelcontextprotocol/inspector

In the inspector UI, connect to http://localhost:3000/mcp.

Core Concepts

MCP Primitives

The MCP protocol defines core primitives that enable communication between servers and clients:

Primitive Server Role Client Role Description
Prompts Provides prompt templates with optional arguments Requests and uses prompts Interactive templates for LLM interactions
Resources Exposes data sources (files, API responses, etc.) Reads and subscribes to resources Contextual data for augmenting LLM context
Tools Defines executable functions Calls tools to perform actions Functions the LLM can invoke to take actions
Sampling Requests LLM completions from client Executes LLM calls and returns results Server-initiated LLM requests (reverse direction)

Capabilities

Capabilities define what features a server or client supports. They are declared during initialization and determine what operations are available.

Server Capabilities

Servers declare their capabilities to inform clients what features they provide:

Capability Feature Flags Description
prompts listChanged Prompt template management and notifications
resources subscribe
listChanged
Resource exposure, subscriptions, and update notifications
tools listChanged Tool discovery, execution, and list change notifications
logging - Server logging to client console
completions - Argument autocompletion suggestions
experimental Custom properties Non-standard experimental features

Client Capabilities

Clients declare their capabilities to inform servers what features they support:

Capability Feature Flags Description
sampling - Client can sample from an LLM (execute model requests)
roots listChanged Client exposes root directories and can notify of changes
elicitation - Client can display schema/form dialogs for structured input
experimental Custom properties Non-standard experimental features

Server Features

The Server API lets you wire prompts, resources, and tools with only a few lines of Kotlin. Each feature is registered up front and then resolved lazily when a client asks for it, so your handlers stay small and suspendable.

Prompts

Prompts are user-controlled templates that clients discover via prompts/list and fetch with prompts/get when a user chooses one (think slash commands or saved flows). They’re best for repeatable, structured starters rather than ad-hoc model calls.

val server = Server(
    serverInfo = Implementation(
        name = "example-server",
        version = "1.0.0"
    ),
    options = ServerOptions(
        capabilities = ServerCapabilities(
            prompts = ServerCapabilities.Prompts(listChanged = true),
        ),
    )
)

server.addPrompt(
    name = "code-review",
    description = "Ask the model to review a diff",
    arguments = listOf(
        PromptArgument(name = "diff", description = "Unified diff", required = true),
    ),
) { request ->
    GetPromptResult(
        description = "Quick code review helper",
        messages = listOf(
            PromptMessage(
                role = Role.User,
                content = TextContent(text = "Review this change:\n${request.arguments?.get("diff")}"),
            ),
        ),
    )
}

Use prompts for anything that deserves a template: bug triage questions, onboarding checklists, or saved searches. Set listChanged = true only if your prompt catalog can change at runtime and your server will emit notifications/prompts/list_changed when it does.

Resources

Resources are application-driven context that clients discover via resources/list or resources/templates/list, then fetch with resources/read. Register each one with a stable URI and return a ReadResourceResult when asked—contents can be text or binary blobs.

val server = Server(
    serverInfo = Implementation(
        name = "example-server",
        version = "1.0.0"
    ),
    options = ServerOptions(
        capabilities = ServerCapabilities(
            resources = ServerCapabilities.Resources(subscribe = true, listChanged = true),
        ),
    )
)

server.addResource(
    uri = "note://release/latest",
    name = "Release notes",
    description = "Last deployment summary",
    mimeType = "text/markdown",
) { request ->
    ReadResourceResult(
        contents = listOf(
            TextResourceContents(
                text = "Ship 42 reached production successfully.",
                uri = request.uri,
                mimeType = "text/markdown",
            ),
        ),
    )
}

Resources can be static text, generated JSON, or blobs—anything the client can surface to the user or inject into the model context. Set subscribe = true if you emit notifications/resources/updated for changes to specific URIs, and listChanged = true if you’ll send notifications/resources/list_changed when the catalog itself changes.

Tools

Tools are model-controlled capabilities the client exposes to the model. Clients discover them via tools/list, invoke them with tools/call, and your handlers receive JSON arguments, can emit streaming logs or progress, and return a CallToolResult. Keep a human in the loop for sensitive operations.

val server = Server(
    serverInfo = Implementation(
        name = "example-server",
        version = "1.0.0"
    ),
    options = ServerOptions(
        capabilities = ServerCapabilities(
            tools = ServerCapabilities.Tools(listChanged = true),
        ),
    )
)

server.addTool(
    name = "echo",
    description = "Return whatever the user sent back to them",
) { request ->
    val text = request.arguments?.get("text")?.jsonPrimitive?.content ?: "(empty)"
    CallToolResult(content = listOf(TextContent(text = "Echo: $text")))
}

Register as many tools as you need—long-running jobs can report progress via the request context, and tools can also trigger sampling (see below) when they need the client’s LLM. Set listChanged = true only if your tool catalog can change at runtime and your server will emit notifications/tools/list_changed when it does.

Completion

Completion provides argument suggestions for prompts or resource templates. Declare the completions capability and handle completion/complete requests to return up to 100 ranked values (include total/hasMore if you paginate).

val server = Server(
    serverInfo = Implementation(
        name = "example-server",
        version = "1.0.0"
    ),
    options = ServerOptions(
        capabilities = ServerCapabilities(
            completions = ServerCapabilities.Completions,
        ),
    )
)

val session = server.createSession(
    StdioServerTransport(
        inputStream = System.`in`.asSource().buffered(),
        outputStream = System.out.asSink().buffered()
    )
)

session.setRequestHandler<CompleteRequest>(Method.Defined.CompletionComplete) { request, _ ->
    val options = listOf("kotlin", "compose", "coroutine")
    val matches = options.filter { it.startsWith(request.argument.value.lowercase()) }

    CompleteResult(
        completion = CompleteResult.Completion(
            values = matches.take(3),
            total = matches.size,
            hasMore = matches.size > 3,
        ),
    )
}

Use context.arguments to refine suggestions for dependent fields (e.g., framework list filtered by chosen language).

Logging

Logging lets the server stream structured log notifications to the client using RFC 5424 levels (debug → emergency). Declare the logging capability; clients can raise the minimum level with logging/setLevel, and the server emits notifications/message with severity, optional logger name, and JSON data.

val server = Server(
    serverInfo = Implementation("example-server", "1.0.0"),
    options = ServerOptions(
        capabilities = ServerCapabilities(
            logging = ServerCapabilities.Logging,
        ),
    )
)

val session = server.createSession(
    StdioServerTransport(
        inputStream = System.`in`.asSource().buffered(),
        outputStream = System.out.asSink().buffered()
    )
)

session.sendLoggingMessage(
    LoggingMessageNotification(
        LoggingMessageNotificationParams(
            level = LoggingLevel.Info,
            logger = "startup",
            data = buildJsonObject { put("message", "Server started") },
        ),
    ),
)

Keep logs free of sensitive data, and expect clients to surface them in their own UI.

Pagination

List operations return paginated results with an opaque nextCursor, clients echo that cursor to fetch the next page. Supported list calls: resources/list, resources/templates/list, prompts/list, and tools/list. Treat cursors as opaque—don’t parse or persist them across sessions.

val server = Server(
    serverInfo = Implementation("example-server", "1.0.0"),
    options = ServerOptions(
        capabilities = ServerCapabilities(
            resources = ServerCapabilities.Resources(),
        ),
    )
)

val session = server.createSession(
    StdioServerTransport(
        inputStream = System.`in`.asSource().buffered(),
        outputStream = System.out.asSink().buffered()
    )
)

val resources = listOf(
    Resource(uri = "note://1", name = "Note 1", description = "First"),
    Resource(uri = "note://2", name = "Note 2", description = "Second"),
    Resource(uri = "note://3", name = "Note 3", description = "Third"),
)
val pageSize = 2

session.setRequestHandler<ListResourcesRequest>(Method.Defined.ResourcesList) { request, _ ->
    val start = request.params?.cursor?.toIntOrNull() ?: 0
    val page = resources.drop(start).take(pageSize)
    val next = if (start + page.size < resources.size) (start + page.size).toString() else null

    ListResourcesResult(
        resources = page,
        nextCursor = next,
    )
}

Include nextCursor only when more items remain an absent cursor ends pagination.

Client Features

Clients advertise their capabilities (roots, sampling, elicitation, etc.) during initialization. After that they can serve requests from the server while still initiating calls such as listTools or callTool.

Roots

Roots let the client declare where the server is allowed to operate. Declare the roots capability, respond to roots/list, and emit notifications/roots/list_changed if you set listChanged = true. URIs must be file:// paths.

val client = Client(
    clientInfo = Implementation("demo-client", "1.0.0"),
    options = ClientOptions(
        capabilities = ClientCapabilities(roots = ClientCapabilities.Roots(listChanged = true)),
    ),
)

client.addRoot(
    uri = "file:///Users/demo/projects",
    name = "Projects",
)
client.sendRootsListChanged()

Call addRoot/removeRoot whenever your file system view changes, and use sendRootsListChanged() to notify the server. Keep root lists user-controlled and revoke entries that are no longer authorized.

Sampling

Sampling lets the server ask the client to call its preferred LLM. Declare the sampling capability (and sampling.tools if you allow tool-enabled sampling), and handle sampling/createMessage. Keep a human in the loop for approvals.

val client = Client(
    clientInfo = Implementation("demo-client", "1.0.0"),
    options = ClientOptions(
        capabilities = ClientCapabilities(
            sampling = buildJsonObject { putJsonObject("tools") { } }, // drop tools if you don't support tool use
        ),
    ),
)

client.setRequestHandler<CreateMessageRequest>(Method.Defined.SamplingCreateMessage) { request, _ ->
    val content = request.messages.lastOrNull()?.content
    val prompt = if (content is TextContent) content.text else "your topic"
    CreateMessageResult(
        model = "gpt-4o-mini",
        role = Role.Assistant,
        content = TextContent(text = "Here is a short note about $prompt"),
    )
}

Inside the handler you can pick any model/provider, require approvals, or reject the request. If you don’t support tool use, omit sampling.tools from capabilities.

Transports

All transports share the same API surface, so you can change deployment style without touching business logic. Pick the transport that best matches where the server runs.

STDIO Transport

StdioClientTransport and StdioServerTransport tunnel MCP messages over stdin/stdout—perfect for editor plugins or CLI tooling that spawns a helper process. No networking setup is required.

Streamable HTTP Transport

StreamableHttpClientTransport and the Ktor mcpStreamableHttp() / mcpStatelessStreamableHttp() helpers expose MCP over a single HTTP endpoint with optional JSON-only or SSE streaming responses. This is the recommended choice for remote deployments and integrates nicely with proxies or service meshes.

These helpers automatically install ContentNegotiation with McpJson — do not install it yourself, or a warning will be logged. Both accept a path parameter (default: "/mcp") to mount the endpoint at any URL:

embeddedServer(CIO, port = 3000) {
    mcpStreamableHttp(path = "/api/mcp") {
        MyServer()
    }
}.start(wait = true)

CORS for browser-based clients (e.g. MCP Inspector): if you connect from a browser-based client you need to install the Ktor CORS plugin so that MCP-specific headers are allowed and exposed:

install(CORS) {
    anyHost() // restrict to specific origins in production
    allowMethod(HttpMethod.Options)
    allowMethod(HttpMethod.Get)
    allowMethod(HttpMethod.Post)
    allowMethod(HttpMethod.Delete)
    allowNonSimpleContentTypes = true
    allowHeader("Mcp-Session-Id")
    allowHeader("Mcp-Protocol-Version")
    exposeHeader("Mcp-Session-Id")
    exposeHeader("Mcp-Protocol-Version")
}

SSE Transport

Server-Sent Events remain available for backwards compatibility with older MCP clients. Two Ktor helpers are provided:

  • Application.mcp { } — installs SSE and ContentNegotiation with McpJson automatically, then registers MCP endpoints at /. Do not install ContentNegotiation yourself — the SDK handles it.
  • Route.mcp { } — registers MCP endpoints at the current route path; requires install(SSE) in the application first. Use this to host MCP alongside other routes or under a path prefix:
embeddedServer(CIO, port = 3000) {
    install(SSE)
    routing {
        route("/api/mcp") {
            mcp { MyServer() }
        }
    }
}.start(wait = true)

Prefer Streamable HTTP for new projects.

WebSocket Transport

WebSocketClientTransport plus the matching server utilities provide full-duplex, low-latency connections—useful when you expect lots of notifications or long-running sessions behind a reverse proxy that already terminates WebSockets.

ChannelTransport (testing)

ChannelTransport provides a simple, non-networked transport for testing and local development. It uses Kotlin coroutines channels to provide a full-duplex connection between a client and server, allowing for easy testing of MCP functionality without the need for network setup.

Connecting your server

  1. Start a sample HTTP server on port 3000:

    ./gradlew :samples:kotlin-mcp-server:run
    
  2. Connect with the MCP Inspector or Claude Desktop/Code:

    npx -y @modelcontextprotocol/inspector --connect http://localhost:3000
    # or
    claude mcp add --transport http kotlin-mcp http://localhost:3000
    
  3. In the Inspector, confirm prompts, tools, resources, completions, and logs show up. Iterate locally until you’re ready to host the server wherever you prefer.

Examples

The samples directory contains runnable projects demonstrating MCP server and client implementations with various transports. See the samples overview for a comparison table and detailed descriptions.

Documentation

Ask DeepWiki

Contributing

Please see the contribution guide and the Code of conduct before contributing.

License

This project is licensed under Apache 2.0 for new contributions, with existing code under MIT—see the LICENSE file for details.