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
dependabot[bot] 8a814292cf
chore(deps): bump zod from 4.3.6 to 4.4.3 in /integration-test/src/jvmTest/typescript in the all-dependencies group across 1 directory (#751)
Bumps the all-dependencies group with 1 update in the
/integration-test/src/jvmTest/typescript directory:
[zod](https://github.com/colinhacks/zod).

Updates `zod` from 4.3.6 to 4.4.3
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/colinhacks/zod/releases">zod's
releases</a>.</em></p>
<blockquote>
<h2>v4.4.3</h2>
<h2>Commits:</h2>
<ul>
<li>4c2fa95ce3f3390fbc522324e406b4e9e89b88f9 docs: use Zernio primary
wordmark for gold sponsor logo</li>
<li>2aeec83eb135e3a83756e973ef44845fc5a455d2 docs: prune lapsed gold
sponsors and rebalance logo sizing</li>
<li>7391be88ac1ee5cd02057f5ccc012a1f5df4efd0 docs: prune lapsed
silver/bronze sponsors and add active ones</li>
<li>2c703322a21b4e2b12f33f49ea8430c451a68b4f docs: normalize bronze
sponsor logos to github avatar pattern</li>
<li>9195250cab0e7950efe39c3926d6c203b4b0a170 docs: remove Mintlify from
bronze sponsors (churned)</li>
<li>b8dffe9e62f17e6571e6249d05cc5102b54d94e4 docs: remove Numeric and
Speakeasy (2+ missed monthly cycles)</li>
<li>1cab69383fcdeae2a366d5e2a2fc4d8fc765d168 fix(v4): restore catch
handling for absent object keys (<a
href="https://redirect.github.com/colinhacks/zod/issues/5937">#5937</a>)
(<a
href="https://redirect.github.com/colinhacks/zod/issues/5939">#5939</a>)</li>
<li>c2be4f819064eed62c7c350a2d399b5faecd15f8 fix(v4): generalize
optin/fallback to transform; restore preprocess on absent keys (<a
href="https://redirect.github.com/colinhacks/zod/issues/5941">#5941</a>)</li>
<li>f3c9ec03ba7a28ae72d25cc295f38674bee0f559 4.4.3</li>
<li>1fb56a5c18c27102dbc92260a4007c7732a0ccca docs: document release
procedure in AGENTS.md</li>
</ul>
<h2>v4.4.2</h2>
<h2>Commits:</h2>
<ul>
<li>0c62df0ea19fd05abdf90473e9eef7eea530fab2 Clean up docs navigation
and stale labels (<a
href="https://redirect.github.com/colinhacks/zod/issues/5901">#5901</a>)</li>
<li>20cc794895cc8604fe0c87d83a5d1c3f89fad0ac chore: add security policy
and refresh tooling deps</li>
<li>6fbe07b0177efdd1bf1c0b05160e70d7a0702337 fix(docs): heading anchor
links now include the hash so it doesnt scoll all the way up, follows
navbar logic (<a
href="https://redirect.github.com/colinhacks/zod/issues/5791">#5791</a>)</li>
<li>4bbed1b1c73eca4ce9e59b1189ed236aa6c8b5bd Tighten discriminated union
option typing</li>
<li>bbac3e567e7fccfaaf7cdc97f1ce30c295e2c908 Update PR guidance for
agents</li>
<li>cf0dc942a32805c292fff59ade20a7ace980735a Merge remote-tracking
branch 'origin/main' into fix-discriminated-union-key-constraint</li>
<li>292c894a5fd2aa42e527900b83d8d7a3009a709c docs: add Zernio gold
sponsor</li>
<li>1fc9f311c28dcf80d0bb5a36b177086cbc3d8eca docs: document codec
inversion</li>
<li>1373c85da9aeff704a9762d27bc58699618aefb7 docs: remove AI disclosure
guidance</li>
<li>e20d02b473c08e3a4e557bc610b1b5fac079b649 chore: ignore triage
notes</li>
<li>e58ea4d91b1dfe8194b73508203213cbc7e9c936 docs: test Zod Mini tab
code heights</li>
<li>905761a5d127e8d5dd2ebb3bc88c75cb0b8149ff docs: document preprocess
input type narrowing</li>
<li>bf64bac850d4dee2b7dde7e64909d5d796d32043 chore: tighten test
guidance in AGENTS.md</li>
<li>8ec4e73f4c4693b6361ad591be40fb41eb8a9f95 chore: update play.ts
scratch</li>
<li>02c2baf7d0d615872fa4528a8020603b71211702 Make z.preprocess defer
optionality to inner schema (<a
href="https://redirect.github.com/colinhacks/zod/issues/5929">#5929</a>)</li>
<li>88015df8e25c44fb5385eb3ef28935119cd5edea fix(docs): drop deprecated
<code>baseUrl</code> from tsconfig</li>
<li>c59d4474e3b4cad1b323462186cf607178ce8267 4.4.2</li>
</ul>
<h2>v4.4.1</h2>
<h2>Commits:</h2>
<ul>
<li>481f7be4238c83ed58183f921b2646f340a91c6a ci: gate release publishing
on full test workflow</li>
<li>95ccab423aec720b2523c3a64cdc7e3204537cc7 test(v3): restore optional
undefined expectations</li>
<li>cede2c63739a5823d6aa5093d291e9a111da943d fix(v4): reject tuple holes
before required defaults (<a
href="https://redirect.github.com/colinhacks/zod/issues/5900">#5900</a>)</li>
<li>edd0bf0f5ada4a8dc581c259407d7bbad0a71ea7 release: 4.4.1</li>
<li>180d83d1dbe6a59260710cc8637a3dea2281ee56 docs: remove Jazz featured
sponsor</li>
</ul>
<h2>v4.4.0</h2>
<h2>4.4.0</h2>
<p>This is a minor release with a wide set of correctness and soundness
fixes. Some fixes intentionally make Zod stricter, so code that depended
on previously accepted invalid or ambiguous inputs may need small
updates.</p>
<h2>Potentially breaking bug fixes</h2>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="1fb56a5c18"><code>1fb56a5</code></a>
docs: document release procedure in AGENTS.md</li>
<li><a
href="f3c9ec03ba"><code>f3c9ec0</code></a>
4.4.3</li>
<li><a
href="c2be4f8190"><code>c2be4f8</code></a>
fix(v4): generalize optin/fallback to transform; restore preprocess on
absent...</li>
<li><a
href="1cab69383f"><code>1cab693</code></a>
fix(v4): restore catch handling for absent object keys (<a
href="https://redirect.github.com/colinhacks/zod/issues/5937">#5937</a>)
(<a
href="https://redirect.github.com/colinhacks/zod/issues/5939">#5939</a>)</li>
<li><a
href="b8dffe9e62"><code>b8dffe9</code></a>
docs: remove Numeric and Speakeasy (2+ missed monthly cycles)</li>
<li><a
href="9195250cab"><code>9195250</code></a>
docs: remove Mintlify from bronze sponsors (churned)</li>
<li><a
href="2c703322a2"><code>2c70332</code></a>
docs: normalize bronze sponsor logos to github avatar pattern</li>
<li><a
href="7391be88ac"><code>7391be8</code></a>
docs: prune lapsed silver/bronze sponsors and add active ones</li>
<li><a
href="2aeec83eb1"><code>2aeec83</code></a>
docs: prune lapsed gold sponsors and rebalance logo sizing</li>
<li><a
href="4c2fa95ce3"><code>4c2fa95</code></a>
docs: use Zernio primary wordmark for gold sponsor logo</li>
<li>Additional commits viewable in <a
href="https://github.com/colinhacks/zod/compare/v4.3.6...v4.4.3">compare
view</a></li>
</ul>
</details>
<details>
<summary>Maintainer changes</summary>
<p>This version was pushed to npm by <a
href="https://www.npmjs.com/~GitHub%20Actions">GitHub Actions</a>, a new
releaser for zod since your current version.</p>
</details>
<br />

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-06 08:39:40 +00:00
.claude/skills refactor: explicit locked detekt config (#729) 2026-04-29 12:31:49 +02:00
.github refactor: explicit locked detekt config (#729) 2026-04-29 12:31:49 +02:00
buildSrc refactor: explicit locked detekt config (#729) 2026-04-29 12:31:49 +02:00
config/detekt refactor: explicit locked detekt config (#729) 2026-04-29 12:31:49 +02:00
conformance-test fix: enable DNS rebinding protection by default (#659) 2026-05-05 11:05:59 +02:00
docs docs: streamable server sample (#595) 2026-03-10 12:45:21 +01:00
gradle chore(deps): bump gradle-wrapper from 9.4.1 to 9.5.0 (#747) 2026-05-05 16:41:55 +02:00
integration-test chore(deps): bump zod from 4.3.6 to 4.4.3 in /integration-test/src/jvmTest/typescript in the all-dependencies group across 1 directory (#751) 2026-05-06 08:39:40 +00:00
kotlin-sdk refactor: explicit locked detekt config (#729) 2026-04-29 12:31:49 +02:00
kotlin-sdk-client refactor: explicit locked detekt config (#729) 2026-04-29 12:31:49 +02:00
kotlin-sdk-core refactor: explicit locked detekt config (#729) 2026-04-29 12:31:49 +02:00
kotlin-sdk-server fix: enable DNS rebinding protection by default (#659) 2026-05-05 11:05:59 +02:00
kotlin-sdk-testing refactor: explicit locked detekt config (#729) 2026-04-29 12:31:49 +02:00
samples chore(deps): bump mcp-kotlin from 0.11.1 to 0.12.0 in /samples/simple-streamable-server (#749) 2026-05-06 10:16:40 +02:00
test-utils refactor: explicit locked detekt config (#729) 2026-04-29 12:31:49 +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 refactor: explicit locked detekt config (#729) 2026-04-29 12:31:49 +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 version to 0.12.0 (#731) 2026-04-29 13:27:42 +02:00
gradlew chore(deps): bump gradle-wrapper from 9.4.1 to 9.5.0 (#747) 2026-05-05 16:41:55 +02:00
gradlew.bat chore(deps): bump gradle-wrapper from 9.4.1 to 9.5.0 (#747) 2026-05-05 16:41:55 +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.