├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── samples ├── weather-stdio-server │ ├── src │ │ ├── main │ │ │ └── kotlin │ │ │ │ └── io │ │ │ │ └── modelcontextprotocol │ │ │ │ └── sample │ │ │ │ └── server │ │ │ │ ├── main.kt │ │ │ │ └── WeatherApi.kt │ │ └── test │ │ │ └── kotlin │ │ │ └── io │ │ │ └── modelcontextprotocol │ │ │ └── sample │ │ │ └── client │ │ │ ├── FakeTest.kt │ │ │ └── ClientStdio.kt │ ├── gradle │ │ ├── wrapper │ │ │ ├── gradle-wrapper.jar │ │ │ └── gradle-wrapper.properties │ │ └── libs.versions.toml │ ├── gradle.properties │ ├── settings.gradle.kts │ ├── build.gradle.kts │ └── gradlew.bat ├── kotlin-mcp-client │ ├── gradle │ │ ├── wrapper │ │ │ ├── gradle-wrapper.jar │ │ │ └── gradle-wrapper.properties │ │ └── libs.versions.toml │ ├── gradle.properties │ ├── src │ │ └── main │ │ │ ├── resources │ │ │ └── simplelogger.properties │ │ │ └── kotlin │ │ │ └── io │ │ │ └── modelcontextprotocol │ │ │ └── sample │ │ │ └── client │ │ │ └── main.kt │ ├── build.gradle.kts │ ├── settings.gradle.kts │ ├── README.md │ └── gradlew.bat └── kotlin-mcp-server │ ├── gradle │ ├── wrapper │ │ ├── gradle-wrapper.jar │ │ └── gradle-wrapper.properties │ └── libs.versions.toml │ ├── gradle.properties │ ├── src │ ├── test │ │ ├── resources │ │ │ └── simplelogger.properties │ │ └── kotlin │ │ │ ├── McpServerType.kt │ │ │ ├── TestEnvironment.kt │ │ │ └── SseServerIntegrationTest.kt │ └── main │ │ ├── resources │ │ └── simplelogger.properties │ │ └── kotlin │ │ └── io │ │ └── modelcontextprotocol │ │ └── sample │ │ └── server │ │ └── main.kt │ ├── mcp-inspector-config.json │ ├── settings.gradle.kts │ ├── build.gradle.kts │ ├── gradlew.bat │ └── README.md ├── kotlin-sdk-core ├── src │ ├── commonMain │ │ └── kotlin │ │ │ └── io │ │ │ └── modelcontextprotocol │ │ │ └── kotlin │ │ │ └── sdk │ │ │ ├── internal │ │ │ └── utils.kt │ │ │ ├── types │ │ │ ├── McpDsl.kt │ │ │ ├── PingRequest.kt │ │ │ ├── McpException.kt │ │ │ ├── pingRequest.dsl.kt │ │ │ ├── roots.dsl.kt │ │ │ ├── methods.kt │ │ │ ├── logging.dsl.kt │ │ │ ├── jsonUtils.kt │ │ │ └── logging.kt │ │ │ ├── types.util.kt │ │ │ ├── shared │ │ │ ├── AbstractTransport.kt │ │ │ ├── Transport.kt │ │ │ ├── TransportSendOptions.kt │ │ │ └── ReadBuffer.kt │ │ │ └── ExperimentalMcpApi.kt │ ├── jvmMain │ │ └── kotlin │ │ │ └── io │ │ │ └── modelcontextprotocol │ │ │ └── kotlin │ │ │ └── sdk │ │ │ └── internal │ │ │ └── utils.jvm.kt │ ├── jsMain │ │ └── kotlin │ │ │ └── io │ │ │ └── modelcontextprotocol │ │ │ └── kotlin │ │ │ └── sdk │ │ │ └── internal │ │ │ └── utils.js.kt │ ├── wasmJsMain │ │ └── kotlin │ │ │ └── io │ │ │ └── modelcontextprotocol │ │ │ └── kotlin │ │ │ └── sdk │ │ │ └── internal │ │ │ └── utils.wasmJs.kt │ ├── nativeMain │ │ └── kotlin │ │ │ └── io │ │ │ └── modelcontextprotocol │ │ │ └── kotlin │ │ │ └── sdk │ │ │ └── internal │ │ │ └── utils.native.kt │ └── commonTest │ │ └── kotlin │ │ └── io │ │ └── modelcontextprotocol │ │ └── kotlin │ │ └── sdk │ │ ├── OldSchemaAudioContentSerializationTest.kt │ │ ├── OldSchemaCallToolResultUtilsTest.kt │ │ ├── shared │ │ ├── ReadBufferTest.kt │ │ └── OldSchemaReadBufferTest.kt │ │ ├── models │ │ ├── ProgressNotificationsTest.kt │ │ └── OldSchemaProgressNotificationsTest.kt │ │ └── types │ │ └── PingRequestTest.kt ├── Module.md └── .openapi-generator-ignore ├── kotlin-sdk ├── build.gradle.kts └── src │ └── commonMain │ └── kotlin │ └── io │ └── modelcontextprotocol │ └── kotlin │ └── sdk │ └── package.kt ├── kotlin-sdk-test ├── src │ ├── jvmTest │ │ └── kotlin │ │ │ └── io │ │ │ └── modelcontextprotocol │ │ │ └── kotlin │ │ │ └── sdk │ │ │ ├── integration │ │ │ ├── kotlin │ │ │ │ ├── sse │ │ │ │ │ ├── ToolIntegrationTestSse.kt │ │ │ │ │ ├── PromptIntegrationTestSse.kt │ │ │ │ │ ├── ResourceIntegrationTestSse.kt │ │ │ │ │ ├── OldSchemaToolIntegrationTestSse.kt │ │ │ │ │ ├── OldSchemaPromptIntegrationTestSse.kt │ │ │ │ │ └── OldSchemaResourceIntegrationTestSse │ │ │ │ └── stdio │ │ │ │ │ ├── ToolIntegrationTestStdio.kt │ │ │ │ │ ├── PromptIntegrationTestStdio.kt │ │ │ │ │ ├── ResourceIntegrationTestStdio.kt │ │ │ │ │ ├── OldSchemaToolIntegrationTestStdio.kt │ │ │ │ │ ├── OldSchemaPromptIntegrationTestStdio.kt │ │ │ │ │ └── OldSchemaResourceIntegrationTestStdio.kt │ │ │ └── typescript │ │ │ │ ├── stdio │ │ │ │ ├── TsClientKotlinServerTestStdio.kt │ │ │ │ ├── KotlinClientTsServerTestStdio.kt │ │ │ │ ├── OldSchemaKotlinClientTsServerTestStdio.kt │ │ │ │ └── simpleStdio.ts │ │ │ │ ├── sse │ │ │ │ ├── TsClientKotlinServerTestSse.kt │ │ │ │ ├── KotlinClientTsServerTestSse.kt │ │ │ │ └── OldSchemaKotlinClientTsServerTestSse.kt │ │ │ │ ├── AbstractTsClientKotlinServerTest.kt │ │ │ │ ├── AbstractKotlinClientTsServerTest.kt │ │ │ │ └── OldSchemaAbstractKotlinClientTsServerTest.kt │ │ │ └── server │ │ │ ├── OldSchemaAbstractServerFeaturesTest.kt │ │ │ ├── AbstractServerFeaturesTest.kt │ │ │ ├── OldSchemaServerInstructionsTest.kt │ │ │ ├── OldSchemaServerToolsTest.kt │ │ │ └── ServerInstructionsTest.kt │ └── commonTest │ │ └── kotlin │ │ └── io │ │ └── modelcontextprotocol │ │ └── kotlin │ │ └── sdk │ │ ├── client │ │ └── WebSocketTransportTest.kt │ │ ├── shared │ │ ├── InMemoryTransport.kt │ │ ├── OldSchemaInMemoryTransport.kt │ │ ├── OldSchemaBaseTransportTest.kt │ │ └── BaseTransportTest.kt │ │ └── integration │ │ └── OldSchemaInMemoryTransportTest.kt └── build.gradle.kts ├── kotlin-sdk-client ├── src │ ├── jvmTest │ │ ├── resources │ │ │ ├── junit-platform.properties │ │ │ └── simplelogger.properties │ │ └── kotlin │ │ │ └── io │ │ │ └── modelcontextprotocol │ │ │ └── kotlin │ │ │ └── sdk │ │ │ └── client │ │ │ └── AbstractStreamableHttpClientTest.kt │ ├── commonTest │ │ └── kotlin │ │ │ └── io │ │ │ └── modelcontextprotocol │ │ │ └── kotlin │ │ │ └── sdk │ │ │ └── client │ │ │ ├── stdio │ │ │ └── StdioClientTransportLifecycleTest.kt │ │ │ ├── streamable │ │ │ └── http │ │ │ │ └── StreamingHttpClientTransportLifecycleTest.kt │ │ │ ├── AbstractClientTransportLifecycleTest.kt │ │ │ ├── OldSchemaMockTransport.kt │ │ │ └── MockTransport.kt │ └── commonMain │ │ └── kotlin │ │ └── io │ │ └── modelcontextprotocol │ │ └── kotlin │ │ └── sdk │ │ └── client │ │ ├── WebSocketClientTransport.kt │ │ ├── WebSocketMcpKtorClientExtensions.kt │ │ ├── StreamableHttpMcpKtorClientExtensions.kt │ │ └── KtorClient.kt ├── Module.md └── build.gradle.kts ├── kotlin-sdk-server ├── Module.md ├── src │ └── commonMain │ │ └── kotlin │ │ └── io │ │ └── modelcontextprotocol │ │ └── kotlin │ │ └── sdk │ │ └── server │ │ ├── WebSocketMcpServerTransport.kt │ │ ├── EventStore.kt │ │ ├── Feature.kt │ │ └── ServerSessionRegistry.kt └── build.gradle.kts ├── settings.gradle.kts ├── .github ├── dependabot.yml └── workflows │ ├── apidocs.yaml │ ├── conformance.yml │ ├── samples.yml │ ├── gradle-publish.yml │ ├── codeql.yml │ └── build.yml ├── gradle.properties ├── .gitignore ├── LICENSE ├── .editorconfig ├── conformance-test ├── build.gradle.kts └── src │ └── test │ └── kotlin │ └── io │ └── modelcontextprotocol │ └── kotlin │ └── sdk │ └── conformance │ └── ConformanceClient.kt ├── gradlew.bat └── docs └── moduledoc.md /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/modelcontextprotocol/kotlin-sdk/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /samples/weather-stdio-server/src/main/kotlin/io/modelcontextprotocol/sample/server/main.kt: -------------------------------------------------------------------------------- 1 | package io.modelcontextprotocol.sample.server 2 | 3 | fun main() = runMcpServer() 4 | -------------------------------------------------------------------------------- /samples/kotlin-mcp-client/gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/modelcontextprotocol/kotlin-sdk/HEAD/samples/kotlin-mcp-client/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /samples/kotlin-mcp-server/gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/modelcontextprotocol/kotlin-sdk/HEAD/samples/kotlin-mcp-server/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /samples/kotlin-mcp-server/gradle.properties: -------------------------------------------------------------------------------- 1 | org.gradle.configuration-cache=true 2 | org.gradle.parallel=true 3 | org.gradle.caching=true 4 | 5 | #mcp.kotlin.overrideVersion=0.0.1-SNAPSHOT 6 | -------------------------------------------------------------------------------- /samples/weather-stdio-server/gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/modelcontextprotocol/kotlin-sdk/HEAD/samples/weather-stdio-server/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /samples/kotlin-mcp-client/gradle.properties: -------------------------------------------------------------------------------- 1 | org.gradle.configuration-cache=true 2 | org.gradle.parallel=true 3 | org.gradle.caching=true 4 | 5 | #mcp.kotlin.overrideVersion=0.0.1-SNAPSHOT 6 | 7 | -------------------------------------------------------------------------------- /samples/weather-stdio-server/gradle.properties: -------------------------------------------------------------------------------- 1 | org.gradle.configuration-cache=true 2 | org.gradle.parallel=true 3 | org.gradle.caching=true 4 | 5 | #mcp.kotlin.overrideVersion=0.0.1-SNAPSHOT 6 | -------------------------------------------------------------------------------- /kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/internal/utils.kt: -------------------------------------------------------------------------------- 1 | package io.modelcontextprotocol.kotlin.sdk.internal 2 | 3 | import kotlinx.coroutines.CoroutineDispatcher 4 | 5 | public expect val IODispatcher: CoroutineDispatcher 6 | -------------------------------------------------------------------------------- /kotlin-sdk-core/Module.md: -------------------------------------------------------------------------------- 1 | # Module kotlin-sdk-core 2 | 3 | The MCP Kotlin SDK Core module provides fundamental functionality for interacting with Model Context Protocol (MCP). 4 | This module serves as the foundation for building MCP-enabled applications in Kotlin. 5 | 6 | -------------------------------------------------------------------------------- /samples/weather-stdio-server/src/test/kotlin/io/modelcontextprotocol/sample/client/FakeTest.kt: -------------------------------------------------------------------------------- 1 | package io.modelcontextprotocol.sample.client 2 | 3 | import kotlin.test.Test 4 | 5 | class FakeTest { 6 | 7 | @Test 8 | fun testSomething() { 9 | assert(true) { "Bingo!" } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-9.2.0-bin.zip 4 | networkTimeout=10000 5 | validateDistributionUrl=true 6 | zipStoreBase=GRADLE_USER_HOME 7 | zipStorePath=wrapper/dists 8 | -------------------------------------------------------------------------------- /kotlin-sdk-core/src/jvmMain/kotlin/io/modelcontextprotocol/kotlin/sdk/internal/utils.jvm.kt: -------------------------------------------------------------------------------- 1 | package io.modelcontextprotocol.kotlin.sdk.internal 2 | 3 | import kotlinx.coroutines.CoroutineDispatcher 4 | import kotlinx.coroutines.Dispatchers 5 | 6 | public actual val IODispatcher: CoroutineDispatcher 7 | get() = Dispatchers.IO 8 | -------------------------------------------------------------------------------- /kotlin-sdk-core/src/jsMain/kotlin/io/modelcontextprotocol/kotlin/sdk/internal/utils.js.kt: -------------------------------------------------------------------------------- 1 | package io.modelcontextprotocol.kotlin.sdk.internal 2 | 3 | import kotlinx.coroutines.CoroutineDispatcher 4 | import kotlinx.coroutines.Dispatchers 5 | 6 | public actual val IODispatcher: CoroutineDispatcher 7 | get() = Dispatchers.Default 8 | -------------------------------------------------------------------------------- /kotlin-sdk-core/src/wasmJsMain/kotlin/io/modelcontextprotocol/kotlin/sdk/internal/utils.wasmJs.kt: -------------------------------------------------------------------------------- 1 | package io.modelcontextprotocol.kotlin.sdk.internal 2 | 3 | import kotlinx.coroutines.CoroutineDispatcher 4 | import kotlinx.coroutines.Dispatchers 5 | 6 | public actual val IODispatcher: CoroutineDispatcher 7 | get() = Dispatchers.Default 8 | -------------------------------------------------------------------------------- /samples/kotlin-mcp-client/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-9.2.0-bin.zip 4 | networkTimeout=10000 5 | validateDistributionUrl=true 6 | zipStoreBase=GRADLE_USER_HOME 7 | zipStorePath=wrapper/dists 8 | -------------------------------------------------------------------------------- /samples/kotlin-mcp-server/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-9.2.0-bin.zip 4 | networkTimeout=10000 5 | validateDistributionUrl=true 6 | zipStoreBase=GRADLE_USER_HOME 7 | zipStorePath=wrapper/dists 8 | -------------------------------------------------------------------------------- /samples/weather-stdio-server/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-9.2.0-bin.zip 4 | networkTimeout=10000 5 | validateDistributionUrl=true 6 | zipStoreBase=GRADLE_USER_HOME 7 | zipStorePath=wrapper/dists 8 | -------------------------------------------------------------------------------- /kotlin-sdk-core/src/nativeMain/kotlin/io/modelcontextprotocol/kotlin/sdk/internal/utils.native.kt: -------------------------------------------------------------------------------- 1 | package io.modelcontextprotocol.kotlin.sdk.internal 2 | 3 | import kotlinx.coroutines.CoroutineDispatcher 4 | import kotlinx.coroutines.Dispatchers 5 | import kotlinx.coroutines.IO 6 | 7 | public actual val IODispatcher: CoroutineDispatcher 8 | get() = Dispatchers.IO 9 | -------------------------------------------------------------------------------- /samples/kotlin-mcp-client/src/main/resources/simplelogger.properties: -------------------------------------------------------------------------------- 1 | # Level of logging for the ROOT logger: ERROR, WARN, INFO, DEBUG, TRACE (default is INFO) 2 | org.slf4j.simpleLogger.defaultLogLevel=INFO 3 | org.slf4j.simpleLogger.showThreadName=true 4 | org.slf4j.simpleLogger.showDateTime=false 5 | 6 | # Log level for specific packages or classes 7 | org.slf4j.simpleLogger.log.io.modelcontextprotocol=INFO 8 | -------------------------------------------------------------------------------- /kotlin-sdk/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | id("mcp.multiplatform") 3 | id("mcp.publishing") 4 | } 5 | 6 | kotlin { 7 | sourceSets { 8 | commonMain { 9 | dependencies { 10 | api(project(":kotlin-sdk-core")) 11 | api(project(":kotlin-sdk-client")) 12 | api(project(":kotlin-sdk-server")) 13 | } 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/kotlin/sse/ToolIntegrationTestSse.kt: -------------------------------------------------------------------------------- 1 | package io.modelcontextprotocol.kotlin.sdk.integration.kotlin.sse 2 | 3 | import io.modelcontextprotocol.kotlin.sdk.integration.kotlin.AbstractToolIntegrationTest 4 | 5 | class ToolIntegrationTestSse : AbstractToolIntegrationTest() { 6 | override val transportKind: TransportKind = TransportKind.SSE 7 | } 8 | -------------------------------------------------------------------------------- /kotlin-sdk-client/src/jvmTest/resources/junit-platform.properties: -------------------------------------------------------------------------------- 1 | ## https://docs.junit.org/5.3.0-M1/user-guide/index.html#writing-tests-parallel-execution 2 | junit.jupiter.execution.parallel.enabled=true 3 | junit.jupiter.execution.parallel.config.strategy=dynamic 4 | junit.jupiter.execution.parallel.mode.default=concurrent 5 | junit.jupiter.execution.parallel.mode.classes.default=concurrent 6 | junit.jupiter.execution.timeout.default=2m 7 | -------------------------------------------------------------------------------- /kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/kotlin/sse/PromptIntegrationTestSse.kt: -------------------------------------------------------------------------------- 1 | package io.modelcontextprotocol.kotlin.sdk.integration.kotlin.sse 2 | 3 | import io.modelcontextprotocol.kotlin.sdk.integration.kotlin.AbstractPromptIntegrationTest 4 | 5 | class SchemaPromptIntegrationTestSse : AbstractPromptIntegrationTest() { 6 | override val transportKind: TransportKind = TransportKind.SSE 7 | } 8 | -------------------------------------------------------------------------------- /kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/kotlin/stdio/ToolIntegrationTestStdio.kt: -------------------------------------------------------------------------------- 1 | package io.modelcontextprotocol.kotlin.sdk.integration.kotlin.stdio 2 | 3 | import io.modelcontextprotocol.kotlin.sdk.integration.kotlin.AbstractToolIntegrationTest 4 | 5 | class ToolIntegrationTestStdio : AbstractToolIntegrationTest() { 6 | override val transportKind: TransportKind = TransportKind.STDIO 7 | } 8 | -------------------------------------------------------------------------------- /kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/kotlin/sse/ResourceIntegrationTestSse.kt: -------------------------------------------------------------------------------- 1 | package io.modelcontextprotocol.kotlin.sdk.integration.kotlin.sse 2 | 3 | import io.modelcontextprotocol.kotlin.sdk.integration.kotlin.AbstractResourceIntegrationTest 4 | 5 | class ResourceIntegrationTestSse : AbstractResourceIntegrationTest() { 6 | override val transportKind: TransportKind = TransportKind.SSE 7 | } 8 | -------------------------------------------------------------------------------- /kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/kotlin/stdio/PromptIntegrationTestStdio.kt: -------------------------------------------------------------------------------- 1 | package io.modelcontextprotocol.kotlin.sdk.integration.kotlin.stdio 2 | 3 | import io.modelcontextprotocol.kotlin.sdk.integration.kotlin.AbstractPromptIntegrationTest 4 | 5 | class PromptIntegrationTestStdio : AbstractPromptIntegrationTest() { 6 | override val transportKind: TransportKind = TransportKind.STDIO 7 | } 8 | -------------------------------------------------------------------------------- /kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/kotlin/stdio/ResourceIntegrationTestStdio.kt: -------------------------------------------------------------------------------- 1 | package io.modelcontextprotocol.kotlin.sdk.integration.kotlin.stdio 2 | 3 | import io.modelcontextprotocol.kotlin.sdk.integration.kotlin.AbstractResourceIntegrationTest 4 | 5 | class ResourceIntegrationTestStdio : AbstractResourceIntegrationTest() { 6 | override val transportKind: TransportKind = TransportKind.STDIO 7 | } 8 | -------------------------------------------------------------------------------- /samples/kotlin-mcp-server/src/test/resources/simplelogger.properties: -------------------------------------------------------------------------------- 1 | # Level of logging for the ROOT logger: ERROR, WARN, INFO, DEBUG, TRACE (default is INFO) 2 | org.slf4j.simpleLogger.defaultLogLevel=INFO 3 | org.slf4j.simpleLogger.showThreadName=true 4 | org.slf4j.simpleLogger.showDateTime=false 5 | 6 | # Log level for specific packages or classes 7 | org.slf4j.simpleLogger.log.io.modelcontextprotocol=TRACE 8 | org.slf4j.simpleLogger.log.io.ktor=TRACE 9 | -------------------------------------------------------------------------------- /kotlin-sdk-client/Module.md: -------------------------------------------------------------------------------- 1 | # Module kotlin-sdk-client 2 | 3 | The Model Context Protocol (MCP) Kotlin SDK client module provides a multiplatform implementation for connecting to and 4 | interacting with MCP servers. 5 | 6 | ## Features 7 | 8 | - Cross-platform support for JVM, WASM, JS, and Native 9 | - Built-in support for multiple transport protocols (stdio, SSE, WebSocket) 10 | - Type-safe API for resource management and server interactions 11 | -------------------------------------------------------------------------------- /kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/kotlin/sse/OldSchemaToolIntegrationTestSse.kt: -------------------------------------------------------------------------------- 1 | package io.modelcontextprotocol.kotlin.sdk.integration.kotlin.sse 2 | 3 | import io.modelcontextprotocol.kotlin.sdk.integration.kotlin.OldSchemaAbstractToolIntegrationTest 4 | 5 | class OldSchemaToolIntegrationTestSse : OldSchemaAbstractToolIntegrationTest() { 6 | override val transportKind: TransportKind = TransportKind.SSE 7 | } 8 | -------------------------------------------------------------------------------- /kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/McpDsl.kt: -------------------------------------------------------------------------------- 1 | package io.modelcontextprotocol.kotlin.sdk.types 2 | 3 | /** 4 | * DSL marker annotation for MCP builder classes. 5 | * 6 | * This annotation is used to prevent accidental access to outer DSL scopes 7 | * within nested DSL blocks, ensuring type-safe and unambiguous builder usage. 8 | * 9 | * @see DslMarker 10 | */ 11 | @DslMarker 12 | public annotation class McpDsl 13 | -------------------------------------------------------------------------------- /kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/kotlin/sse/OldSchemaPromptIntegrationTestSse.kt: -------------------------------------------------------------------------------- 1 | package io.modelcontextprotocol.kotlin.sdk.integration.kotlin.sse 2 | 3 | import io.modelcontextprotocol.kotlin.sdk.integration.kotlin.OldSchemaAbstractPromptIntegrationTest 4 | 5 | class OldSchemaPromptIntegrationTestSse : OldSchemaAbstractPromptIntegrationTest() { 6 | override val transportKind: TransportKind = TransportKind.SSE 7 | } 8 | -------------------------------------------------------------------------------- /kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/kotlin/sse/OldSchemaResourceIntegrationTestSse: -------------------------------------------------------------------------------- 1 | package io.modelcontextprotocol.kotlin.sdk.integration.kotlin.sse 2 | 3 | import io.modelcontextprotocol.kotlin.sdk.integration.kotlin.OldSchemaAbstractResourceIntegrationTest 4 | 5 | class OldSchemaResourceIntegrationTestSse : OldSchemaAbstractResourceIntegrationTest() { 6 | override val transportKind: TransportKind = TransportKind.SSE 7 | } 8 | -------------------------------------------------------------------------------- /kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/kotlin/stdio/OldSchemaToolIntegrationTestStdio.kt: -------------------------------------------------------------------------------- 1 | package io.modelcontextprotocol.kotlin.sdk.integration.kotlin.stdio 2 | 3 | import io.modelcontextprotocol.kotlin.sdk.integration.kotlin.OldSchemaAbstractToolIntegrationTest 4 | 5 | class OldSchemaToolIntegrationTestStdio : OldSchemaAbstractToolIntegrationTest() { 6 | override val transportKind: TransportKind = TransportKind.STDIO 7 | } 8 | -------------------------------------------------------------------------------- /kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/kotlin/stdio/OldSchemaPromptIntegrationTestStdio.kt: -------------------------------------------------------------------------------- 1 | package io.modelcontextprotocol.kotlin.sdk.integration.kotlin.stdio 2 | 3 | import io.modelcontextprotocol.kotlin.sdk.integration.kotlin.OldSchemaAbstractPromptIntegrationTest 4 | 5 | class OldSchemaPromptIntegrationTestStdio : OldSchemaAbstractPromptIntegrationTest() { 6 | override val transportKind: TransportKind = TransportKind.STDIO 7 | } 8 | -------------------------------------------------------------------------------- /kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/kotlin/stdio/OldSchemaResourceIntegrationTestStdio.kt: -------------------------------------------------------------------------------- 1 | package io.modelcontextprotocol.kotlin.sdk.integration.kotlin.stdio 2 | 3 | import io.modelcontextprotocol.kotlin.sdk.integration.kotlin.OldSchemaAbstractResourceIntegrationTest 4 | 5 | class OldSchemaResourceIntegrationTestStdio : OldSchemaAbstractResourceIntegrationTest() { 6 | override val transportKind: TransportKind = TransportKind.STDIO 7 | } 8 | -------------------------------------------------------------------------------- /kotlin-sdk-client/src/jvmTest/resources/simplelogger.properties: -------------------------------------------------------------------------------- 1 | # Level of logging for the ROOT logger: ERROR, WARN, INFO, DEBUG, TRACE (default is INFO) 2 | org.slf4j.simpleLogger.defaultLogLevel=INFO 3 | org.slf4j.simpleLogger.showThreadName=true 4 | org.slf4j.simpleLogger.showDateTime=false 5 | 6 | # Log level for specific packages or classes 7 | org.slf4j.simpleLogger.log.io.ktor.server=DEBUG 8 | org.slf4j.simpleLogger.log.io.modelcontextprotocol=DEBUG 9 | org.slf4j.simpleLogger.log.dev.mokksy=DEBUG 10 | -------------------------------------------------------------------------------- /samples/kotlin-mcp-server/src/main/resources/simplelogger.properties: -------------------------------------------------------------------------------- 1 | # Level of logging for the ROOT logger: ERROR, WARN, INFO, DEBUG, TRACE (default is INFO) 2 | org.slf4j.simpleLogger.defaultLogLevel=INFO 3 | org.slf4j.simpleLogger.showThreadName=true 4 | org.slf4j.simpleLogger.showDateTime=false 5 | 6 | org.slf4j.simpleLogger.logFile=./build/stdout.log 7 | 8 | # Log level for specific packages or classes 9 | org.slf4j.simpleLogger.log.io.modelcontextprotocol=DEBUG 10 | org.slf4j.simpleLogger.log.io.ktor=DEBUG 11 | -------------------------------------------------------------------------------- /kotlin-sdk-server/Module.md: -------------------------------------------------------------------------------- 1 | # Module kotlin-sdk-server 2 | 3 | The server module provides core server-side functionality for the Model Context Protocol (MCP) Kotlin SDK. 4 | This module includes essential parts for writing MCP server applications. 5 | 6 | ## Features 7 | 8 | * Server-side request handling 9 | * Model context management 10 | * Protocol implementation 11 | * Integration utilities 12 | 13 | For detailed usage instructions and examples, please refer to 14 | the [documentation](https://github.com/modelcontextprotocol/kotlin-sdk/blob/main/README.md). 15 | -------------------------------------------------------------------------------- /samples/kotlin-mcp-client/src/main/kotlin/io/modelcontextprotocol/sample/client/main.kt: -------------------------------------------------------------------------------- 1 | package io.modelcontextprotocol.sample.client 2 | 3 | import kotlinx.coroutines.runBlocking 4 | 5 | fun main(args: Array) = runBlocking { 6 | require(args.isNotEmpty()) { 7 | "Usage: java -jar /build/libs/kotlin-mcp-client-0.1.0-all.jar " 8 | } 9 | val serverPath = args.first() 10 | val client = MCPClient() 11 | client.use { 12 | client.connectToServer(serverPath) 13 | client.chatLoop() 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /settings.gradle.kts: -------------------------------------------------------------------------------- 1 | rootProject.name = "kotlin-sdk" 2 | 3 | pluginManagement { 4 | repositories { 5 | mavenCentral() 6 | gradlePluginPortal() 7 | } 8 | 9 | plugins { 10 | id("org.gradle.toolchains.foojay-resolver-convention") version "1.0.0" 11 | } 12 | } 13 | 14 | dependencyResolutionManagement { 15 | repositories { 16 | mavenCentral() 17 | } 18 | } 19 | 20 | include( 21 | ":kotlin-sdk-core", 22 | ":kotlin-sdk-client", 23 | ":kotlin-sdk-server", 24 | ":kotlin-sdk", 25 | ":kotlin-sdk-test", 26 | ":conformance-test", 27 | ) 28 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # Configuration options: 2 | # https://docs.github.com/en/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file#about-the-dependabotyml-file 3 | 4 | version: 2 5 | updates: 6 | - package-ecosystem: "gradle" 7 | directory: "/" 8 | schedule: 9 | interval: "weekly" 10 | labels: 11 | - "dependencies" 12 | - "kotlin" 13 | 14 | - package-ecosystem: "github-actions" 15 | directory: "/" 16 | schedule: 17 | interval: "weekly" 18 | labels: 19 | - "dependencies" 20 | - "github-actions" 21 | -------------------------------------------------------------------------------- /kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/stdio/TsClientKotlinServerTestStdio.kt: -------------------------------------------------------------------------------- 1 | package io.modelcontextprotocol.kotlin.sdk.integration.typescript.stdio 2 | 3 | import io.modelcontextprotocol.kotlin.sdk.integration.typescript.AbstractTsClientKotlinServerTest 4 | import io.modelcontextprotocol.kotlin.sdk.integration.typescript.TransportKind 5 | 6 | class TsClientKotlinServerTestStdio : AbstractTsClientKotlinServerTest() { 7 | override val transportKind = TransportKind.STDIO 8 | override fun runClient(vararg args: String): String = runStdioClient(*args) 9 | } 10 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | org.gradle.jvmargs=-Xmx4g -Dfile.encoding=UTF-8 2 | org.gradle.parallel=true 3 | org.gradle.caching=true 4 | # Dokka 5 | org.jetbrains.dokka.experimental.gradle.pluginMode=V2Enabled 6 | org.jetbrains.dokka.experimental.gradle.pluginMode.noWarn=true 7 | # Kotlin 8 | kotlin.code.style=official 9 | kotlin.daemon.jvmargs=-Xmx4G 10 | # Build JS targets using npm package manager https://kotlinlang.org/docs/js-project-setup.html#npm-dependencies 11 | kotlin.js.yarn=false 12 | # MPP 13 | kotlin.mpp.enableCInteropCommonization=true 14 | kotlin.native.ignoreDisabledTargets=true 15 | 16 | group=io.modelcontextprotocol 17 | version=0.8.2-SNAPSHOT 18 | -------------------------------------------------------------------------------- /kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/PingRequest.kt: -------------------------------------------------------------------------------- 1 | package io.modelcontextprotocol.kotlin.sdk.types 2 | 3 | import kotlinx.serialization.EncodeDefault 4 | import kotlinx.serialization.ExperimentalSerializationApi 5 | import kotlinx.serialization.Serializable 6 | 7 | @Serializable 8 | public data class PingRequest(override val params: BaseRequestParams? = null) : 9 | ClientRequest, 10 | ServerRequest { 11 | @OptIn(ExperimentalSerializationApi::class) 12 | @EncodeDefault 13 | override val method: Method = Method.Defined.Ping 14 | 15 | public val meta: RequestMeta? 16 | get() = params?.meta 17 | } 18 | -------------------------------------------------------------------------------- /samples/kotlin-mcp-client/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | alias(libs.plugins.kotlin.jvm) 3 | alias(libs.plugins.shadow) 4 | application 5 | } 6 | 7 | application { 8 | mainClass.set("io.modelcontextprotocol.sample.client.MainKt") 9 | } 10 | 11 | group = "org.example" 12 | version = "0.1.0" 13 | 14 | dependencies { 15 | implementation(dependencies.platform(libs.ktor.bom)) 16 | implementation(libs.mcp.kotlin.client) 17 | implementation(libs.ktor.client.cio) 18 | implementation(libs.anthropic.java) 19 | implementation(libs.slf4j.simple) 20 | } 21 | 22 | tasks.test { 23 | useJUnitPlatform() 24 | } 25 | 26 | kotlin { 27 | jvmToolchain(17) 28 | } 29 | -------------------------------------------------------------------------------- /samples/kotlin-mcp-server/src/test/kotlin/McpServerType.kt: -------------------------------------------------------------------------------- 1 | import io.ktor.server.engine.EmbeddedServer 2 | import io.modelcontextprotocol.sample.server.runSseMcpServerUsingKtorPlugin 3 | import io.modelcontextprotocol.sample.server.runSseMcpServerWithPlainConfiguration 4 | 5 | enum class McpServerType( 6 | val sseEndpoint: String, 7 | val serverFactory: (port: Int) -> EmbeddedServer<*, *> 8 | ) { 9 | KTOR_PLUGIN( 10 | sseEndpoint = "", 11 | serverFactory = { port -> runSseMcpServerUsingKtorPlugin(port, wait = false) } 12 | ), 13 | PLAIN_CONFIGURATION( 14 | sseEndpoint = "/sse", 15 | serverFactory = { port -> runSseMcpServerWithPlainConfiguration(port, wait = false) } 16 | ) 17 | } 18 | -------------------------------------------------------------------------------- /samples/kotlin-mcp-server/mcp-inspector-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "mcpServers": { 3 | "stdio-server": { 4 | "command": "java", 5 | "args": [ 6 | "-Dorg.slf4j.simpleLogger.defaultLogLevel=off", 7 | "-jar", 8 | "./build/libs/kotlin-mcp-server-0.1.0-all.jar" 9 | ], 10 | "env": { 11 | }, 12 | "note": "For SSE connections, add this URL directly in your MCP Client" 13 | }, 14 | "sse-server": { 15 | "type": "sse", 16 | "url": "http://127.0.0.1:3001/sse", 17 | "note": "SSE with plain configuration" 18 | }, 19 | "sse-ktor-server": { 20 | "type": "sse", 21 | "url": "http://127.0.0.1:3002/", 22 | "note": "SSE with Ktor plugin" 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/McpException.kt: -------------------------------------------------------------------------------- 1 | package io.modelcontextprotocol.kotlin.sdk.types 2 | 3 | import kotlinx.serialization.json.JsonElement 4 | import kotlin.jvm.JvmOverloads 5 | 6 | /** 7 | * Represents an error specific to the MCP protocol. 8 | * 9 | * @property code The MCP/JSON‑RPC error code. 10 | * @property data Optional additional error payload as a JSON element; `null` when not provided. 11 | * @param message The error message. 12 | * @param cause The original cause. 13 | */ 14 | public class McpException @JvmOverloads public constructor( 15 | public val code: Int, 16 | message: String, 17 | public val data: JsonElement? = null, 18 | cause: Throwable? = null, 19 | ) : Exception("MCP error $code: $message", cause) 20 | -------------------------------------------------------------------------------- /samples/weather-stdio-server/settings.gradle.kts: -------------------------------------------------------------------------------- 1 | rootProject.name = "weather-stdio-server" 2 | 3 | pluginManagement { 4 | repositories { 5 | mavenCentral() 6 | gradlePluginPortal() 7 | } 8 | } 9 | 10 | dependencyResolutionManagement { 11 | repositories { 12 | mavenLocal() 13 | mavenCentral() 14 | } 15 | 16 | versionCatalogs { 17 | create("libs") { 18 | val mcpKotlinVersion = providers.gradleProperty( 19 | "mcp.kotlin.overrideVersion", 20 | ).orNull 21 | if (mcpKotlinVersion != null) { 22 | logger.lifecycle("Using the override version $mcpKotlinVersion of MCP Kotlin SDK") 23 | version("mcp-kotlin", mcpKotlinVersion) 24 | } 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /samples/kotlin-mcp-client/gradle/libs.versions.toml: -------------------------------------------------------------------------------- 1 | [versions] 2 | anthropic = "2.9.0" 3 | kotlin = "2.2.21" 4 | ktor = "3.2.3" 5 | mcp-kotlin = "0.8.1" 6 | shadow = "9.2.2" 7 | slf4j = "2.0.17" 8 | 9 | [libraries] 10 | anthropic-java = { group = "com.anthropic", name = "anthropic-java", version.ref = "anthropic" } 11 | ktor-bom = { group = "io.ktor", name = "ktor-bom", version.ref = "ktor" } 12 | ktor-client-cio = { group = "io.ktor", name = "ktor-client-cio" } 13 | mcp-kotlin-client = { group = "io.modelcontextprotocol", name = "kotlin-sdk-client", version.ref = "mcp-kotlin" } 14 | slf4j-simple = { group = "org.slf4j", name = "slf4j-simple", version.ref = "slf4j" } 15 | 16 | [plugins] 17 | kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" } 18 | shadow = { id = "com.gradleup.shadow", version.ref = "shadow" } 19 | -------------------------------------------------------------------------------- /samples/kotlin-mcp-client/settings.gradle.kts: -------------------------------------------------------------------------------- 1 | rootProject.name = "kotlin-mcp-client" 2 | 3 | plugins { 4 | // Apply the foojay-resolver plugin to allow automatic download of JDKs 5 | id("org.gradle.toolchains.foojay-resolver-convention") version "1.0.0" 6 | } 7 | 8 | dependencyResolutionManagement { 9 | repositories { 10 | mavenLocal() 11 | mavenCentral() 12 | } 13 | 14 | versionCatalogs { 15 | create("libs") { 16 | val mcpKotlinVersion = providers.gradleProperty( 17 | "mcp.kotlin.overrideVersion", 18 | ).orNull 19 | if (mcpKotlinVersion != null) { 20 | logger.lifecycle("Using the override version $mcpKotlinVersion of MCP Kotlin SDK") 21 | version("mcp-kotlin", mcpKotlinVersion) 22 | } 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /samples/kotlin-mcp-server/settings.gradle.kts: -------------------------------------------------------------------------------- 1 | rootProject.name = "kotlin-mcp-server" 2 | 3 | plugins { 4 | // Apply the foojay-resolver plugin to allow automatic download of JDKs 5 | id("org.gradle.toolchains.foojay-resolver-convention") version "1.0.0" 6 | } 7 | 8 | dependencyResolutionManagement { 9 | repositories { 10 | mavenLocal() 11 | mavenCentral() 12 | } 13 | 14 | versionCatalogs { 15 | create("libs") { 16 | val mcpKotlinVersion = providers.gradleProperty( 17 | "mcp.kotlin.overrideVersion", 18 | ).orNull 19 | if (mcpKotlinVersion != null) { 20 | logger.lifecycle("Using the override version $mcpKotlinVersion of MCP Kotlin SDK") 21 | version("mcp-kotlin", mcpKotlinVersion) 22 | } 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /kotlin-sdk/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/package.kt: -------------------------------------------------------------------------------- 1 | @file:Suppress("ktlint:standard:no-empty-class-body", "ktlint:standard:kdoc") 2 | /** 3 | * # MCP Kotlin SDK 4 | * 5 | * A Kotlin Multiplatform implementation of the Model Context Protocol (MCP). 6 | * 7 | * This is the main SDK module that provides a convenient single dependency 8 | * for all MCP functionality including: 9 | * 10 | * - Core protocol types and utilities ([kotlin-sdk-core]) 11 | * - Client implementations ([kotlin-sdk-client]) 12 | * - Server implementations ([kotlin-sdk-server]) 13 | * 14 | * ## Usage 15 | * 16 | * Add this dependency to your project to get access to all MCP Kotlin SDK functionality: 17 | * 18 | * ```kotlin 19 | * implementation("io.modelcontextprotocol:kotlin-sdk:$version") 20 | * ``` 21 | * 22 | * This will transitively include all core, client, and server components. 23 | */ 24 | 25 | package io.modelcontextprotocol.kotlin.sdk 26 | -------------------------------------------------------------------------------- /kotlin-sdk-client/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/stdio/StdioClientTransportLifecycleTest.kt: -------------------------------------------------------------------------------- 1 | package io.modelcontextprotocol.kotlin.sdk.client.stdio 2 | 3 | import io.modelcontextprotocol.kotlin.sdk.client.AbstractClientTransportLifecycleTest 4 | import io.modelcontextprotocol.kotlin.sdk.client.StdioClientTransport 5 | import kotlinx.io.Buffer 6 | import kotlin.test.Ignore 7 | import kotlin.test.Test 8 | 9 | class StdioClientTransportLifecycleTest : AbstractClientTransportLifecycleTest() { 10 | 11 | /** 12 | * Dummy method to make IDE treat this class as a test 13 | */ 14 | @Test 15 | @Ignore 16 | fun dummyTest() = Unit 17 | 18 | override fun createTransport(): StdioClientTransport { 19 | val inputBuffer = Buffer() 20 | val outputBuffer = Buffer() 21 | return StdioClientTransport( 22 | input = inputBuffer, 23 | output = outputBuffer, 24 | ) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /kotlin-sdk-core/.openapi-generator-ignore: -------------------------------------------------------------------------------- 1 | # OpenAPI Generator Ignore 2 | # Generated by openapi-generator https://github.com/openapitools/openapi-generator 3 | 4 | # Use this file to prevent files from being overwritten by the generator. 5 | # The patterns follow closely to .gitignore or .dockerignore. 6 | 7 | # As an example, the C# client generator defines ApiClient.cs. 8 | # You can make changes and tell OpenAPI Generator to ignore just this file by uncommenting the following line: 9 | **/auth/** 10 | **/infrastructure/** 11 | !**/Base64ByteArray.kt 12 | !**/Bytes.kt 13 | 14 | 15 | ## Exclude Models which are causing problems 16 | **/JSONRPCResponse.kt 17 | **/Result.kt 18 | **/ListToolsResult.kt 19 | **/InitializeRequest.kt 20 | **/InitializeResult.kt 21 | **/ServerCapabilities.kt 22 | **/InitializeRequestParams.kt 23 | **/CreateMessageRequest.kt 24 | **/CreateMessageRequestParams.kt 25 | **/ClientCapabilities.kt 26 | **/Tool.kt 27 | **/ToolInputSchema.kt 28 | **/ToolOutputSchema.kt 29 | 30 | -------------------------------------------------------------------------------- /samples/kotlin-mcp-server/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | alias(libs.plugins.kotlin.jvm) 3 | alias(libs.plugins.kotlin.serialization) 4 | alias(libs.plugins.shadow) 5 | application 6 | } 7 | 8 | group = "org.example" 9 | version = "0.1.0" 10 | 11 | application { 12 | mainClass.set("io.modelcontextprotocol.sample.server.MainKt") 13 | } 14 | 15 | dependencies { 16 | implementation(dependencies.platform(libs.ktor.bom)) 17 | implementation(libs.mcp.kotlin.server) 18 | implementation(libs.ktor.server.cio) 19 | implementation(libs.ktor.server.cors) 20 | implementation(libs.slf4j.simple) 21 | 22 | testImplementation(libs.mcp.kotlin.client) 23 | testImplementation(libs.ktor.client.cio) 24 | testImplementation(kotlin("test")) 25 | } 26 | 27 | tasks.test { 28 | useJUnitPlatform() 29 | } 30 | 31 | kotlin { 32 | jvmToolchain(17) 33 | compilerOptions { 34 | javaParameters = true 35 | freeCompilerArgs.addAll( 36 | "-Xdebug", 37 | ) 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .gradle 2 | build/ 3 | !gradle/wrapper/gradle-wrapper.jar 4 | !**/src/main/**/build/ 5 | !**/src/test/**/build/ 6 | 7 | ### IntelliJ IDEA ### 8 | .idea/modules.xml 9 | .idea/misc.xml 10 | .idea/jarRepositories.xml 11 | .idea/compiler.xml 12 | .idea/libraries/ 13 | .idea 14 | !.idea/icon.png 15 | *.iws 16 | *.iml 17 | *.ipr 18 | out/ 19 | !**/src/main/**/out/ 20 | !**/src/test/**/out/ 21 | 22 | ### Kotlin ### 23 | .kotlin 24 | kotlin-js-store 25 | yarn.lock 26 | 27 | ### Eclipse ### 28 | .apt_generated 29 | .classpath 30 | .factorypath 31 | .project 32 | .settings 33 | .springBeans 34 | .sts4-cache 35 | bin/ 36 | !**/src/main/**/bin/ 37 | !**/src/test/**/bin/ 38 | 39 | ### NetBeans ### 40 | /nbproject/private/ 41 | /nbbuild/ 42 | /dist/ 43 | /nbdist/ 44 | /.nb-gradle/ 45 | 46 | ### VS Code ### 47 | .vscode/ 48 | 49 | ### Mac OS ### 50 | .DS_Store 51 | 52 | ### Node.js ### 53 | node_modules 54 | dist 55 | 56 | ### SWE agents ### 57 | .claude/ 58 | .junie/ 59 | 60 | ### Conformance test results ### 61 | conformance-test/results/ 62 | -------------------------------------------------------------------------------- /samples/kotlin-mcp-server/src/main/kotlin/io/modelcontextprotocol/sample/server/main.kt: -------------------------------------------------------------------------------- 1 | package io.modelcontextprotocol.sample.server 2 | 3 | import kotlinx.coroutines.runBlocking 4 | 5 | /** 6 | * Start sse-server mcp on port 3001. 7 | * 8 | * @param args 9 | * - "--stdio": Runs an MCP server using standard input/output. 10 | * - "--sse-server-ktor ": Runs an SSE MCP server using Ktor plugin (default if no argument is provided). 11 | * - "--sse-server ": Runs an SSE MCP server with a plain configuration. 12 | */ 13 | fun main(vararg args: String): Unit = runBlocking { 14 | val command = args.firstOrNull() ?: "--stdio" 15 | val port = args.getOrNull(1)?.toIntOrNull() ?: 3001 16 | when (command) { 17 | "--stdio" -> runMcpServerUsingStdio() 18 | 19 | "--sse-server-ktor" -> runSseMcpServerUsingKtorPlugin(port) 20 | 21 | "--sse-server" -> runSseMcpServerWithPlainConfiguration(port) 22 | 23 | else -> { 24 | error("Unknown command: $command") 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/stdio/KotlinClientTsServerTestStdio.kt: -------------------------------------------------------------------------------- 1 | package io.modelcontextprotocol.kotlin.sdk.integration.typescript.stdio 2 | 3 | import io.modelcontextprotocol.kotlin.sdk.client.Client 4 | import io.modelcontextprotocol.kotlin.sdk.integration.typescript.AbstractKotlinClientTsServerTest 5 | import io.modelcontextprotocol.kotlin.sdk.integration.typescript.TransportKind 6 | 7 | class KotlinClientTsServerTestStdio : AbstractKotlinClientTsServerTest() { 8 | 9 | override val transportKind = TransportKind.STDIO 10 | 11 | override suspend fun useClient(block: suspend (Client) -> T): T = withClientStdio { client, proc -> 12 | try { 13 | block(client) 14 | } finally { 15 | try { 16 | client.close() 17 | } catch (_: Exception) {} 18 | try { 19 | stopProcess(proc, name = "TypeScript stdio server") 20 | } catch (_: Exception) {} 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /samples/kotlin-mcp-server/gradle/libs.versions.toml: -------------------------------------------------------------------------------- 1 | [versions] 2 | kotlin = "2.2.21" 3 | ktor = "3.2.3" 4 | mcp-kotlin = "0.8.1" 5 | slf4j = "2.0.17" 6 | shadow = "9.2.2" 7 | 8 | [libraries] 9 | ktor-bom = { group = "io.ktor", name = "ktor-bom", version.ref = "ktor" } 10 | ktor-client-cio = { group = "io.ktor", name = "ktor-client-cio" } 11 | ktor-server-cio = { group = "io.ktor", name = "ktor-server-cio" } 12 | ktor-server-cors = { group = "io.ktor", name = "ktor-server-cors" } 13 | mcp-kotlin-client = { group = "io.modelcontextprotocol", name = "kotlin-sdk-client", version.ref = "mcp-kotlin" } 14 | mcp-kotlin-server = { group = "io.modelcontextprotocol", name = "kotlin-sdk-server", version.ref = "mcp-kotlin" } 15 | slf4j-simple = { group = "org.slf4j", name = "slf4j-simple", version.ref = "slf4j" } 16 | 17 | [plugins] 18 | kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" } 19 | kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" } 20 | shadow = { id = "com.gradleup.shadow", version.ref = "shadow" } 21 | -------------------------------------------------------------------------------- /kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/stdio/OldSchemaKotlinClientTsServerTestStdio.kt: -------------------------------------------------------------------------------- 1 | package io.modelcontextprotocol.kotlin.sdk.integration.typescript.stdio 2 | 3 | import io.modelcontextprotocol.kotlin.sdk.client.Client 4 | import io.modelcontextprotocol.kotlin.sdk.integration.typescript.OldSchemaAbstractKotlinClientTsServerTest 5 | import io.modelcontextprotocol.kotlin.sdk.integration.typescript.OldSchemaTransportKind 6 | 7 | class OldSchemaKotlinClientTsServerTestStdio : OldSchemaAbstractKotlinClientTsServerTest() { 8 | 9 | override val transportKind = OldSchemaTransportKind.STDIO 10 | 11 | override suspend fun useClient(block: suspend (Client) -> T): T = withClientStdio { client, proc -> 12 | try { 13 | block(client) 14 | } finally { 15 | try { 16 | client.close() 17 | } catch (_: Exception) {} 18 | try { 19 | stopProcess(proc, name = "TypeScript stdio server") 20 | } catch (_: Exception) {} 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /samples/weather-stdio-server/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | alias(libs.plugins.kotlin.jvm) 3 | alias(libs.plugins.kotlin.serialization) 4 | alias(libs.plugins.shadow) 5 | application 6 | } 7 | 8 | application { 9 | mainClass.set("io.modelcontextprotocol.sample.server.MainKt") 10 | } 11 | 12 | group = "org.example" 13 | version = "0.1.0" 14 | 15 | dependencies { 16 | implementation(dependencies.platform(libs.ktor.bom)) 17 | implementation(libs.ktor.client.content.negotiation) 18 | implementation(libs.ktor.serialization.kotlinx.json) 19 | implementation(libs.mcp.kotlin.server) 20 | implementation(libs.ktor.server.cio) 21 | implementation(libs.ktor.client.cio) 22 | implementation(libs.slf4j.simple) 23 | runtimeOnly(libs.kotlin.logging) 24 | runtimeOnly(libs.kotlinx.collections.immutable) 25 | 26 | testImplementation(kotlin("test")) 27 | 28 | testImplementation(libs.mcp.kotlin.client) 29 | testImplementation(libs.kotlinx.coroutines.test) 30 | } 31 | 32 | tasks.test { 33 | useJUnitPlatform() 34 | } 35 | 36 | kotlin { 37 | jvmToolchain(17) 38 | } 39 | -------------------------------------------------------------------------------- /kotlin-sdk-test/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | id("mcp.multiplatform") 3 | } 4 | 5 | kotlin { 6 | jvm { 7 | tasks.withType { 8 | useJUnitPlatform() 9 | } 10 | } 11 | sourceSets { 12 | commonTest { 13 | dependencies { 14 | implementation(project(":kotlin-sdk")) 15 | implementation(kotlin("test")) 16 | implementation(libs.kotest.assertions.core) 17 | implementation(libs.kotest.assertions.json) 18 | implementation(libs.kotlin.logging) 19 | implementation(libs.kotlinx.coroutines.test) 20 | implementation(libs.ktor.client.cio) 21 | implementation(libs.ktor.server.cio) 22 | implementation(libs.ktor.server.websockets) 23 | implementation(libs.ktor.server.test.host) 24 | } 25 | } 26 | jvmTest { 27 | dependencies { 28 | implementation(libs.awaitility) 29 | runtimeOnly(libs.slf4j.simple) 30 | } 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Anthropic, PBC 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/OldSchemaAudioContentSerializationTest.kt: -------------------------------------------------------------------------------- 1 | package io.modelcontextprotocol.kotlin.sdk 2 | 3 | import io.kotest.assertions.json.shouldEqualJson 4 | import io.modelcontextprotocol.kotlin.sdk.shared.McpJson 5 | import kotlin.test.Test 6 | import kotlin.test.assertEquals 7 | 8 | class OldSchemaAudioContentSerializationTest { 9 | 10 | private val audioContentJson = """ 11 | { 12 | "data": "base64-encoded-audio-data", 13 | "mimeType": "audio/wav", 14 | "type": "audio" 15 | } 16 | """.trimIndent() 17 | 18 | private val audioContent = AudioContent( 19 | data = "base64-encoded-audio-data", 20 | mimeType = "audio/wav", 21 | ) 22 | 23 | @Test 24 | fun `should serialize audio content`() { 25 | McpJson.encodeToString(audioContent) shouldEqualJson audioContentJson 26 | } 27 | 28 | @Test 29 | fun `should deserialize audio content`() { 30 | val content = McpJson.decodeFromString(audioContentJson) 31 | assertEquals(expected = audioContent, actual = content) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/WebSocketMcpServerTransport.kt: -------------------------------------------------------------------------------- 1 | package io.modelcontextprotocol.kotlin.sdk.server 2 | 3 | import io.github.oshai.kotlinlogging.KotlinLogging 4 | import io.ktor.http.HttpHeaders 5 | import io.ktor.server.websocket.WebSocketServerSession 6 | import io.modelcontextprotocol.kotlin.sdk.shared.MCP_SUBPROTOCOL 7 | import io.modelcontextprotocol.kotlin.sdk.shared.WebSocketMcpTransport 8 | 9 | private val logger = KotlinLogging.logger {} 10 | 11 | /** 12 | * Server-side implementation of the MCP (Model Context Protocol) transport over WebSocket. 13 | * 14 | * @property session The WebSocket server session used for communication. 15 | */ 16 | public class WebSocketMcpServerTransport(override val session: WebSocketServerSession) : WebSocketMcpTransport() { 17 | override suspend fun initializeSession() { 18 | logger.debug { "Checking session headers" } 19 | val subprotocol = session.call.request.headers[HttpHeaders.SecWebSocketProtocol] 20 | if (subprotocol != MCP_SUBPROTOCOL) { 21 | error("Invalid subprotocol: $subprotocol, expected $MCP_SUBPROTOCOL") 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | insert_final_newline = true 7 | indent_style = space 8 | indent_size = 4 9 | max_line_length = 120 10 | 11 | [*.json] 12 | indent_size = 2 13 | 14 | [{*.yaml,*.yml}] 15 | indent_size = 2 16 | 17 | [*.{kt,kts}] 18 | ij_kotlin_code_style_defaults = KOTLIN_OFFICIAL 19 | 20 | # Disable wildcard imports entirely 21 | ij_kotlin_name_count_to_use_star_import = 2147483647 22 | ij_kotlin_name_count_to_use_star_import_for_members = 2147483647 23 | ij_kotlin_packages_to_use_import_on_demand = unset 24 | 25 | ktlint_code_style = intellij_idea 26 | ktlint_experimental = enabled 27 | ktlint_standard_filename = disabled 28 | ktlint_standard_no-empty-first-line-in-class-body = disabled 29 | ktlint_class_signature_rule_force_multiline_when_parameter_count_greater_or_equal_than = 4 30 | ktlint_function_signature_rule_force_multiline_when_parameter_count_greater_or_equal_than = 4 31 | ktlint_standard_chain-method-continuation = disabled 32 | ktlint_ignore_back_ticked_identifier = true 33 | ktlint_standard_multiline-expression-wrapping = disabled 34 | ktlint_standard_when-entry-bracing = disabled 35 | 36 | [*/build/**/*] 37 | ktlint = disabled -------------------------------------------------------------------------------- /conformance-test/build.gradle.kts: -------------------------------------------------------------------------------- 1 | import org.gradle.api.tasks.testing.logging.TestExceptionFormat 2 | 3 | plugins { 4 | kotlin("jvm") 5 | } 6 | 7 | dependencies { 8 | testImplementation(project(":kotlin-sdk")) 9 | testImplementation(kotlin("test")) 10 | testImplementation(libs.kotlin.logging) 11 | testImplementation(libs.ktor.client.cio) 12 | testImplementation(libs.ktor.server.cio) 13 | testImplementation(libs.ktor.server.websockets) 14 | testRuntimeOnly(libs.slf4j.simple) 15 | } 16 | 17 | tasks.test { 18 | useJUnitPlatform() 19 | 20 | testLogging { 21 | events("passed", "skipped", "failed") 22 | showStandardStreams = true 23 | showExceptions = true 24 | showCauses = true 25 | showStackTraces = true 26 | exceptionFormat = TestExceptionFormat.FULL 27 | } 28 | 29 | doFirst { 30 | systemProperty("test.classpath", classpath.asPath) 31 | 32 | println("\n" + "=".repeat(60)) 33 | println("MCP CONFORMANCE TESTS") 34 | println("=".repeat(60)) 35 | println("These tests validate compliance with the MCP specification.") 36 | println("=".repeat(60) + "\n") 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /kotlin-sdk-client/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/AbstractStreamableHttpClientTest.kt: -------------------------------------------------------------------------------- 1 | package io.modelcontextprotocol.kotlin.sdk.client 2 | 3 | import io.ktor.client.HttpClient 4 | import io.ktor.client.engine.apache5.Apache5 5 | import io.ktor.client.plugins.logging.LogLevel 6 | import io.ktor.client.plugins.logging.Logging 7 | import io.ktor.client.plugins.sse.SSE 8 | import org.junit.jupiter.api.AfterEach 9 | import org.junit.jupiter.api.TestInstance 10 | 11 | @TestInstance(TestInstance.Lifecycle.PER_CLASS) 12 | internal abstract class AbstractStreamableHttpClientTest { 13 | 14 | // start mokksy on random port 15 | protected val mockMcp: MockMcp = MockMcp(verbose = true) 16 | 17 | @AfterEach 18 | fun afterEach() { 19 | mockMcp.checkForUnmatchedRequests() 20 | } 21 | 22 | protected suspend fun connect(client: Client) { 23 | client.connect( 24 | StreamableHttpClientTransport( 25 | url = mockMcp.url, 26 | client = HttpClient(Apache5) { 27 | install(SSE) 28 | install(Logging) { 29 | level = LogLevel.ALL 30 | } 31 | }, 32 | ), 33 | ) 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/EventStore.kt: -------------------------------------------------------------------------------- 1 | package io.modelcontextprotocol.kotlin.sdk.server 2 | 3 | import io.modelcontextprotocol.kotlin.sdk.types.JSONRPCMessage 4 | 5 | /** 6 | * Interface for resumability support via event storage 7 | */ 8 | public interface EventStore { 9 | /** 10 | * Stores an event for later retrieval 11 | * @param streamId ID of the stream the event belongs to 12 | * @param message The JSON-RPC message to store 13 | * @returns The generated event ID for the stored event 14 | */ 15 | public suspend fun storeEvent(streamId: String, message: JSONRPCMessage): String 16 | 17 | /** 18 | * Replays events after the specified event ID 19 | * @param lastEventId The last event ID that was received 20 | * @param sender Function to send events 21 | * @return The stream ID for the replayed events 22 | */ 23 | public suspend fun replayEventsAfter( 24 | lastEventId: String, 25 | sender: suspend (eventId: String, message: JSONRPCMessage) -> Unit, 26 | ): String 27 | 28 | /** 29 | * Returns the stream ID associated with [eventId], or null if the event is unknown. 30 | * Default implementation is a no-op which disables extra validation during replay. 31 | */ 32 | public suspend fun getStreamIdForEventId(eventId: String): String? 33 | } 34 | -------------------------------------------------------------------------------- /kotlin-sdk-server/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | id("mcp.multiplatform") 3 | id("mcp.publishing") 4 | id("mcp.dokka") 5 | alias(libs.plugins.kotlinx.binary.compatibility.validator) 6 | } 7 | 8 | kotlin { 9 | sourceSets { 10 | commonMain { 11 | dependencies { 12 | api(project(":kotlin-sdk-core")) 13 | api(libs.ktor.server.core) 14 | api(libs.ktor.server.sse) 15 | implementation(libs.ktor.server.websockets) 16 | implementation(libs.kotlin.logging) 17 | } 18 | } 19 | 20 | commonTest { 21 | dependencies { 22 | implementation(kotlin("test")) 23 | implementation(libs.kotlinx.coroutines.test) 24 | } 25 | } 26 | 27 | jvmTest { 28 | dependencies { 29 | implementation(libs.ktor.client.logging) 30 | implementation(libs.ktor.server.content.negotiation) 31 | implementation(libs.ktor.client.content.negotiation) 32 | implementation(libs.ktor.serialization) 33 | implementation(libs.ktor.server.test.host) 34 | implementation(libs.kotest.assertions.core) 35 | implementation(libs.kotest.assertions.json) 36 | runtimeOnly(libs.slf4j.simple) 37 | } 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /.github/workflows/apidocs.yaml: -------------------------------------------------------------------------------- 1 | name: Build and Publish API Docs to GitHub Pages 2 | 3 | on: 4 | release: 5 | types: 6 | - published 7 | # Allow running this workflow manually from the Actions tab 8 | workflow_dispatch: 9 | 10 | jobs: 11 | deploy: 12 | 13 | permissions: 14 | pages: write # to deploy to Pages 15 | id-token: write # to verify the deployment originates from an appropriate source 16 | 17 | environment: 18 | name: github-pages 19 | url: ${{ steps.deployment.outputs.page_url }} 20 | 21 | runs-on: ubuntu-latest 22 | steps: 23 | - uses: actions/checkout@v6 24 | with: 25 | submodules: true # Fetch Hugo themes (true OR recursive) 26 | fetch-depth: 0 # Fetch all history for .GitInfo and .Lastmod 27 | 28 | - name: Set up JDK 21 29 | uses: actions/setup-java@v5 30 | with: 31 | java-version: '21' 32 | distribution: 'temurin' 33 | cache: gradle 34 | 35 | - name: Setup Gradle 36 | uses: gradle/actions/setup-gradle@v5 37 | 38 | - name: Generate Dokka Site 39 | run: |- 40 | ./gradlew clean dokkaGenerate 41 | 42 | - name: Upload artifact 43 | uses: actions/upload-pages-artifact@v4 44 | with: 45 | path: build/dokka/html 46 | 47 | - name: Deploy to GitHub Pages 48 | id: deployment 49 | uses: actions/deploy-pages@v4 50 | -------------------------------------------------------------------------------- /.github/workflows/conformance.yml: -------------------------------------------------------------------------------- 1 | name: Conformance Tests 2 | 3 | on: 4 | workflow_dispatch: 5 | pull_request: 6 | branches: [ main ] 7 | push: 8 | branches: [ main ] 9 | 10 | concurrency: 11 | group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} 12 | # Cancel only when the run is NOT on `main` branch 13 | cancel-in-progress: ${{ github.ref != 'refs/heads/main' }} 14 | 15 | jobs: 16 | run-conformance: 17 | runs-on: macos-latest-xlarge 18 | name: Run Conformance Tests 19 | timeout-minutes: 20 20 | env: 21 | JAVA_OPTS: "-Xmx8g -Dfile.encoding=UTF-8 -Djava.awt.headless=true -Dkotlin.daemon.jvm.options=-Xmx6g" 22 | steps: 23 | - uses: actions/checkout@v6 24 | 25 | - name: Set up JDK 21 26 | uses: actions/setup-java@v5 27 | with: 28 | java-version: '21' 29 | distribution: 'temurin' 30 | 31 | - name: Setup Node.js 32 | uses: actions/setup-node@v6 33 | with: 34 | node-version: '24.11.1' 35 | 36 | - name: Setup Gradle 37 | uses: gradle/actions/setup-gradle@v5 38 | with: 39 | add-job-summary: 'always' 40 | cache-read-only: true 41 | 42 | - name: Run Conformance Tests 43 | run: ./gradlew --no-daemon :conformance-test:test 44 | 45 | - name: Upload Conformance Results 46 | if: ${{ !cancelled() }} 47 | uses: actions/upload-artifact@v5 48 | with: 49 | name: conformance-results 50 | path: conformance-test/results/ 51 | -------------------------------------------------------------------------------- /samples/kotlin-mcp-server/src/test/kotlin/TestEnvironment.kt: -------------------------------------------------------------------------------- 1 | import io.ktor.client.HttpClient 2 | import io.ktor.client.engine.cio.CIO 3 | import io.ktor.client.plugins.sse.SSE 4 | import io.ktor.server.engine.EmbeddedServer 5 | import io.modelcontextprotocol.kotlin.sdk.client.Client 6 | import io.modelcontextprotocol.kotlin.sdk.client.mcpSseTransport 7 | import io.modelcontextprotocol.kotlin.sdk.types.Implementation 8 | import kotlinx.coroutines.runBlocking 9 | import java.util.concurrent.TimeUnit 10 | 11 | class TestEnvironment(private val serverConfig: McpServerType) { 12 | 13 | val server: EmbeddedServer<*, *> = serverConfig.serverFactory(0) 14 | val client: Client 15 | 16 | init { 17 | client = runBlocking { 18 | val port = server.engine.resolvedConnectors().single().port 19 | initClient(port, serverConfig) 20 | } 21 | 22 | Runtime.getRuntime().addShutdownHook( 23 | Thread { 24 | println("🏁 Shutting down server (${serverConfig.name})") 25 | server.stop(500, 700, TimeUnit.MILLISECONDS) 26 | println("☑️ Shutdown complete") 27 | }, 28 | ) 29 | } 30 | 31 | private suspend fun initClient(port: Int, config: McpServerType): Client { 32 | val client = Client( 33 | Implementation(name = "test-client", version = "0.1.0"), 34 | ) 35 | 36 | val httpClient = HttpClient(CIO) { 37 | install(SSE) 38 | } 39 | 40 | val transport = httpClient.mcpSseTransport("http://127.0.0.1:$port/${config.sseEndpoint}") 41 | client.connect(transport) 42 | return client 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /samples/weather-stdio-server/gradle/libs.versions.toml: -------------------------------------------------------------------------------- 1 | [versions] 2 | collections-immutable = "0.4.0" 3 | coroutines = "1.10.2" 4 | kotlin = "2.2.21" 5 | ktor = "3.2.3" 6 | logging = "7.0.13" 7 | mcp-kotlin = "0.8.1" 8 | shadow = "9.2.2" 9 | slf4j = "2.0.17" 10 | 11 | [libraries] 12 | kotlin-logging = { group = "io.github.oshai", name = "kotlin-logging-jvm", version.ref = "logging" } 13 | kotlinx-collections-immutable = { group = "org.jetbrains.kotlinx", name = "kotlinx-collections-immutable-jvm", version.ref = "collections-immutable" } 14 | kotlinx-coroutines-test = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-test", version.ref = "coroutines" } 15 | ktor-bom = { group = "io.ktor", name = "ktor-bom", version.ref = "ktor" } 16 | ktor-client-cio = { group = "io.ktor", name = "ktor-client-cio" } 17 | ktor-client-content-negotiation = { group = "io.ktor", name = "ktor-client-content-negotiation" } 18 | ktor-serialization-kotlinx-json = { group = "io.ktor", name = "ktor-serialization-kotlinx-json-jvm" } 19 | ktor-server-cio = { group = "io.ktor", name = "ktor-server-cio", version.ref = "ktor" } 20 | mcp-kotlin-client = { group = "io.modelcontextprotocol", name = "kotlin-sdk-client", version.ref = "mcp-kotlin" } 21 | mcp-kotlin-server = { group = "io.modelcontextprotocol", name = "kotlin-sdk-server", version.ref = "mcp-kotlin" } 22 | slf4j-simple = { group = "org.slf4j", name = "slf4j-simple", version.ref = "slf4j" } 23 | 24 | [plugins] 25 | kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" } 26 | kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" } 27 | shadow = { id = "com.gradleup.shadow", version.ref = "shadow" } 28 | -------------------------------------------------------------------------------- /kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/OldSchemaAbstractServerFeaturesTest.kt: -------------------------------------------------------------------------------- 1 | package io.modelcontextprotocol.kotlin.sdk.server 2 | 3 | import io.modelcontextprotocol.kotlin.sdk.Implementation 4 | import io.modelcontextprotocol.kotlin.sdk.client.Client 5 | import io.modelcontextprotocol.kotlin.sdk.shared.InMemoryTransport 6 | import kotlinx.coroutines.launch 7 | import kotlinx.coroutines.runBlocking 8 | import org.junit.jupiter.api.BeforeEach 9 | 10 | abstract class OldSchemaAbstractServerFeaturesTest { 11 | 12 | protected lateinit var server: Server 13 | protected lateinit var client: Client 14 | 15 | abstract fun getServerCapabilities(): io.modelcontextprotocol.kotlin.sdk.types.ServerCapabilities 16 | 17 | protected open fun getServerInstructionsProvider(): (() -> String)? = null 18 | 19 | @BeforeEach 20 | fun setUp() { 21 | val serverOptions = ServerOptions( 22 | capabilities = getServerCapabilities(), 23 | ) 24 | 25 | server = Server( 26 | serverInfo = Implementation(name = "test server", version = "1.0"), 27 | options = serverOptions, 28 | instructionsProvider = getServerInstructionsProvider(), 29 | ) 30 | 31 | val (clientTransport, serverTransport) = InMemoryTransport.createLinkedPair() 32 | 33 | client = Client( 34 | clientInfo = Implementation(name = "test client", version = "1.0"), 35 | ) 36 | 37 | runBlocking { 38 | // Connect client and server 39 | launch { client.connect(clientTransport) } 40 | launch { server.createSession(serverTransport) } 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /.github/workflows/samples.yml: -------------------------------------------------------------------------------- 1 | name: Build Samples 2 | 3 | on: 4 | workflow_dispatch: 5 | pull_request: 6 | branches: [ main ] 7 | paths: 8 | - 'samples/**' 9 | push: 10 | branches: [ main ] 11 | paths: 12 | - 'samples/**' 13 | 14 | concurrency: 15 | group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} 16 | # Cancel only when the run is NOT on `main` branch 17 | cancel-in-progress: ${{ github.ref != 'refs/heads/main' }} 18 | 19 | jobs: 20 | build: 21 | runs-on: ubuntu-latest 22 | strategy: 23 | matrix: 24 | sample: 25 | - kotlin-mcp-client 26 | - kotlin-mcp-server 27 | - weather-stdio-server 28 | 29 | name: Build Sample 30 | timeout-minutes: 10 31 | env: 32 | JAVA_OPTS: "-Xmx8g -Dfile.encoding=UTF-8 -Djava.awt.headless=true -Dkotlin.daemon.jvm.options=-Xmx6g" 33 | steps: 34 | - uses: actions/checkout@v6 35 | 36 | - name: Set up JDK 21 37 | uses: actions/setup-java@v5 38 | with: 39 | java-version: 21 40 | distribution: 'temurin' 41 | 42 | - name: Setup Gradle 43 | uses: gradle/actions/setup-gradle@v5 44 | with: 45 | add-job-summary: 'always' 46 | cache-read-only: true 47 | 48 | - name: "Build Sample: ${{ matrix.sample }}" 49 | working-directory: ./samples/${{ matrix.sample }} 50 | run: ./gradlew --no-daemon clean build 51 | 52 | - name: Upload Reports 53 | if: ${{ !cancelled() }} 54 | uses: actions/upload-artifact@v5 55 | with: 56 | name: reports-${{ matrix.sample }} 57 | path: | 58 | **/build/reports/ 59 | -------------------------------------------------------------------------------- /kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/AbstractServerFeaturesTest.kt: -------------------------------------------------------------------------------- 1 | package io.modelcontextprotocol.kotlin.sdk.server 2 | 3 | import io.modelcontextprotocol.kotlin.sdk.client.Client 4 | import io.modelcontextprotocol.kotlin.sdk.shared.InMemoryTransport 5 | import io.modelcontextprotocol.kotlin.sdk.types.Implementation 6 | import io.modelcontextprotocol.kotlin.sdk.types.ServerCapabilities 7 | import kotlinx.coroutines.launch 8 | import kotlinx.coroutines.runBlocking 9 | import org.junit.jupiter.api.BeforeEach 10 | 11 | abstract class AbstractServerFeaturesTest { 12 | 13 | protected lateinit var server: Server 14 | protected lateinit var client: Client 15 | 16 | abstract fun getServerCapabilities(): ServerCapabilities 17 | 18 | protected open fun getServerInstructionsProvider(): (() -> String)? = null 19 | 20 | @BeforeEach 21 | fun setUp() { 22 | val serverOptions = ServerOptions( 23 | capabilities = getServerCapabilities(), 24 | ) 25 | 26 | server = Server( 27 | serverInfo = Implementation(name = "test server", version = "1.0"), 28 | options = serverOptions, 29 | instructionsProvider = getServerInstructionsProvider(), 30 | ) 31 | 32 | val (clientTransport, serverTransport) = InMemoryTransport.createLinkedPair() 33 | 34 | client = Client( 35 | clientInfo = Implementation(name = "test client", version = "1.0"), 36 | ) 37 | 38 | runBlocking { 39 | // Connect client and server 40 | launch { client.connect(clientTransport) } 41 | launch { server.createSession(serverTransport) } 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /kotlin-sdk-client/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/streamable/http/StreamingHttpClientTransportLifecycleTest.kt: -------------------------------------------------------------------------------- 1 | package io.modelcontextprotocol.kotlin.sdk.client.streamable.http 2 | 3 | import io.ktor.client.HttpClient 4 | import io.ktor.client.engine.mock.MockEngine 5 | import io.ktor.client.engine.mock.respond 6 | import io.ktor.client.plugins.sse.SSE 7 | import io.ktor.http.ContentType 8 | import io.ktor.http.HttpHeaders 9 | import io.ktor.http.HttpStatusCode 10 | import io.ktor.http.headersOf 11 | import io.modelcontextprotocol.kotlin.sdk.client.AbstractClientTransportLifecycleTest 12 | import io.modelcontextprotocol.kotlin.sdk.client.StreamableHttpClientTransport 13 | import kotlin.test.Ignore 14 | import kotlin.test.Test 15 | import kotlin.time.Duration.Companion.seconds 16 | 17 | class StreamingHttpClientTransportLifecycleTest : 18 | AbstractClientTransportLifecycleTest() { 19 | 20 | /** 21 | * Dummy method to make IDE treat this class as a test 22 | */ 23 | @Test 24 | @Ignore 25 | fun dummyTest() = Unit 26 | 27 | override fun createTransport(): StreamableHttpClientTransport { 28 | val mockEngine = MockEngine { 29 | respond( 30 | "this is not valid json", 31 | status = HttpStatusCode.OK, 32 | headers = headersOf(HttpHeaders.ContentType, ContentType.Application.Json.toString()), 33 | ) 34 | } 35 | val httpClient = HttpClient(mockEngine) { 36 | install(SSE) { 37 | reconnectionTime = 1.seconds 38 | } 39 | } 40 | 41 | return StreamableHttpClientTransport(httpClient, url = "http://localhost:8080/mcp") 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /kotlin-sdk-client/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/WebSocketClientTransport.kt: -------------------------------------------------------------------------------- 1 | package io.modelcontextprotocol.kotlin.sdk.client 2 | 3 | import io.github.oshai.kotlinlogging.KotlinLogging 4 | import io.ktor.client.HttpClient 5 | import io.ktor.client.plugins.websocket.webSocketSession 6 | import io.ktor.client.request.HttpRequestBuilder 7 | import io.ktor.client.request.header 8 | import io.ktor.http.HttpHeaders 9 | import io.ktor.websocket.WebSocketSession 10 | import io.modelcontextprotocol.kotlin.sdk.shared.MCP_SUBPROTOCOL 11 | import io.modelcontextprotocol.kotlin.sdk.shared.WebSocketMcpTransport 12 | import kotlin.properties.Delegates 13 | 14 | private val logger = KotlinLogging.logger {} 15 | 16 | /** 17 | * Client transport for WebSocket: this will connect to a server over the WebSocket protocol. 18 | */ 19 | public class WebSocketClientTransport( 20 | private val client: HttpClient, 21 | private val urlString: String?, 22 | private val requestBuilder: HttpRequestBuilder.() -> Unit = {}, 23 | ) : WebSocketMcpTransport() { 24 | override var session: WebSocketSession by Delegates.notNull() 25 | 26 | override suspend fun initializeSession() { 27 | logger.debug { "Websocket session initialization started..." } 28 | 29 | session = urlString?.let { 30 | client.webSocketSession(it) { 31 | requestBuilder() 32 | 33 | header(HttpHeaders.SecWebSocketProtocol, MCP_SUBPROTOCOL) 34 | } 35 | } ?: client.webSocketSession { 36 | requestBuilder() 37 | 38 | header(HttpHeaders.SecWebSocketProtocol, MCP_SUBPROTOCOL) 39 | } 40 | 41 | logger.debug { "Websocket session initialization finished" } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types.util.kt: -------------------------------------------------------------------------------- 1 | package io.modelcontextprotocol.kotlin.sdk 2 | 3 | import io.modelcontextprotocol.kotlin.sdk.types.error 4 | import io.modelcontextprotocol.kotlin.sdk.types.success 5 | import kotlinx.serialization.json.JsonObject 6 | 7 | @Deprecated( 8 | message = "Use `io.modelcontextprotocol.kotlin.sdk.types.EmptyJsonObject` instead", 9 | replaceWith = ReplaceWith("EmptyJsonObject", "io.modelcontextprotocol.kotlin.sdk.types.EmptyJsonObject"), 10 | level = DeprecationLevel.WARNING, 11 | ) 12 | public val EmptyJsonObject: JsonObject = io.modelcontextprotocol.kotlin.sdk.types.EmptyJsonObject 13 | 14 | /** 15 | * Creates a [CallToolResult] with single [TextContent] and [meta]. 16 | */ 17 | @Deprecated( 18 | message = "Use `CallToolResult.success` instead", 19 | replaceWith = ReplaceWith("CallToolResult.Companion.success"), 20 | level = DeprecationLevel.WARNING, 21 | ) 22 | public fun CallToolResult.ok( 23 | content: String, 24 | meta: JsonObject = EmptyJsonObject, 25 | ): io.modelcontextprotocol.kotlin.sdk.types.CallToolResult = 26 | io.modelcontextprotocol.kotlin.sdk.types.CallToolResult.success(content, meta) 27 | 28 | /** 29 | * Creates a [CallToolResult] with single [TextContent] and [meta], with `isError` being true. 30 | */ 31 | @Deprecated( 32 | message = "Use `CallToolResult.error` instead", 33 | replaceWith = ReplaceWith("CallToolResult.Companion.error"), 34 | level = DeprecationLevel.WARNING, 35 | ) 36 | public fun CallToolResult.error( 37 | content: String, 38 | meta: JsonObject = EmptyJsonObject, 39 | ): io.modelcontextprotocol.kotlin.sdk.types.CallToolResult = 40 | io.modelcontextprotocol.kotlin.sdk.types.CallToolResult.error(content, meta) 41 | -------------------------------------------------------------------------------- /kotlin-sdk-test/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/WebSocketTransportTest.kt: -------------------------------------------------------------------------------- 1 | package io.modelcontextprotocol.kotlin.sdk.client 2 | 3 | import io.ktor.server.testing.testApplication 4 | import io.ktor.server.websocket.WebSockets 5 | import io.modelcontextprotocol.kotlin.sdk.server.mcpWebSocket 6 | import io.modelcontextprotocol.kotlin.sdk.server.mcpWebSocketTransport 7 | import io.modelcontextprotocol.kotlin.sdk.shared.BaseTransportTest 8 | import kotlinx.coroutines.CompletableDeferred 9 | import kotlin.test.Ignore 10 | import kotlin.test.Test 11 | 12 | class WebSocketTransportTest : BaseTransportTest() { 13 | @Test 14 | @Ignore // "Test disabled for investigation #17" 15 | fun `should start then close cleanly`() = testApplication { 16 | install(WebSockets) 17 | routing { 18 | mcpWebSocket() 19 | } 20 | 21 | val client = createClient { 22 | install(io.ktor.client.plugins.websocket.WebSockets) 23 | }.mcpWebSocketTransport() 24 | 25 | testTransportOpenClose(client) 26 | } 27 | 28 | @Test 29 | @Ignore // "Test disabled for investigation #17" 30 | fun `should read messages`() = testApplication { 31 | val clientFinished = CompletableDeferred() 32 | 33 | install(WebSockets) 34 | routing { 35 | mcpWebSocketTransport { 36 | onMessage { 37 | send(it) 38 | } 39 | 40 | clientFinished.await() 41 | } 42 | } 43 | 44 | val transport = createClient { 45 | install(io.ktor.client.plugins.websocket.WebSockets) 46 | }.mcpWebSocketTransport() 47 | 48 | testTransportRead(transport) 49 | 50 | clientFinished.complete(Unit) 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/shared/AbstractTransport.kt: -------------------------------------------------------------------------------- 1 | package io.modelcontextprotocol.kotlin.sdk.shared 2 | 3 | import io.modelcontextprotocol.kotlin.sdk.types.JSONRPCMessage 4 | import kotlinx.coroutines.CompletableDeferred 5 | 6 | /** 7 | * Implements [onClose], [onError] and [onMessage] functions of [Transport] providing 8 | * corresponding [_onClose], [_onError] and [_onMessage] properties to use for an implementation. 9 | */ 10 | @Suppress("PropertyName") 11 | public abstract class AbstractTransport : Transport { 12 | protected var _onClose: (() -> Unit) = {} 13 | private set 14 | protected var _onError: ((Throwable) -> Unit) = {} 15 | private set 16 | 17 | // to not skip messages 18 | private val _onMessageInitialized = CompletableDeferred() 19 | protected var _onMessage: (suspend ((JSONRPCMessage) -> Unit)) = { 20 | _onMessageInitialized.await() 21 | _onMessage.invoke(it) 22 | } 23 | private set 24 | 25 | override fun onClose(block: () -> Unit) { 26 | val old = _onClose 27 | _onClose = { 28 | old() 29 | block() 30 | } 31 | } 32 | 33 | override fun onError(block: (Throwable) -> Unit) { 34 | val old = _onError 35 | _onError = { e -> 36 | old(e) 37 | block(e) 38 | } 39 | } 40 | 41 | override fun onMessage(block: suspend (JSONRPCMessage) -> Unit) { 42 | val old: suspend (JSONRPCMessage) -> Unit = when (_onMessageInitialized.isCompleted) { 43 | true -> _onMessage 44 | false -> { _ -> } 45 | } 46 | 47 | _onMessage = { message -> 48 | old(message) 49 | block(message) 50 | } 51 | 52 | _onMessageInitialized.complete(Unit) 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /.github/workflows/gradle-publish.yml: -------------------------------------------------------------------------------- 1 | # This workflow uses actions that are not certified by GitHub. 2 | # They are provided by a third-party and are governed by 3 | # separate terms of service, privacy policy, and support 4 | # documentation. 5 | # This workflow will build a package using Gradle and then publish it to GitHub packages when a release is created 6 | # For more information see: https://github.com/actions/setup-java/blob/main/docs/advanced-usage.md#Publishing-using-gradle 7 | 8 | name: Release 9 | 10 | on: 11 | release: 12 | types: [ published ] 13 | 14 | jobs: 15 | build: 16 | runs-on: macos-latest-xlarge 17 | environment: release 18 | env: 19 | JAVA_OPTS: "-Xmx8g -Dfile.encoding=UTF-8 -Djava.awt.headless=true -Dkotlin.daemon.jvm.options=-Xmx6g" 20 | 21 | permissions: 22 | contents: write 23 | packages: write 24 | 25 | steps: 26 | - uses: actions/checkout@v6 27 | - name: Set up JDK 21 28 | uses: actions/setup-java@v5 29 | with: 30 | java-version: '21' 31 | distribution: 'temurin' 32 | 33 | - name: Setup Gradle 34 | uses: gradle/actions/setup-gradle@v5 35 | 36 | - name: Clean Build with Gradle 37 | run: ./gradlew clean build -x :conformance-test:test 38 | 39 | - name: Publish to Maven Central Portal 40 | id: publish 41 | run: ./gradlew publishToMavenCentral --no-configuration-cache 42 | env: 43 | ORG_GRADLE_PROJECT_mavenCentralUsername: ${{ secrets.OSSRH_USERNAME }} 44 | ORG_GRADLE_PROJECT_mavenCentralPassword: ${{ secrets.OSSRH_TOKEN }} 45 | ORG_GRADLE_PROJECT_signingInMemoryKey: ${{ secrets.GPG_SECRET_KEY }} 46 | ORG_GRADLE_PROJECT_signingInMemoryKeyPassword: ${{ secrets.SIGNING_PASSPHRASE }} 47 | GPG_SECRET_KEY: ${{ secrets.GPG_SECRET_KEY }} 48 | SIGNING_PASSPHRASE: ${{ secrets.SIGNING_PASSPHRASE }} 49 | -------------------------------------------------------------------------------- /kotlin-sdk-test/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/shared/InMemoryTransport.kt: -------------------------------------------------------------------------------- 1 | package io.modelcontextprotocol.kotlin.sdk.shared 2 | 3 | import io.modelcontextprotocol.kotlin.sdk.types.JSONRPCMessage 4 | 5 | /** 6 | * In-memory transport for creating clients and servers that talk to each other within the same process. 7 | */ 8 | class InMemoryTransport : AbstractTransport() { 9 | private var otherTransport: InMemoryTransport? = null 10 | private val messageQueue: MutableList = mutableListOf() 11 | 12 | /** 13 | * Creates a pair of linked in-memory transports that can communicate with each other. 14 | * One should be passed to a Client and one to a Server. 15 | */ 16 | companion object { 17 | fun createLinkedPair(): Pair { 18 | val clientTransport = InMemoryTransport() 19 | val serverTransport = InMemoryTransport() 20 | clientTransport.otherTransport = serverTransport 21 | serverTransport.otherTransport = clientTransport 22 | return Pair(clientTransport, serverTransport) 23 | } 24 | } 25 | 26 | override suspend fun start() { 27 | // Process any messages that were queued before start was called 28 | while (messageQueue.isNotEmpty()) { 29 | messageQueue.removeFirstOrNull()?.let { message -> 30 | _onMessage.invoke(message) // todo? 31 | } 32 | } 33 | } 34 | 35 | override suspend fun close() { 36 | val other = otherTransport 37 | otherTransport = null 38 | other?.close() 39 | _onClose.invoke() 40 | } 41 | 42 | override suspend fun send(message: JSONRPCMessage, options: TransportSendOptions?) { 43 | val other = otherTransport ?: throw IllegalStateException("Not connected") 44 | 45 | other._onMessage.invoke(message) 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /kotlin-sdk-client/build.gradle.kts: -------------------------------------------------------------------------------- 1 | @file:OptIn(ExperimentalWasmDsl::class) 2 | 3 | import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl 4 | 5 | plugins { 6 | id("mcp.multiplatform") 7 | id("mcp.publishing") 8 | id("mcp.dokka") 9 | alias(libs.plugins.kotlinx.binary.compatibility.validator) 10 | `netty-convention` 11 | } 12 | 13 | kotlin { 14 | iosArm64() 15 | iosX64() 16 | iosSimulatorArm64() 17 | watchosX64() 18 | watchosArm64() 19 | watchosSimulatorArm64() 20 | tvosX64() 21 | tvosArm64() 22 | tvosSimulatorArm64() 23 | js { 24 | browser() 25 | nodejs() 26 | } 27 | wasmJs { 28 | browser() 29 | nodejs() 30 | } 31 | 32 | sourceSets { 33 | commonMain { 34 | dependencies { 35 | api(project(":kotlin-sdk-core")) 36 | api(libs.ktor.client.core) 37 | implementation(libs.kotlin.logging) 38 | } 39 | } 40 | 41 | commonTest { 42 | dependencies { 43 | implementation(kotlin("test")) 44 | implementation(libs.kotest.assertions.core) 45 | implementation(libs.kotlinx.coroutines.test) 46 | implementation(libs.ktor.client.logging) 47 | implementation(libs.ktor.client.mock) 48 | implementation(libs.ktor.server.websockets) 49 | } 50 | } 51 | 52 | jvmTest { 53 | dependencies { 54 | implementation(libs.awaitility) 55 | implementation(libs.ktor.client.apache5) 56 | implementation(libs.mockk) 57 | implementation(libs.junit.jupiter.params) 58 | implementation(libs.mokksy) 59 | implementation(dependencies.platform(libs.netty.bom)) 60 | runtimeOnly(libs.slf4j.simple) 61 | } 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/pingRequest.dsl.kt: -------------------------------------------------------------------------------- 1 | package io.modelcontextprotocol.kotlin.sdk.types 2 | 3 | import io.modelcontextprotocol.kotlin.sdk.ExperimentalMcpApi 4 | import kotlin.contracts.ExperimentalContracts 5 | import kotlin.contracts.InvocationKind 6 | import kotlin.contracts.contract 7 | 8 | /** 9 | * Creates a [PingRequest] using a type-safe DSL builder. 10 | * 11 | * ## Optional 12 | * - [meta][PingRequestBuilder.meta] - Metadata for the request 13 | * 14 | * Example with no parameters: 15 | * ```kotlin 16 | * val request = buildPingRequest { } 17 | * ``` 18 | * 19 | * Example with metadata: 20 | * ```kotlin 21 | * val request = buildPingRequest { 22 | * meta { 23 | * put("timestamp", JsonPrimitive(System.currentTimeMillis())) 24 | * } 25 | * } 26 | * ``` 27 | * 28 | * @param block Configuration lambda for setting up the ping request 29 | * @return A configured [PingRequest] instance 30 | * @see PingRequestBuilder 31 | * @see PingRequest 32 | */ 33 | @OptIn(ExperimentalContracts::class) 34 | @ExperimentalMcpApi 35 | internal inline fun buildPingRequest(block: PingRequestBuilder.() -> Unit): PingRequest { 36 | contract { callsInPlace(block, InvocationKind.EXACTLY_ONCE) } 37 | return PingRequestBuilder().apply(block).build() 38 | } 39 | 40 | /** 41 | * DSL builder for constructing [PingRequest] instances. 42 | * 43 | * This builder creates ping requests to check connection status with the server. 44 | * All fields are optional. 45 | * 46 | * ## Optional 47 | * - [meta] - Metadata for the request 48 | * 49 | * @see buildPingRequest 50 | * @see PingRequest 51 | */ 52 | @McpDsl 53 | public class PingRequestBuilder @PublishedApi internal constructor() : RequestBuilder() { 54 | @PublishedApi 55 | override fun build(): PingRequest { 56 | val params = meta?.let { BaseRequestParams(meta = it) } 57 | return PingRequest(params) 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/sse/TsClientKotlinServerTestSse.kt: -------------------------------------------------------------------------------- 1 | package io.modelcontextprotocol.kotlin.sdk.integration.typescript.sse 2 | 3 | import io.modelcontextprotocol.kotlin.sdk.integration.typescript.AbstractTsClientKotlinServerTest 4 | import io.modelcontextprotocol.kotlin.sdk.integration.typescript.TransportKind 5 | import org.junit.jupiter.api.AfterEach 6 | import org.junit.jupiter.api.BeforeEach 7 | 8 | class TsClientKotlinServerTestSse : AbstractTsClientKotlinServerTest() { 9 | 10 | override val transportKind = TransportKind.SSE 11 | 12 | private var port: Int = 0 13 | private lateinit var serverUrl: String 14 | private var httpServer: KotlinServerForTsClient? = null 15 | 16 | @BeforeEach 17 | fun setUp() { 18 | port = findFreePort() 19 | serverUrl = "http://localhost:$port/mcp" 20 | killProcessOnPort(port) 21 | httpServer = KotlinServerForTsClient().also { it.start(port) } 22 | check(waitForPort(port = port)) { "Kotlin test server did not become ready on localhost:$port within timeout" } 23 | println("Kotlin server started on port $port") 24 | } 25 | 26 | @AfterEach 27 | fun tearDown() { 28 | try { 29 | httpServer?.stop() 30 | println("HTTP server stopped") 31 | } catch (e: Exception) { 32 | println("Error during server shutdown: ${e.message}") 33 | } 34 | } 35 | 36 | override fun beforeServer() {} 37 | override fun afterServer() {} 38 | 39 | override fun runClient(vararg args: String): String { 40 | val cmd = buildString { 41 | append("npx tsx myClient.ts ") 42 | append(serverUrl) 43 | if (args.isNotEmpty()) { 44 | append(' ') 45 | append(args.joinToString(" ")) 46 | } 47 | } 48 | return executeCommand(cmd, tsClientDir) 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/sse/KotlinClientTsServerTestSse.kt: -------------------------------------------------------------------------------- 1 | package io.modelcontextprotocol.kotlin.sdk.integration.typescript.sse 2 | 3 | import io.modelcontextprotocol.kotlin.sdk.client.Client 4 | import io.modelcontextprotocol.kotlin.sdk.integration.typescript.AbstractKotlinClientTsServerTest 5 | import io.modelcontextprotocol.kotlin.sdk.integration.typescript.TransportKind 6 | import kotlinx.coroutines.withTimeout 7 | import org.junit.jupiter.api.AfterEach 8 | import org.junit.jupiter.api.BeforeEach 9 | import kotlin.time.Duration.Companion.seconds 10 | 11 | class KotlinClientTsServerTestSse : AbstractKotlinClientTsServerTest() { 12 | 13 | override val transportKind = TransportKind.SSE 14 | 15 | private var port: Int = 0 16 | private val host = "localhost" 17 | private lateinit var serverUrl: String 18 | private lateinit var tsServerProcess: Process 19 | 20 | @BeforeEach 21 | fun setUpSse() { 22 | port = findFreePort() 23 | serverUrl = "http://$host:$port/mcp" 24 | tsServerProcess = startTypeScriptServer(port) 25 | println("TypeScript server started on port $port") 26 | } 27 | 28 | @AfterEach 29 | fun tearDownSse() { 30 | if (::tsServerProcess.isInitialized) { 31 | try { 32 | println("Stopping TypeScript server") 33 | stopProcess(tsServerProcess) 34 | } catch (e: Exception) { 35 | println("Warning: Error during TypeScript server stop: ${e.message}") 36 | } 37 | } 38 | } 39 | 40 | override suspend fun useClient(block: suspend (Client) -> T): T = withClient(serverUrl) { client -> 41 | try { 42 | withTimeout(20.seconds) { block(client) } 43 | } finally { 44 | try { 45 | withTimeout(3.seconds) { client.close() } 46 | } catch (_: Exception) {} 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /kotlin-sdk-test/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/shared/OldSchemaInMemoryTransport.kt: -------------------------------------------------------------------------------- 1 | package io.modelcontextprotocol.kotlin.sdk.shared 2 | 3 | import io.modelcontextprotocol.kotlin.sdk.JSONRPCMessage 4 | 5 | /** 6 | * In-memory transport for creating clients and servers that talk to each other within the same process. 7 | */ 8 | class OldSchemaInMemoryTransport : AbstractTransport() { 9 | private var otherTransport: OldSchemaInMemoryTransport? = null 10 | private val messageQueue: MutableList = mutableListOf() 11 | 12 | /** 13 | * Creates a pair of linked in-memory transports that can communicate with each other. 14 | * One should be passed to a Client and one to a Server. 15 | */ 16 | companion object { 17 | fun createLinkedPair(): Pair { 18 | val clientTransport = OldSchemaInMemoryTransport() 19 | val serverTransport = OldSchemaInMemoryTransport() 20 | clientTransport.otherTransport = serverTransport 21 | serverTransport.otherTransport = clientTransport 22 | return Pair(clientTransport, serverTransport) 23 | } 24 | } 25 | 26 | override suspend fun start() { 27 | // Process any messages that were queued before start was called 28 | while (messageQueue.isNotEmpty()) { 29 | messageQueue.removeFirstOrNull()?.let { message -> 30 | _onMessage.invoke(message) // todo? 31 | } 32 | } 33 | } 34 | 35 | override suspend fun close() { 36 | val other = otherTransport 37 | otherTransport = null 38 | other?.close() 39 | _onClose.invoke() 40 | } 41 | 42 | override suspend fun send(message: JSONRPCMessage, options: TransportSendOptions?) { 43 | val other = otherTransport ?: throw IllegalStateException("Not connected") 44 | 45 | other._onMessage.invoke(message) 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/roots.dsl.kt: -------------------------------------------------------------------------------- 1 | package io.modelcontextprotocol.kotlin.sdk.types 2 | 3 | import io.modelcontextprotocol.kotlin.sdk.ExperimentalMcpApi 4 | import kotlin.contracts.ExperimentalContracts 5 | import kotlin.contracts.InvocationKind 6 | import kotlin.contracts.contract 7 | 8 | /** 9 | * Creates a [ListRootsRequest] using a type-safe DSL builder. 10 | * 11 | * ## Optional 12 | * - [meta][ListRootsRequestBuilder.meta] - Metadata for the request 13 | * 14 | * Example with no parameters: 15 | * ```kotlin 16 | * val request = buildListRootsRequest { } 17 | * ``` 18 | * 19 | * Example with metadata: 20 | * ```kotlin 21 | * val request = buildListRootsRequest { 22 | * meta { 23 | * put("context", "initialization") 24 | * } 25 | * } 26 | * ``` 27 | * 28 | * @param block Configuration lambda for setting up the list roots request 29 | * @return A configured [ListRootsRequest] instance 30 | * @see ListRootsRequestBuilder 31 | * @see ListRootsRequest 32 | */ 33 | @OptIn(ExperimentalContracts::class) 34 | @ExperimentalMcpApi 35 | internal inline fun buildListRootsRequest(block: ListRootsRequestBuilder.() -> Unit): ListRootsRequest { 36 | contract { callsInPlace(block, InvocationKind.EXACTLY_ONCE) } 37 | return ListRootsRequestBuilder().apply(block).build() 38 | } 39 | 40 | /** 41 | * DSL builder for constructing [ListRootsRequest] instances. 42 | * 43 | * This builder retrieves the list of root URIs provided by the client. 44 | * All fields are optional. 45 | * 46 | * ## Optional 47 | * - [meta] - Metadata for the request 48 | * 49 | * @see buildListRootsRequest 50 | * @see ListRootsRequest 51 | */ 52 | @McpDsl 53 | public class ListRootsRequestBuilder @PublishedApi internal constructor() : RequestBuilder() { 54 | @PublishedApi 55 | override fun build(): ListRootsRequest { 56 | val params = meta?.let { BaseRequestParams(meta = it) } 57 | return ListRootsRequest(params) 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /kotlin-sdk-client/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/WebSocketMcpKtorClientExtensions.kt: -------------------------------------------------------------------------------- 1 | package io.modelcontextprotocol.kotlin.sdk.client 2 | 3 | import io.github.oshai.kotlinlogging.KotlinLogging 4 | import io.ktor.client.HttpClient 5 | import io.ktor.client.request.HttpRequestBuilder 6 | import io.modelcontextprotocol.kotlin.sdk.LIB_VERSION 7 | import io.modelcontextprotocol.kotlin.sdk.shared.IMPLEMENTATION_NAME 8 | import io.modelcontextprotocol.kotlin.sdk.types.Implementation 9 | 10 | private val logger = KotlinLogging.logger {} 11 | 12 | /** 13 | * Returns a new WebSocket transport for the Model Context Protocol using the provided HttpClient. 14 | * 15 | * @param urlString Optional URL of the MCP server. 16 | * @param requestBuilder Optional lambda to configure the HTTP request. 17 | * @return A [WebSocketClientTransport] configured for MCP communication. 18 | */ 19 | public fun HttpClient.mcpWebSocketTransport( 20 | urlString: String? = null, 21 | requestBuilder: HttpRequestBuilder.() -> Unit = {}, 22 | ): WebSocketClientTransport = WebSocketClientTransport(this, urlString, requestBuilder) 23 | 24 | /** 25 | * Creates and connects an MCP client over WebSocket using the provided HttpClient. 26 | * 27 | * @param urlString Optional URL of the MCP server. 28 | * @param requestBuilder Optional lambda to configure the HTTP request. 29 | * @return A connected [Client] ready for MCP communication. 30 | */ 31 | public suspend fun HttpClient.mcpWebSocket( 32 | urlString: String? = null, 33 | requestBuilder: HttpRequestBuilder.() -> Unit = {}, 34 | ): Client { 35 | val transport = mcpWebSocketTransport(urlString, requestBuilder) 36 | val client = Client( 37 | Implementation( 38 | name = IMPLEMENTATION_NAME, 39 | version = LIB_VERSION, 40 | ), 41 | ) 42 | logger.debug { "Client started to connect to server" } 43 | client.connect(transport) 44 | logger.debug { "Client finished to connect to server" } 45 | return client 46 | } 47 | -------------------------------------------------------------------------------- /kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/sse/OldSchemaKotlinClientTsServerTestSse.kt: -------------------------------------------------------------------------------- 1 | package io.modelcontextprotocol.kotlin.sdk.integration.typescript.sse 2 | 3 | import io.modelcontextprotocol.kotlin.sdk.client.Client 4 | import io.modelcontextprotocol.kotlin.sdk.integration.typescript.OldSchemaAbstractKotlinClientTsServerTest 5 | import io.modelcontextprotocol.kotlin.sdk.integration.typescript.OldSchemaTransportKind 6 | import kotlinx.coroutines.withTimeout 7 | import org.junit.jupiter.api.AfterEach 8 | import org.junit.jupiter.api.BeforeEach 9 | import kotlin.time.Duration.Companion.seconds 10 | 11 | class OldSchemaKotlinClientTsServerTestSse : OldSchemaAbstractKotlinClientTsServerTest() { 12 | 13 | override val transportKind = OldSchemaTransportKind.SSE 14 | 15 | private var port: Int = 0 16 | private val host = "localhost" 17 | private lateinit var serverUrl: String 18 | private lateinit var tsServerProcess: Process 19 | 20 | @BeforeEach 21 | fun setUpSse() { 22 | port = findFreePort() 23 | serverUrl = "http://$host:$port/mcp" 24 | tsServerProcess = startTypeScriptServer(port) 25 | println("TypeScript server started on port $port") 26 | } 27 | 28 | @AfterEach 29 | fun tearDownSse() { 30 | if (::tsServerProcess.isInitialized) { 31 | try { 32 | println("Stopping TypeScript server") 33 | stopProcess(tsServerProcess) 34 | } catch (e: Exception) { 35 | println("Warning: Error during TypeScript server stop: ${e.message}") 36 | } 37 | } 38 | } 39 | 40 | override suspend fun useClient(block: suspend (Client) -> T): T = withClient(serverUrl) { client -> 41 | try { 42 | withTimeout(20.seconds) { block(client) } 43 | } finally { 44 | try { 45 | withTimeout(3.seconds) { client.close() } 46 | } catch (_: Exception) {} 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /kotlin-sdk-client/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/StreamableHttpMcpKtorClientExtensions.kt: -------------------------------------------------------------------------------- 1 | package io.modelcontextprotocol.kotlin.sdk.client 2 | 3 | import io.ktor.client.HttpClient 4 | import io.ktor.client.request.HttpRequestBuilder 5 | import io.modelcontextprotocol.kotlin.sdk.LIB_VERSION 6 | import io.modelcontextprotocol.kotlin.sdk.shared.IMPLEMENTATION_NAME 7 | import io.modelcontextprotocol.kotlin.sdk.types.Implementation 8 | import kotlin.time.Duration 9 | 10 | /** 11 | * Returns a new Streamable HTTP transport for the Model Context Protocol using the provided HttpClient. 12 | * 13 | * @param url URL of the MCP server. 14 | * @param reconnectionTime Optional duration to wait before attempting to reconnect. 15 | * @param requestBuilder Optional lambda to configure the HTTP request. 16 | * @return A [StreamableHttpClientTransport] configured for MCP communication. 17 | */ 18 | public fun HttpClient.mcpStreamableHttpTransport( 19 | url: String, 20 | reconnectionTime: Duration? = null, 21 | requestBuilder: HttpRequestBuilder.() -> Unit = {}, 22 | ): StreamableHttpClientTransport = 23 | StreamableHttpClientTransport(this, url, reconnectionTime, requestBuilder = requestBuilder) 24 | 25 | /** 26 | * Creates and connects an MCP client over Streamable HTTP using the provided HttpClient. 27 | * 28 | * @param url URL of the MCP server. 29 | * @param reconnectionTime Optional duration to wait before attempting to reconnect. 30 | * @param requestBuilder Optional lambda to configure the HTTP request. 31 | * @return A connected [Client] ready for MCP communication. 32 | */ 33 | public suspend fun HttpClient.mcpStreamableHttp( 34 | url: String, 35 | reconnectionTime: Duration? = null, 36 | requestBuilder: HttpRequestBuilder.() -> Unit = {}, 37 | ): Client { 38 | val transport = mcpStreamableHttpTransport(url, reconnectionTime, requestBuilder) 39 | val client = Client(Implementation(name = IMPLEMENTATION_NAME, version = LIB_VERSION)) 40 | client.connect(transport) 41 | return client 42 | } 43 | -------------------------------------------------------------------------------- /kotlin-sdk-client/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/KtorClient.kt: -------------------------------------------------------------------------------- 1 | package io.modelcontextprotocol.kotlin.sdk.client 2 | 3 | import io.ktor.client.HttpClient 4 | import io.ktor.client.request.HttpRequestBuilder 5 | import io.modelcontextprotocol.kotlin.sdk.LIB_VERSION 6 | import io.modelcontextprotocol.kotlin.sdk.shared.IMPLEMENTATION_NAME 7 | import io.modelcontextprotocol.kotlin.sdk.types.Implementation 8 | import kotlin.time.Duration 9 | 10 | /** 11 | * Returns a new SSE transport for the Model Context Protocol using the provided HttpClient. 12 | * 13 | * @param urlString Optional URL of the MCP server. 14 | * @param reconnectionTime Optional duration to wait before attempting to reconnect. 15 | * @param requestBuilder Optional lambda to configure the HTTP request. 16 | * @return A [SSEClientTransport] configured for MCP communication. 17 | */ 18 | public fun HttpClient.mcpSseTransport( 19 | urlString: String? = null, 20 | reconnectionTime: Duration? = null, 21 | requestBuilder: HttpRequestBuilder.() -> Unit = {}, 22 | ): SseClientTransport = SseClientTransport(this, urlString, reconnectionTime, requestBuilder) 23 | 24 | /** 25 | * Creates and connects an MCP client over SSE using the provided HttpClient. 26 | * 27 | * @param urlString Optional URL of the MCP server. 28 | * @param reconnectionTime Optional duration to wait before attempting to reconnect. 29 | * @param requestBuilder Optional lambda to configure the HTTP request. 30 | * @return A connected [Client] ready for MCP communication. 31 | */ 32 | public suspend fun HttpClient.mcpSse( 33 | urlString: String? = null, 34 | reconnectionTime: Duration? = null, 35 | requestBuilder: HttpRequestBuilder.() -> Unit = {}, 36 | ): Client { 37 | val transport = mcpSseTransport(urlString, reconnectionTime, requestBuilder) 38 | val client = Client( 39 | Implementation( 40 | name = IMPLEMENTATION_NAME, 41 | version = LIB_VERSION, 42 | ), 43 | ) 44 | client.connect(transport) 45 | return client 46 | } 47 | -------------------------------------------------------------------------------- /.github/workflows/codeql.yml: -------------------------------------------------------------------------------- 1 | name: "CodeQL Advanced" 2 | 3 | on: 4 | push: 5 | branches: [ "main" ] 6 | pull_request: 7 | branches: [ "main" ] 8 | schedule: 9 | - cron: '0 4 * * 0' 10 | 11 | jobs: 12 | analyze: 13 | name: Analyze (${{ matrix.language }}) 14 | runs-on: ubuntu-latest 15 | permissions: 16 | # required for all workflows 17 | security-events: write 18 | 19 | # required to fetch internal or private CodeQL packs 20 | packages: read 21 | 22 | # only required for workflows in private repositories 23 | actions: read 24 | contents: read 25 | 26 | strategy: 27 | matrix: 28 | language: [ java-kotlin ] 29 | 30 | steps: 31 | - name: Checkout repository 32 | uses: actions/checkout@v6 33 | with: 34 | fetch-depth: 0 35 | 36 | - uses: actions/setup-java@v5 37 | with: 38 | distribution: temurin 39 | java-version: '21' 40 | 41 | # Initializes the CodeQL tools for scanning. 42 | - name: Initialize CodeQL 43 | uses: github/codeql-action/init@v4 44 | with: 45 | languages: ${{ matrix.language }} 46 | build-mode: manual 47 | 48 | - uses: actions/cache@v4 49 | with: 50 | path: | 51 | ~/.gradle/caches 52 | ~/.gradle/wrapper 53 | key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }} 54 | 55 | - name: Build Kotlin sources 56 | run: | 57 | ./gradlew \ 58 | :kotlin-sdk-core:compileKotlinJvm \ 59 | :kotlin-sdk-client:compileKotlinJvm \ 60 | :kotlin-sdk-server:compileKotlinJvm \ 61 | :kotlin-sdk:compileKotlinJvm \ 62 | :kotlin-sdk-test:compileKotlinJvm \ 63 | -Pkotlin.incremental=false \ 64 | --no-daemon --stacktrace --rerun-tasks 65 | 66 | - name: Analyze 67 | uses: github/codeql-action/analyze@v4 68 | with: 69 | category: '/language:${{ matrix.language }}' 70 | -------------------------------------------------------------------------------- /samples/weather-stdio-server/src/test/kotlin/io/modelcontextprotocol/sample/client/ClientStdio.kt: -------------------------------------------------------------------------------- 1 | package io.modelcontextprotocol.sample.client 2 | 3 | import io.modelcontextprotocol.kotlin.sdk.client.Client 4 | import io.modelcontextprotocol.kotlin.sdk.client.StdioClientTransport 5 | import io.modelcontextprotocol.kotlin.sdk.types.Implementation 6 | import io.modelcontextprotocol.kotlin.sdk.types.TextContent 7 | import kotlinx.coroutines.runBlocking 8 | import kotlinx.io.asSink 9 | import kotlinx.io.asSource 10 | import kotlinx.io.buffered 11 | 12 | fun main(): Unit = runBlocking { 13 | val process = ProcessBuilder( 14 | "java", 15 | "-jar", 16 | "${System.getProperty("user.dir")}/build/libs/weather-stdio-server-0.1.0-all.jar", 17 | ).redirectErrorStream(true) 18 | .start() 19 | 20 | val transport = StdioClientTransport( 21 | input = process.inputStream.asSource().buffered(), 22 | output = process.outputStream.asSink().buffered(), 23 | ) 24 | 25 | // Initialize the MCP client with client information 26 | val client = Client( 27 | clientInfo = Implementation(name = "weather", version = "1.0.0"), 28 | ) 29 | 30 | client.connect(transport) 31 | 32 | val toolsList = client.listTools().tools.map { it.name } 33 | println("Available Tools = $toolsList") 34 | 35 | val weatherForecastResult = client.callTool( 36 | name = "get_forecast", 37 | arguments = mapOf( 38 | "latitude" to 38.5816, 39 | "longitude" to -121.4944, 40 | ), 41 | ).content.map { if (it is TextContent) it.text else it.toString() } 42 | 43 | println("Weather Forecast: ${weatherForecastResult.joinToString(separator = "\n", prefix = "\n", postfix = "\n")}") 44 | 45 | val alertResult = 46 | client.callTool( 47 | name = "get_alert", 48 | arguments = mapOf("state" to "TX"), 49 | ).content.map { if (it is TextContent) it.text else it.toString() } 50 | 51 | println("Alert Response = $alertResult") 52 | 53 | client.close() 54 | } 55 | -------------------------------------------------------------------------------- /kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/shared/Transport.kt: -------------------------------------------------------------------------------- 1 | package io.modelcontextprotocol.kotlin.sdk.shared 2 | 3 | import io.modelcontextprotocol.kotlin.sdk.types.JSONRPCMessage 4 | 5 | /** 6 | * Describes the minimal contract for MCP transport that a client or server can communicate over. 7 | */ 8 | public interface Transport { 9 | /** 10 | * Starts processing messages on the transport, including any connection steps that might need to be taken. 11 | * 12 | * This method should only be called after callbacks are installed, or else messages may be lost. 13 | * 14 | * NOTE: This method should not be called explicitly when using Client, Server, or Protocol classes, 15 | * as they will implicitly call start(). 16 | */ 17 | public suspend fun start() 18 | 19 | /** 20 | * Sends a JSON-RPC message (request or response). 21 | * 22 | * @property message The JSON-RPC message to send, either a request or a response. 23 | * @property options Optional transport-specific options that control sending behavior. 24 | * Different transport implementations may support different options. 25 | */ 26 | public suspend fun send(message: JSONRPCMessage, options: TransportSendOptions? = null) 27 | 28 | /** 29 | * Closes the connection. 30 | */ 31 | public suspend fun close() 32 | 33 | /** 34 | * Callback for when the connection is closed for any reason. 35 | * 36 | * This should be invoked when close() is called as well. 37 | */ 38 | public fun onClose(block: () -> Unit) 39 | 40 | /** 41 | * Callback for when an error occurs. 42 | * 43 | * Note that errors are not necessarily fatal; they are used for reporting any kind of 44 | * exceptional condition out of a band. 45 | */ 46 | public fun onError(block: (Throwable) -> Unit) 47 | 48 | /** 49 | * Callback for when a message (request or response) is received over the connection. 50 | */ 51 | public fun onMessage(block: suspend (JSONRPCMessage) -> Unit) 52 | } 53 | -------------------------------------------------------------------------------- /kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/ExperimentalMcpApi.kt: -------------------------------------------------------------------------------- 1 | package io.modelcontextprotocol.kotlin.sdk 2 | 3 | import kotlin.annotation.AnnotationTarget.ANNOTATION_CLASS 4 | import kotlin.annotation.AnnotationTarget.CLASS 5 | import kotlin.annotation.AnnotationTarget.CONSTRUCTOR 6 | import kotlin.annotation.AnnotationTarget.FIELD 7 | import kotlin.annotation.AnnotationTarget.FUNCTION 8 | import kotlin.annotation.AnnotationTarget.LOCAL_VARIABLE 9 | import kotlin.annotation.AnnotationTarget.PROPERTY 10 | import kotlin.annotation.AnnotationTarget.PROPERTY_GETTER 11 | import kotlin.annotation.AnnotationTarget.PROPERTY_SETTER 12 | import kotlin.annotation.AnnotationTarget.TYPEALIAS 13 | import kotlin.annotation.AnnotationTarget.VALUE_PARAMETER 14 | 15 | /** 16 | * Annotation marking an API as experimental and subject to changes or removal in the future. 17 | * 18 | * This annotation is used to signal that a particular element, such as a class, function, or property, 19 | * is part of an experimental API. Such APIs may not be stable, and their usage requires opting in 20 | * explicitly. 21 | * 22 | * Users of the annotated API must explicitly accept the opt-in requirement to ensure they are aware 23 | * of the potential instability or unfinished nature of the API. 24 | * 25 | * Targets that can be annotated include: 26 | * - Classes 27 | * - Annotation classes 28 | * - Properties 29 | * - Fields 30 | * - Local variables 31 | * - Value parameters 32 | * - Constructors 33 | * - Functions 34 | * - Property getters 35 | * - Property setters 36 | * - Type aliases 37 | */ 38 | @RequiresOptIn( 39 | message = "This API is experimental. It may be changed in the future without notice.", 40 | level = RequiresOptIn.Level.WARNING, 41 | ) 42 | @MustBeDocumented 43 | @Target( 44 | CLASS, 45 | ANNOTATION_CLASS, 46 | PROPERTY, 47 | FIELD, 48 | LOCAL_VARIABLE, 49 | VALUE_PARAMETER, 50 | CONSTRUCTOR, 51 | FUNCTION, 52 | PROPERTY_GETTER, 53 | PROPERTY_SETTER, 54 | TYPEALIAS, 55 | ) 56 | @Retention(AnnotationRetention.BINARY) 57 | public annotation class ExperimentalMcpApi 58 | -------------------------------------------------------------------------------- /kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/methods.kt: -------------------------------------------------------------------------------- 1 | package io.modelcontextprotocol.kotlin.sdk.types 2 | 3 | import kotlinx.serialization.Serializable 4 | 5 | /** 6 | * Represents a method in the protocol, which can be predefined or custom. 7 | */ 8 | @Serializable(with = MethodSerializer::class) 9 | public sealed interface Method { 10 | public val value: String 11 | 12 | /** 13 | * Enum of predefined methods supported by the protocol. 14 | */ 15 | @Serializable 16 | public enum class Defined(override val value: String) : Method { 17 | Initialize("initialize"), 18 | Ping("ping"), 19 | ResourcesList("resources/list"), 20 | ResourcesTemplatesList("resources/templates/list"), 21 | ResourcesRead("resources/read"), 22 | ResourcesSubscribe("resources/subscribe"), 23 | ResourcesUnsubscribe("resources/unsubscribe"), 24 | PromptsList("prompts/list"), 25 | PromptsGet("prompts/get"), 26 | NotificationsCancelled("notifications/cancelled"), 27 | NotificationsInitialized("notifications/initialized"), 28 | NotificationsProgress("notifications/progress"), 29 | NotificationsMessage("notifications/message"), 30 | NotificationsResourcesUpdated("notifications/resources/updated"), 31 | NotificationsResourcesListChanged("notifications/resources/list_changed"), 32 | NotificationsToolsListChanged("notifications/tools/list_changed"), 33 | NotificationsRootsListChanged("notifications/roots/list_changed"), 34 | NotificationsPromptsListChanged("notifications/prompts/list_changed"), 35 | ToolsList("tools/list"), 36 | ToolsCall("tools/call"), 37 | LoggingSetLevel("logging/setLevel"), 38 | SamplingCreateMessage("sampling/createMessage"), 39 | CompletionComplete("completion/complete"), 40 | RootsList("roots/list"), 41 | ElicitationCreate("elicitation/create"), 42 | } 43 | 44 | /** 45 | * Represents a custom method defined by the user. 46 | */ 47 | @Serializable 48 | public data class Custom(override val value: String) : Method 49 | } 50 | -------------------------------------------------------------------------------- /kotlin-sdk-test/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/shared/OldSchemaBaseTransportTest.kt: -------------------------------------------------------------------------------- 1 | package io.modelcontextprotocol.kotlin.sdk.shared 2 | 3 | import io.modelcontextprotocol.kotlin.sdk.InitializedNotification 4 | import io.modelcontextprotocol.kotlin.sdk.JSONRPCMessage 5 | import io.modelcontextprotocol.kotlin.sdk.PingRequest 6 | import io.modelcontextprotocol.kotlin.sdk.toJSON 7 | import kotlinx.coroutines.CompletableDeferred 8 | import kotlin.test.assertEquals 9 | import kotlin.test.assertFalse 10 | import kotlin.test.assertTrue 11 | import kotlin.test.fail 12 | 13 | abstract class OldSchemaBaseTransportTest { 14 | 15 | protected suspend fun testTransportOpenClose(transport: Transport) { 16 | transport.onError { error -> 17 | fail("Unexpected error: $error") 18 | } 19 | 20 | var didClose = false 21 | transport.onClose { didClose = true } 22 | 23 | transport.start() 24 | assertFalse(didClose, "Transport should not be closed immediately after start") 25 | 26 | transport.close() 27 | assertTrue(didClose, "Transport should be closed after close() call") 28 | } 29 | 30 | protected suspend fun testTransportRead(transport: Transport) { 31 | transport.onError { error -> 32 | error.printStackTrace() 33 | fail("Unexpected error: $error") 34 | } 35 | 36 | val messages = listOf( 37 | PingRequest().toJSON(), 38 | InitializedNotification().toJSON(), 39 | ) 40 | 41 | val readMessages = mutableListOf() 42 | val finished = CompletableDeferred() 43 | 44 | transport.onMessage { message -> 45 | readMessages.add(message) 46 | if (message == messages.last()) { 47 | finished.complete(Unit) 48 | } 49 | } 50 | 51 | transport.start() 52 | 53 | for (message in messages) { 54 | transport.send(message) 55 | } 56 | 57 | finished.await() 58 | 59 | assertEquals(messages, readMessages, "Assert messages received") 60 | 61 | transport.close() 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/Feature.kt: -------------------------------------------------------------------------------- 1 | package io.modelcontextprotocol.kotlin.sdk.server 2 | 3 | import io.modelcontextprotocol.kotlin.sdk.types.CallToolRequest 4 | import io.modelcontextprotocol.kotlin.sdk.types.CallToolResult 5 | import io.modelcontextprotocol.kotlin.sdk.types.GetPromptRequest 6 | import io.modelcontextprotocol.kotlin.sdk.types.GetPromptResult 7 | import io.modelcontextprotocol.kotlin.sdk.types.Prompt 8 | import io.modelcontextprotocol.kotlin.sdk.types.ReadResourceRequest 9 | import io.modelcontextprotocol.kotlin.sdk.types.ReadResourceResult 10 | import io.modelcontextprotocol.kotlin.sdk.types.Resource 11 | import io.modelcontextprotocol.kotlin.sdk.types.Tool 12 | 13 | internal typealias FeatureKey = String 14 | 15 | /** 16 | * Represents a feature with an associated unique key. 17 | */ 18 | internal interface Feature { 19 | val key: FeatureKey 20 | } 21 | 22 | /** 23 | * A wrapper class representing a registered tool on the server. 24 | * 25 | * @property tool The tool definition. 26 | * @property handler A suspend function to handle the tool call requests. 27 | */ 28 | public data class RegisteredTool(val tool: Tool, val handler: suspend (CallToolRequest) -> CallToolResult) : Feature { 29 | override val key: String = tool.name 30 | } 31 | 32 | /** 33 | * A wrapper class representing a registered prompt on the server. 34 | * 35 | * @property prompt The prompt definition. 36 | * @property messageProvider A suspend function that returns the prompt content when requested by the client. 37 | */ 38 | public data class RegisteredPrompt( 39 | val prompt: Prompt, 40 | val messageProvider: suspend (GetPromptRequest) -> GetPromptResult, 41 | ) : Feature { 42 | override val key: String = prompt.name 43 | } 44 | 45 | /** 46 | * A wrapper class representing a registered resource on the server. 47 | * 48 | * @property resource The resource definition. 49 | * @property readHandler A suspend function to handle read requests for this resource. 50 | */ 51 | public data class RegisteredResource( 52 | val resource: Resource, 53 | val readHandler: suspend (ReadResourceRequest) -> ReadResourceResult, 54 | ) : Feature { 55 | override val key: String = resource.uri 56 | } 57 | -------------------------------------------------------------------------------- /kotlin-sdk-test/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/shared/BaseTransportTest.kt: -------------------------------------------------------------------------------- 1 | package io.modelcontextprotocol.kotlin.sdk.shared 2 | 3 | import io.modelcontextprotocol.kotlin.sdk.types.InitializedNotification 4 | import io.modelcontextprotocol.kotlin.sdk.types.JSONRPCMessage 5 | import io.modelcontextprotocol.kotlin.sdk.types.PingRequest 6 | import io.modelcontextprotocol.kotlin.sdk.types.toJSON 7 | import kotlinx.coroutines.CompletableDeferred 8 | import kotlinx.coroutines.delay 9 | import kotlin.test.assertEquals 10 | import kotlin.test.assertFalse 11 | import kotlin.test.assertTrue 12 | import kotlin.test.fail 13 | import kotlin.time.Duration.Companion.seconds 14 | 15 | abstract class BaseTransportTest { 16 | 17 | protected suspend fun testTransportOpenClose(transport: Transport) { 18 | transport.onError { error -> 19 | fail("Unexpected error: $error") 20 | } 21 | 22 | var didClose = false 23 | transport.onClose { didClose = true } 24 | 25 | transport.start() 26 | delay(1.seconds) 27 | 28 | assertFalse(didClose, "Transport should not be closed immediately after start") 29 | 30 | transport.close() 31 | assertTrue(didClose, "Transport should be closed after close() call") 32 | } 33 | 34 | protected suspend fun testTransportRead(transport: Transport) { 35 | transport.onError { error -> 36 | error.printStackTrace() 37 | fail("Unexpected error: $error") 38 | } 39 | 40 | val messages = listOf( 41 | PingRequest().toJSON(), 42 | InitializedNotification().toJSON(), 43 | ) 44 | 45 | val readMessages = mutableListOf() 46 | val finished = CompletableDeferred() 47 | 48 | transport.onMessage { message -> 49 | readMessages.add(message) 50 | if (message == messages.last()) { 51 | finished.complete(Unit) 52 | } 53 | } 54 | 55 | transport.start() 56 | 57 | for (message in messages) { 58 | transport.send(message) 59 | } 60 | 61 | finished.await() 62 | 63 | assertEquals(messages, readMessages, "Assert messages received") 64 | 65 | transport.close() 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/OldSchemaCallToolResultUtilsTest.kt: -------------------------------------------------------------------------------- 1 | package io.modelcontextprotocol.kotlin.sdk 2 | 3 | import kotlinx.serialization.json.JsonPrimitive 4 | import kotlinx.serialization.json.buildJsonObject 5 | import kotlin.test.Test 6 | import kotlin.test.assertEquals 7 | import kotlin.test.assertFalse 8 | import kotlin.test.assertTrue 9 | 10 | class OldSchemaCallToolResultUtilsTest { 11 | 12 | @Test 13 | fun testOkWithOnlyText() { 14 | val content = "TextMessage" 15 | val result = CallToolResult.ok(content) 16 | 17 | assertEquals(1, result.content.size) 18 | assertEquals(content, (result.content[0] as TextContent).text) 19 | assertFalse(result.isError == true) 20 | assertEquals(EmptyJsonObject, result.meta) 21 | } 22 | 23 | @Test 24 | fun testOkWithMeta() { 25 | val content = "TextMessageWithMeta" 26 | val meta = buildJsonObject { 27 | put("key1", JsonPrimitive("value1")) 28 | put("key2", JsonPrimitive(42)) 29 | } 30 | val result = CallToolResult.ok(content, meta) 31 | 32 | assertEquals(1, result.content.size) 33 | assertEquals(content, (result.content[0] as TextContent).text) 34 | assertFalse(result.isError == true) 35 | assertEquals(meta, result.meta) 36 | } 37 | 38 | @Test 39 | fun testErrorWithOnlyText() { 40 | val content = "ErrorMessage" 41 | val result = CallToolResult.error(content) 42 | 43 | assertEquals(1, result.content.size) 44 | assertEquals(content, (result.content[0] as TextContent).text) 45 | assertTrue(result.isError == true) 46 | assertEquals(EmptyJsonObject, result.meta) 47 | } 48 | 49 | @Test 50 | fun testErrorWithMeta() { 51 | val content = "ErrorMessageWithMeta" 52 | val meta = buildJsonObject { 53 | put("errorCode", JsonPrimitive(404)) 54 | put("errorDetail", JsonPrimitive("资源未找到")) 55 | } 56 | val result = CallToolResult.error(content, meta) 57 | 58 | assertEquals(1, result.content.size) 59 | assertEquals(content, (result.content[0] as TextContent).text) 60 | assertTrue(result.isError == true) 61 | assertEquals(meta, result.meta) 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/shared/ReadBufferTest.kt: -------------------------------------------------------------------------------- 1 | package io.modelcontextprotocol.kotlin.sdk.shared 2 | 3 | import io.ktor.utils.io.charsets.Charsets 4 | import io.ktor.utils.io.core.toByteArray 5 | import io.modelcontextprotocol.kotlin.sdk.types.JSONRPCMessage 6 | import io.modelcontextprotocol.kotlin.sdk.types.JSONRPCNotification 7 | import kotlinx.serialization.json.Json 8 | import kotlin.test.Test 9 | import kotlin.test.assertEquals 10 | import kotlin.test.assertNull 11 | 12 | class ReadBufferTest { 13 | private val testMessage: JSONRPCMessage = JSONRPCNotification(method = "foobar") 14 | 15 | private val json = Json { 16 | ignoreUnknownKeys = true 17 | encodeDefaults = true 18 | } 19 | 20 | @Test 21 | fun `should have no messages after initialization`() { 22 | val readBuffer = ReadBuffer() 23 | assertNull(readBuffer.readMessage()) 24 | } 25 | 26 | @Test 27 | fun `should only yield a message after a newline`() { 28 | val readBuffer = ReadBuffer() 29 | 30 | // Append message without a newline 31 | val messageBytes = json.encodeToString(testMessage).encodeToByteArray() 32 | readBuffer.append(messageBytes) 33 | assertNull(readBuffer.readMessage()) 34 | 35 | // Append a newline and verify message is now available 36 | readBuffer.append("\n".encodeToByteArray()) 37 | assertEquals(testMessage, readBuffer.readMessage()) 38 | assertNull(readBuffer.readMessage()) 39 | } 40 | 41 | @Test 42 | fun `skip empty line`() { 43 | val readBuffer = ReadBuffer() 44 | readBuffer.append("\n".toByteArray()) 45 | assertNull(readBuffer.readMessage()) 46 | } 47 | 48 | @Test 49 | fun `should be reusable after clearing`() { 50 | val readBuffer = ReadBuffer() 51 | 52 | readBuffer.append("foobar".toByteArray(Charsets.UTF_8)) 53 | readBuffer.clear() 54 | assertNull(readBuffer.readMessage()) 55 | 56 | val messageJson = serializeMessage(testMessage) 57 | readBuffer.append(messageJson.toByteArray(Charsets.UTF_8)) 58 | readBuffer.append("\n".toByteArray(Charsets.UTF_8)) 59 | val message = readBuffer.readMessage() 60 | assertEquals(testMessage, message) 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/shared/OldSchemaReadBufferTest.kt: -------------------------------------------------------------------------------- 1 | package io.modelcontextprotocol.kotlin.sdk.shared 2 | 3 | import io.ktor.utils.io.charsets.Charsets 4 | import io.ktor.utils.io.core.toByteArray 5 | import io.modelcontextprotocol.kotlin.sdk.JSONRPCMessage 6 | import io.modelcontextprotocol.kotlin.sdk.JSONRPCNotification 7 | import kotlinx.serialization.json.Json 8 | import kotlin.test.Test 9 | import kotlin.test.assertEquals 10 | import kotlin.test.assertNull 11 | 12 | class OldSchemaReadBufferTest { 13 | private val testMessage: JSONRPCMessage = JSONRPCNotification(method = "foobar") 14 | 15 | private val json = Json { 16 | ignoreUnknownKeys = true 17 | encodeDefaults = true 18 | } 19 | 20 | @Test 21 | fun `should have no messages after initialization`() { 22 | val readBuffer = ReadBuffer() 23 | assertNull(readBuffer.readMessage()) 24 | } 25 | 26 | @Test 27 | fun `should only yield a message after a newline`() { 28 | val readBuffer = ReadBuffer() 29 | 30 | // Append message without a newline 31 | val messageBytes = json.encodeToString(testMessage).encodeToByteArray() 32 | readBuffer.append(messageBytes) 33 | assertNull(readBuffer.readMessage()) 34 | 35 | // Append a newline and verify message is now available 36 | readBuffer.append("\n".encodeToByteArray()) 37 | assertEquals(testMessage, readBuffer.readMessage()) 38 | assertNull(readBuffer.readMessage()) 39 | } 40 | 41 | @Test 42 | fun `skip empty line`() { 43 | val readBuffer = ReadBuffer() 44 | readBuffer.append("\n".toByteArray()) 45 | assertNull(readBuffer.readMessage()) 46 | } 47 | 48 | @Test 49 | fun `should be reusable after clearing`() { 50 | val readBuffer = ReadBuffer() 51 | 52 | readBuffer.append("foobar".toByteArray(Charsets.UTF_8)) 53 | readBuffer.clear() 54 | assertNull(readBuffer.readMessage()) 55 | 56 | val messageJson = serializeMessage(testMessage) 57 | readBuffer.append(messageJson.toByteArray(Charsets.UTF_8)) 58 | readBuffer.append("\n".toByteArray(Charsets.UTF_8)) 59 | val message = readBuffer.readMessage() 60 | assertEquals(testMessage, message) 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /kotlin-sdk-server/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/server/ServerSessionRegistry.kt: -------------------------------------------------------------------------------- 1 | package io.modelcontextprotocol.kotlin.sdk.server 2 | 3 | import io.github.oshai.kotlinlogging.KotlinLogging 4 | import kotlinx.atomicfu.atomic 5 | import kotlinx.atomicfu.update 6 | import kotlinx.collections.immutable.persistentMapOf 7 | 8 | internal typealias ServerSessionKey = String 9 | 10 | /** 11 | * Represents a registry for managing server sessions. 12 | */ 13 | internal class ServerSessionRegistry { 14 | 15 | private val logger = KotlinLogging.logger {} 16 | 17 | /** 18 | * Atomic variable used to maintain a thread-safe registry of sessions. 19 | * Stores a persistent map where each session is identified by its unique key. 20 | */ 21 | private val registry = atomic(persistentMapOf()) 22 | 23 | /** 24 | * Returns a read-only view of the current server sessions. 25 | */ 26 | internal val sessions: Map 27 | get() = registry.value 28 | 29 | /** 30 | * Returns a server session by its ID. 31 | * @param sessionId The ID of the session to retrieve. 32 | * @throws IllegalArgumentException If the session doesn't exist. 33 | */ 34 | internal fun getSession(sessionId: ServerSessionKey): ServerSession = 35 | sessions[sessionId] ?: throw IllegalArgumentException("Session not found: $sessionId") 36 | 37 | /** 38 | * Returns a server session by its ID, or null if it doesn't exist. 39 | * @param sessionId The ID of the session to retrieve. 40 | */ 41 | internal fun getSessionOrNull(sessionId: ServerSessionKey): ServerSession? = sessions[sessionId] 42 | 43 | /** 44 | * Registers a server session. 45 | * @param session The session to register. 46 | */ 47 | internal fun addSession(session: ServerSession) { 48 | logger.info { "Adding session: ${session.sessionId}" } 49 | registry.update { sessions -> sessions.put(session.sessionId, session) } 50 | } 51 | 52 | /** 53 | * Removes a server session by its ID. 54 | * @param sessionId The ID of the session to remove. 55 | */ 56 | internal fun removeSession(sessionId: ServerSessionKey) { 57 | logger.info { "Removing session: $sessionId" } 58 | registry.update { sessions -> sessions.remove(sessionId) } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/shared/TransportSendOptions.kt: -------------------------------------------------------------------------------- 1 | package io.modelcontextprotocol.kotlin.sdk.shared 2 | 3 | import io.modelcontextprotocol.kotlin.sdk.types.RequestId 4 | 5 | /** 6 | * Options for sending a JSON-RPC message through transport. 7 | * 8 | * @property relatedRequestId if present, 9 | * `relatedRequestId` is used to indicate to the transport which incoming request to associate this outgoing message with. 10 | * @property resumptionToken the resumption token used to continue long-running requests that were interrupted. 11 | * This allows clients to reconnect and continue from where they left off, if supported by the transport. 12 | * @property onResumptionToken a callback that is invoked when the resumption token changes, if supported by the transport. 13 | * This allows clients to persist the latest token for potential reconnection. 14 | */ 15 | public open class TransportSendOptions( 16 | public val relatedRequestId: RequestId? = null, 17 | public val resumptionToken: String? = null, 18 | public val onResumptionToken: ((String) -> Unit)? = null, 19 | ) { 20 | public operator fun component1(): RequestId? = relatedRequestId 21 | public operator fun component2(): String? = resumptionToken 22 | public operator fun component3(): ((String) -> Unit)? = onResumptionToken 23 | 24 | public open fun copy( 25 | relatedRequestId: RequestId? = this.relatedRequestId, 26 | resumptionToken: String? = this.resumptionToken, 27 | onResumptionToken: ((String) -> Unit)? = this.onResumptionToken, 28 | ): TransportSendOptions = TransportSendOptions(relatedRequestId, resumptionToken, onResumptionToken) 29 | 30 | override fun equals(other: Any?): Boolean { 31 | if (this === other) return true 32 | if (other == null || this::class != other::class) return false 33 | 34 | other as TransportSendOptions 35 | 36 | return relatedRequestId == other.relatedRequestId && 37 | resumptionToken == other.resumptionToken && 38 | onResumptionToken == other.onResumptionToken 39 | } 40 | 41 | override fun hashCode(): Int { 42 | var result = relatedRequestId?.hashCode() ?: 0 43 | result = 31 * result + (resumptionToken?.hashCode() ?: 0) 44 | result = 31 * result + (onResumptionToken?.hashCode() ?: 0) 45 | return result 46 | } 47 | 48 | override fun toString(): String = 49 | "TransportSendOptions(relatedRequestId=$relatedRequestId, resumptionToken=$resumptionToken, onResumptionToken=$onResumptionToken)" 50 | } 51 | -------------------------------------------------------------------------------- /kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/shared/ReadBuffer.kt: -------------------------------------------------------------------------------- 1 | package io.modelcontextprotocol.kotlin.sdk.shared 2 | 3 | import io.github.oshai.kotlinlogging.KotlinLogging 4 | import io.modelcontextprotocol.kotlin.sdk.types.JSONRPCMessage 5 | import io.modelcontextprotocol.kotlin.sdk.types.McpJson 6 | import kotlinx.io.Buffer 7 | import kotlinx.io.indexOf 8 | import kotlinx.io.readString 9 | 10 | /** 11 | * Buffers a continuous stdio stream into discrete JSON-RPC messages. 12 | */ 13 | public class ReadBuffer { 14 | 15 | private val logger = KotlinLogging.logger { } 16 | 17 | private val buffer: Buffer = Buffer() 18 | 19 | public fun append(chunk: ByteArray) { 20 | buffer.write(chunk) 21 | } 22 | 23 | public fun readMessage(): JSONRPCMessage? { 24 | if (buffer.exhausted()) return null 25 | var lfIndex = buffer.indexOf('\n'.code.toByte()) 26 | val line = when (lfIndex) { 27 | -1L -> return null 28 | 29 | 0L -> { 30 | buffer.skip(1) 31 | return null 32 | } 33 | 34 | else -> { 35 | var skipBytes = 1 36 | if (buffer[lfIndex - 1] == '\r'.code.toByte()) { 37 | lfIndex -= 1 38 | skipBytes += 1 39 | } 40 | val string = buffer.readString(lfIndex) 41 | buffer.skip(skipBytes.toLong()) 42 | string 43 | } 44 | } 45 | try { 46 | return deserializeMessage(line) 47 | } catch (e: Exception) { 48 | logger.error(e) { "Failed to deserialize message from line: $line\nAttempting to recover..." } 49 | // if there is a non-JSON object prefix, try to parse from the first '{' onward. 50 | val braceIndex = line.indexOf('{') 51 | if (braceIndex != -1) { 52 | val trimmed = line.substring(braceIndex) 53 | try { 54 | return deserializeMessage(trimmed) 55 | } catch (ignored: Exception) { 56 | logger.error(ignored) { "Deserialization failed for line: $line\nSkipping..." } 57 | } 58 | } 59 | } 60 | 61 | return null 62 | } 63 | 64 | public fun clear() { 65 | buffer.clear() 66 | } 67 | } 68 | 69 | internal fun deserializeMessage(line: String): JSONRPCMessage = McpJson.decodeFromString(line) 70 | 71 | public fun serializeMessage(message: JSONRPCMessage): String = McpJson.encodeToString(message) + "\n" 72 | -------------------------------------------------------------------------------- /kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/models/ProgressNotificationsTest.kt: -------------------------------------------------------------------------------- 1 | package io.modelcontextprotocol.kotlin.sdk.models 2 | 3 | import io.kotest.matchers.shouldBe 4 | import io.modelcontextprotocol.kotlin.sdk.types.McpJson 5 | import io.modelcontextprotocol.kotlin.sdk.types.ProgressNotification 6 | import io.modelcontextprotocol.kotlin.sdk.types.ProgressNotificationParams 7 | import io.modelcontextprotocol.kotlin.sdk.types.RequestId 8 | import kotlin.test.Test 9 | 10 | class ProgressNotificationsTest { 11 | 12 | /** 13 | * https://modelcontextprotocol.io/specification/2025-06-18/basic/utilities/progress#progress-flow 14 | */ 15 | @Test 16 | fun `Read ProgressNotifications with string token`() { 17 | //language=json 18 | val json = """ 19 | { 20 | "jsonrpc": "2.0", 21 | "method": "notifications/progress", 22 | "params": { 23 | "progressToken": "abc123", 24 | "progress": 50, 25 | "total": 100, 26 | "message": "Reticulating splines..." 27 | } 28 | } 29 | """.trimIndent() 30 | 31 | val result = McpJson.decodeFromString(json) 32 | 33 | result shouldBe ProgressNotification( 34 | params = ProgressNotificationParams( 35 | progressToken = RequestId.StringId("abc123"), 36 | progress = 50.0, 37 | message = "Reticulating splines...", 38 | total = 100.0, 39 | ), 40 | ) 41 | } 42 | 43 | /** 44 | * https://modelcontextprotocol.io/specification/2025-06-18/basic/utilities/progress#progress-flow 45 | */ 46 | @Test 47 | fun `Read ProgressNotifications with integer token`() { 48 | //language=json 49 | val json = """ 50 | { 51 | "jsonrpc": "2.0", 52 | "method": "notifications/progress", 53 | "params": { 54 | "progressToken": 100500, 55 | "progress": 50, 56 | "total": 100, 57 | "message": "Reticulating splines..." 58 | } 59 | } 60 | """.trimIndent() 61 | 62 | val result = McpJson.decodeFromString(json) 63 | result shouldBe ProgressNotification( 64 | params = ProgressNotificationParams( 65 | progressToken = RequestId.NumberId(100500), 66 | progress = 50.0, 67 | message = "Reticulating splines...", 68 | total = 100.0, 69 | ), 70 | ) 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/models/OldSchemaProgressNotificationsTest.kt: -------------------------------------------------------------------------------- 1 | package io.modelcontextprotocol.kotlin.sdk.models 2 | 3 | import io.kotest.matchers.shouldBe 4 | import io.modelcontextprotocol.kotlin.sdk.ProgressNotification 5 | import io.modelcontextprotocol.kotlin.sdk.RequestId 6 | import io.modelcontextprotocol.kotlin.sdk.shared.McpJson 7 | import kotlin.test.Test 8 | 9 | class OldSchemaProgressNotificationsTest { 10 | 11 | /** 12 | * https://modelcontextprotocol.io/specification/2025-06-18/basic/utilities/progress#progress-flow 13 | */ 14 | @Test 15 | fun `Read ProgressNotifications with string token`() { 16 | //language=json 17 | val json = """ 18 | { 19 | "jsonrpc": "2.0", 20 | "method": "notifications/progress", 21 | "params": { 22 | "progressToken": "abc123", 23 | "progress": 50, 24 | "total": 100, 25 | "message": "Reticulating splines..." 26 | } 27 | } 28 | """.trimIndent() 29 | 30 | val result = McpJson.decodeFromString(json) 31 | 32 | result shouldBe ProgressNotification( 33 | params = ProgressNotification.Params( 34 | progressToken = RequestId.StringId("abc123"), 35 | progress = 50.0, 36 | message = "Reticulating splines...", 37 | total = 100.0, 38 | ), 39 | ) 40 | } 41 | 42 | /** 43 | * https://modelcontextprotocol.io/specification/2025-06-18/basic/utilities/progress#progress-flow 44 | */ 45 | @Test 46 | fun `Read ProgressNotifications with integer token`() { 47 | //language=json 48 | val json = """ 49 | { 50 | "jsonrpc": "2.0", 51 | "method": "notifications/progress", 52 | "params": { 53 | "progressToken": 100500, 54 | "progress": 50, 55 | "total": 100, 56 | "message": "Reticulating splines..." 57 | } 58 | } 59 | """.trimIndent() 60 | 61 | val result = McpJson.decodeFromString(json) 62 | result shouldBe ProgressNotification( 63 | params = ProgressNotification.Params( 64 | progressToken = RequestId.NumberId(100500), 65 | progress = 50.0, 66 | message = "Reticulating splines...", 67 | total = 100.0, 68 | ), 69 | ) 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /kotlin-sdk-client/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/AbstractClientTransportLifecycleTest.kt: -------------------------------------------------------------------------------- 1 | package io.modelcontextprotocol.kotlin.sdk.client 2 | 3 | import io.kotest.assertions.throwables.shouldThrow 4 | import io.kotest.matchers.shouldBe 5 | import io.kotest.matchers.string.shouldContain 6 | import io.modelcontextprotocol.kotlin.sdk.shared.AbstractTransport 7 | import io.modelcontextprotocol.kotlin.sdk.types.PingRequest 8 | import io.modelcontextprotocol.kotlin.sdk.types.toJSON 9 | import kotlinx.coroutines.delay 10 | import kotlinx.coroutines.test.runTest 11 | import kotlin.test.BeforeTest 12 | import kotlin.test.Test 13 | import kotlin.time.Duration.Companion.milliseconds 14 | 15 | abstract class AbstractClientTransportLifecycleTest { 16 | 17 | protected lateinit var transport: T 18 | 19 | @BeforeTest 20 | fun beforeEach() { 21 | transport = createTransport() 22 | } 23 | 24 | @Test 25 | fun `should throw when started twice`() = runTest { 26 | transport.start() 27 | 28 | val exception = shouldThrow { 29 | transport.start() 30 | } 31 | exception.message shouldContain "already started" 32 | } 33 | 34 | @Test 35 | fun `should be idempotent when closed twice`() = runTest { 36 | val transport = createTransport() 37 | 38 | transport.start() 39 | transport.close() 40 | 41 | // Second close should not throw 42 | transport.close() 43 | } 44 | 45 | @Test 46 | fun `should throw when sending before start`() = runTest { 47 | val transport = createTransport() 48 | 49 | val exception = shouldThrow { 50 | transport.send(PingRequest().toJSON()) 51 | } 52 | exception.message shouldContain "not started" 53 | } 54 | 55 | @Test 56 | fun `should throw when sending after close`() = runTest { 57 | val transport = createTransport() 58 | 59 | transport.start() 60 | delay(50.milliseconds) 61 | transport.close() 62 | 63 | shouldThrow { 64 | transport.send(PingRequest().toJSON()) 65 | } 66 | } 67 | 68 | @Test 69 | fun `should call onClose exactly once`() = runTest { 70 | val transport = createTransport() 71 | 72 | var closeCallCount = 0 73 | transport.onClose { closeCallCount++ } 74 | 75 | transport.start() 76 | delay(50.milliseconds) 77 | 78 | // Multiple close attempts 79 | transport.close() 80 | transport.close() 81 | 82 | closeCallCount shouldBe 1 83 | } 84 | 85 | protected abstract fun createTransport(): T 86 | } 87 | -------------------------------------------------------------------------------- /kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/logging.dsl.kt: -------------------------------------------------------------------------------- 1 | package io.modelcontextprotocol.kotlin.sdk.types 2 | 3 | import io.modelcontextprotocol.kotlin.sdk.ExperimentalMcpApi 4 | import kotlin.contracts.ExperimentalContracts 5 | import kotlin.contracts.InvocationKind 6 | import kotlin.contracts.contract 7 | 8 | /** 9 | * Creates a [SetLevelRequest] using a type-safe DSL builder. 10 | * 11 | * ## Required 12 | * - [loggingLevel][SetLevelRequestBuilder.loggingLevel] - The logging level to set 13 | * 14 | * ## Optional 15 | * - [meta][SetLevelRequestBuilder.meta] - Metadata for the request 16 | * 17 | * Example setting info level: 18 | * ```kotlin 19 | * val request = buildSetLevelRequest { 20 | * loggingLevel = LoggingLevel.Info 21 | * } 22 | * ``` 23 | * 24 | * Example setting debug level: 25 | * ```kotlin 26 | * val request = buildSetLevelRequest { 27 | * loggingLevel = LoggingLevel.Debug 28 | * } 29 | * ``` 30 | * 31 | * @param block Configuration lambda for setting up the logging level request 32 | * @return A configured [SetLevelRequest] instance 33 | * @see SetLevelRequestBuilder 34 | * @see SetLevelRequest 35 | * @see LoggingLevel 36 | */ 37 | @OptIn(ExperimentalContracts::class) 38 | @ExperimentalMcpApi 39 | internal inline fun buildSetLevelRequest(block: SetLevelRequestBuilder.() -> Unit): SetLevelRequest { 40 | contract { callsInPlace(block, InvocationKind.EXACTLY_ONCE) } 41 | return SetLevelRequestBuilder().apply(block).build() 42 | } 43 | 44 | /** 45 | * DSL builder for constructing [SetLevelRequest] instances. 46 | * 47 | * This builder creates requests to set the logging level for the server. 48 | * 49 | * ## Required 50 | * - [loggingLevel] - The logging level to set 51 | * 52 | * ## Optional 53 | * - [meta] - Metadata for the request 54 | * 55 | * @see buildSetLevelRequest 56 | * @see SetLevelRequest 57 | * @see LoggingLevel 58 | */ 59 | @McpDsl 60 | public class SetLevelRequestBuilder @PublishedApi internal constructor() : RequestBuilder() { 61 | /** 62 | * The logging level to set. This is a required field. 63 | * 64 | * Available levels (from least to most severe): 65 | * - [LoggingLevel.Debug] 66 | * - [LoggingLevel.Info] 67 | * - [LoggingLevel.Notice] 68 | * - [LoggingLevel.Warning] 69 | * - [LoggingLevel.Error] 70 | * - [LoggingLevel.Critical] 71 | * - [LoggingLevel.Alert] 72 | * - [LoggingLevel.Emergency] 73 | * 74 | * Example: `loggingLevel = LoggingLevel.Warning` 75 | */ 76 | public var loggingLevel: LoggingLevel? = null 77 | 78 | @PublishedApi 79 | override fun build(): SetLevelRequest { 80 | val level = requireNotNull(loggingLevel) { 81 | "Missing required field 'loggingLevel'. Example: loggingLevel = LoggingLevel.Info" 82 | } 83 | 84 | val params = SetLevelRequestParams(level = level, meta = meta) 85 | return SetLevelRequest(params = params) 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /samples/kotlin-mcp-client/README.md: -------------------------------------------------------------------------------- 1 | # Kotlin MCP Client 2 | 3 | This project demonstrates how to build a Model Context Protocol (MCP) client in Kotlin that interacts with an MCP server 4 | via a STDIO transport layer while leveraging Anthropic's API for natural language processing. The client uses the MCP 5 | Kotlin SDK to communicate with an MCP server that exposes various tools, and it uses Anthropic's API to process user 6 | queries and integrate tool responses into the conversation. 7 | 8 | For more information about the MCP SDK and protocol, please refer to 9 | the [MCP documentation](https://modelcontextprotocol.io/introduction). 10 | 11 | ## Prerequisites 12 | 13 | - **Java 17 or later** 14 | - **Gradle** (or the Gradle wrapper provided with the project) 15 | - An Anthropic API key set in your environment variable `ANTHROPIC_API_KEY` 16 | - Basic understanding of MCP concepts and Kotlin programming 17 | 18 | ## Overview 19 | 20 | The client application performs the following tasks: 21 | 22 | - **Connecting to an MCP server** — 23 | launches an MCP server process (implemented in JavaScript, Python, or Java) using STDIO transport. 24 | It connects to the server, retrieves available tools, and converts them to Anthropic’s tool format. 25 | - **Processing queries** — 26 | accepts user queries, sends them to Anthropic’s API along with the registered tools, and handles responses. 27 | If the response indicates a tool should be called, it invokes the corresponding MCP tool and continues the 28 | conversation based on the tool’s result. 29 | - **Interactive chat loop** — 30 | runs an interactive command-line loop, allowing users to continuously submit queries and receive responses. 31 | 32 | ## Building and Running 33 | 34 | Use the Gradle wrapper to build the application. In a terminal, run: 35 | 36 | ```shell 37 | ./gradlew clean build 38 | ``` 39 | 40 | To run the client, execute the jar file and provide the path to your MCP server script. 41 | 42 | To run the client with any MCP server: 43 | 44 | ```shell 45 | java -jar build/libs/.jar path/to/server.jar # jvm server 46 | java -jar build/libs/.jar path/to/server.py # python server 47 | java -jar build/libs/.jar path/to/build/index.js # node server 48 | ``` 49 | 50 | > [!NOTE] 51 | > The client uses STDIO transport, so it launches the MCP server as a separate process. 52 | > Ensure the server script is executable and is a valid `.js`, `.py`, or `.jar` file. 53 | 54 | ## Configuration for Anthropic 55 | 56 | Ensure your Anthropic API key is available in your environment: 57 | 58 | ```shell 59 | export ANTHROPIC_API_KEY=your_anthropic_api_key_here 60 | ``` 61 | 62 | The client uses `AnthropicOkHttpClient.fromEnv()` to automatically load the API key from `ANTHROPIC_API_KEY` and 63 | `ANTHROPIC_AUTH_TOKEN` environment variables. 64 | 65 | ## Additional Resources 66 | 67 | - [MCP Specification](https://spec.modelcontextprotocol.io/) 68 | - [Kotlin MCP SDK](https://github.com/modelcontextprotocol/kotlin-sdk) 69 | - [Anthropic Java SDK](https://github.com/anthropics/anthropic-sdk-java/tree/main) 70 | -------------------------------------------------------------------------------- /kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/PingRequestTest.kt: -------------------------------------------------------------------------------- 1 | package io.modelcontextprotocol.kotlin.sdk.types 2 | 3 | import io.kotest.assertions.json.shouldEqualJson 4 | import kotlinx.serialization.json.buildJsonObject 5 | import kotlinx.serialization.json.int 6 | import kotlinx.serialization.json.jsonObject 7 | import kotlinx.serialization.json.jsonPrimitive 8 | import kotlinx.serialization.json.put 9 | import kotlin.test.Test 10 | import kotlin.test.assertEquals 11 | import kotlin.test.assertIs 12 | import kotlin.test.assertNotNull 13 | 14 | class PingRequestTest { 15 | 16 | @Test 17 | fun `should serialize PingRequest without params`() { 18 | val request = PingRequest() 19 | 20 | val json = McpJson.encodeToString(request) 21 | 22 | json shouldEqualJson """ 23 | { 24 | "method": "ping" 25 | } 26 | """.trimIndent() 27 | } 28 | 29 | @Test 30 | fun `should serialize PingRequest with meta`() { 31 | val request = PingRequest( 32 | BaseRequestParams( 33 | meta = RequestMeta( 34 | buildJsonObject { put("progressToken", "ping-42") }, 35 | ), 36 | ), 37 | ) 38 | 39 | val json = McpJson.encodeToString(request) 40 | 41 | json shouldEqualJson """ 42 | { 43 | "method": "ping", 44 | "params": { 45 | "_meta": { 46 | "progressToken": "ping-42" 47 | } 48 | } 49 | } 50 | """.trimIndent() 51 | } 52 | 53 | @Test 54 | fun `should convert PingRequest to JSONRPCRequest`() { 55 | val request = PingRequest( 56 | BaseRequestParams( 57 | meta = RequestMeta( 58 | buildJsonObject { put("progressToken", 99) }, 59 | ), 60 | ), 61 | ) 62 | 63 | val jsonRpc = request.toJSON() 64 | 65 | assertEquals("ping", jsonRpc.method) 66 | val params = assertNotNull(jsonRpc.params).jsonObject 67 | val meta = params["_meta"]?.jsonObject 68 | assertNotNull(meta) 69 | assertEquals(99, meta["progressToken"]?.jsonPrimitive?.int) 70 | } 71 | 72 | @Test 73 | fun `should deserialize JSONRPCRequest to PingRequest`() { 74 | val json = """ 75 | { 76 | "id": "ping-1", 77 | "method": "ping", 78 | "jsonrpc": "2.0", 79 | "params": { 80 | "_meta": { 81 | "progressToken": "pong-1" 82 | } 83 | } 84 | } 85 | """.trimIndent() 86 | 87 | val jsonRpc = McpJson.decodeFromString(json) 88 | val request = jsonRpc.fromJSON() 89 | 90 | val pingRequest = assertIs(request) 91 | val meta = pingRequest.params?.meta?.json 92 | assertNotNull(meta) 93 | assertEquals("pong-1", meta["progressToken"]?.jsonPrimitive?.content) 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/jsonUtils.kt: -------------------------------------------------------------------------------- 1 | package io.modelcontextprotocol.kotlin.sdk.types 2 | 3 | import kotlinx.serialization.ExperimentalSerializationApi 4 | import kotlinx.serialization.json.ClassDiscriminatorMode 5 | import kotlinx.serialization.json.Json 6 | import kotlinx.serialization.json.JsonElement 7 | import kotlinx.serialization.json.JsonNull 8 | import kotlinx.serialization.json.JsonObject 9 | import kotlinx.serialization.json.JsonPrimitive 10 | import kotlinx.serialization.json.add 11 | import kotlinx.serialization.json.buildJsonArray 12 | import kotlinx.serialization.json.buildJsonObject 13 | 14 | public val EmptyJsonObject: JsonObject = JsonObject(emptyMap()) 15 | 16 | @OptIn(ExperimentalSerializationApi::class) 17 | public val McpJson: Json by lazy { 18 | Json { 19 | ignoreUnknownKeys = true 20 | encodeDefaults = true 21 | isLenient = true 22 | classDiscriminatorMode = ClassDiscriminatorMode.NONE 23 | explicitNulls = false 24 | } 25 | } 26 | 27 | public fun Map.toJson(): Map = this.mapValues { (_, value) -> 28 | runCatching { convertToJsonElement(value) } 29 | .getOrElse { JsonPrimitive(value.toString()) } 30 | } 31 | 32 | @OptIn(ExperimentalUnsignedTypes::class, ExperimentalSerializationApi::class) 33 | private fun convertToJsonElement(value: Any?): JsonElement = when (value) { 34 | null -> JsonNull 35 | 36 | is JsonElement -> value 37 | 38 | is String -> JsonPrimitive(value) 39 | 40 | is Number -> JsonPrimitive(value) 41 | 42 | is Boolean -> JsonPrimitive(value) 43 | 44 | is Char -> JsonPrimitive(value.toString()) 45 | 46 | is Enum<*> -> JsonPrimitive(value.name) 47 | 48 | is Map<*, *> -> buildJsonObject { value.forEach { (k, v) -> put(k.toString(), convertToJsonElement(v)) } } 49 | 50 | is Collection<*> -> buildJsonArray { value.forEach { add(convertToJsonElement(it)) } } 51 | 52 | is Array<*> -> buildJsonArray { value.forEach { add(convertToJsonElement(it)) } } 53 | 54 | // Primitive arrays 55 | is IntArray -> buildJsonArray { value.forEach { add(it) } } 56 | 57 | is LongArray -> buildJsonArray { value.forEach { add(it) } } 58 | 59 | is FloatArray -> buildJsonArray { value.forEach { add(it) } } 60 | 61 | is DoubleArray -> buildJsonArray { value.forEach { add(it) } } 62 | 63 | is BooleanArray -> buildJsonArray { value.forEach { add(it) } } 64 | 65 | is ShortArray -> buildJsonArray { value.forEach { add(it) } } 66 | 67 | is ByteArray -> buildJsonArray { value.forEach { add(it) } } 68 | 69 | is CharArray -> buildJsonArray { value.forEach { add(it.toString()) } } 70 | 71 | // Unsigned arrays 72 | is UIntArray -> buildJsonArray { value.forEach { add(JsonPrimitive(it)) } } 73 | 74 | is ULongArray -> buildJsonArray { value.forEach { add(JsonPrimitive(it)) } } 75 | 76 | is UShortArray -> buildJsonArray { value.forEach { add(JsonPrimitive(it)) } } 77 | 78 | is UByteArray -> buildJsonArray { value.forEach { add(JsonPrimitive(it)) } } 79 | 80 | else -> { 81 | JsonPrimitive(value.toString()) 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: 4 | workflow_dispatch: 5 | pull_request: 6 | branches: [ main ] 7 | push: 8 | branches: [ main ] 9 | 10 | concurrency: 11 | group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} 12 | # Cancel only when the run is NOT on `main` branch 13 | cancel-in-progress: ${{ github.ref != 'refs/heads/main' }} 14 | 15 | permissions: 16 | checks: write 17 | pull-requests: write # only required if `comment: true` was enabled 18 | 19 | jobs: 20 | build: 21 | runs-on: macos-latest-xlarge 22 | name: Build 23 | timeout-minutes: 20 24 | env: 25 | JAVA_OPTS: "-Xmx8g -Dfile.encoding=UTF-8 -Djava.awt.headless=true -Dkotlin.daemon.jvm.options=-Xmx6g" 26 | steps: 27 | - uses: actions/checkout@v6 28 | 29 | - name: Set up JDK 21 30 | uses: actions/setup-java@v5 31 | with: 32 | java-version: '21' 33 | distribution: 'temurin' 34 | 35 | - name: Setup Gradle 36 | uses: gradle/actions/setup-gradle@v5 37 | with: 38 | add-job-summary: 'always' 39 | cache-read-only: true 40 | 41 | - name: Build with Gradle 42 | run: |- 43 | ./gradlew --no-daemon \ 44 | --rerun-tasks \ 45 | clean \ 46 | ktlintCheck \ 47 | build \ 48 | -x :conformance-test:test \ 49 | koverLog koverHtmlReport \ 50 | publishToMavenLocal \ 51 | -Pversion=0.0.1-SNAPSHOT 52 | 53 | - name: Build Kotlin-MCP-Client Sample 54 | working-directory: ./samples/kotlin-mcp-client 55 | run: ./gradlew --no-daemon clean build -Pmcp.kotlin.overrideVersion=0.0.1-SNAPSHOT 56 | 57 | - name: Build Kotlin-MCP-Server Sample 58 | working-directory: ./samples/kotlin-mcp-server 59 | run: ./gradlew --no-daemon clean build -Pmcp.kotlin.overrideVersion=0.0.1-SNAPSHOT 60 | 61 | - name: Build Weather-Stdio-Server Sample 62 | working-directory: ./samples/weather-stdio-server 63 | run: ./gradlew --no-daemon clean build -Pmcp.kotlin.overrideVersion=0.0.1-SNAPSHOT 64 | 65 | - name: Upload Reports 66 | if: ${{ !cancelled() }} 67 | uses: actions/upload-artifact@v5 68 | with: 69 | name: reports 70 | path: | 71 | **/build/reports/ 72 | 73 | - name: Publish Test Report 74 | uses: mikepenz/action-junit-report@v6 75 | if: ${{ !cancelled() }} # always run even if the previous step fails 76 | with: 77 | annotate_only: true 78 | detailed_summary: true 79 | flaky_summary: true 80 | group_suite: true 81 | include_empty_in_summary: false 82 | include_time_in_summary: true 83 | report_paths: '**/test-results/**/TEST-*.xml' 84 | truncate_stack_traces: false 85 | 86 | - name: Disable Auto-Merge on Fail 87 | if: failure() && github.event_name == 'pull_request' 88 | run: gh pr merge --disable-auto "$PR_URL" 89 | env: 90 | PR_URL: ${{github.event.pull_request.html_url}} 91 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 92 | -------------------------------------------------------------------------------- /samples/kotlin-mcp-client/gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | @rem SPDX-License-Identifier: Apache-2.0 17 | @rem 18 | 19 | @if "%DEBUG%"=="" @echo off 20 | @rem ########################################################################## 21 | @rem 22 | @rem Gradle startup script for Windows 23 | @rem 24 | @rem ########################################################################## 25 | 26 | @rem Set local scope for the variables with windows NT shell 27 | if "%OS%"=="Windows_NT" setlocal 28 | 29 | set DIRNAME=%~dp0 30 | if "%DIRNAME%"=="" set DIRNAME=. 31 | @rem This is normally unused 32 | set APP_BASE_NAME=%~n0 33 | set APP_HOME=%DIRNAME% 34 | 35 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 36 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 37 | 38 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 39 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 40 | 41 | @rem Find java.exe 42 | if defined JAVA_HOME goto findJavaFromJavaHome 43 | 44 | set JAVA_EXE=java.exe 45 | %JAVA_EXE% -version >NUL 2>&1 46 | if %ERRORLEVEL% equ 0 goto execute 47 | 48 | echo. 1>&2 49 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 50 | echo. 1>&2 51 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2 52 | echo location of your Java installation. 1>&2 53 | 54 | goto fail 55 | 56 | :findJavaFromJavaHome 57 | set JAVA_HOME=%JAVA_HOME:"=% 58 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 59 | 60 | if exist "%JAVA_EXE%" goto execute 61 | 62 | echo. 1>&2 63 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 64 | echo. 1>&2 65 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2 66 | echo location of your Java installation. 1>&2 67 | 68 | goto fail 69 | 70 | :execute 71 | @rem Setup the command line 72 | 73 | 74 | 75 | @rem Execute Gradle 76 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* 77 | 78 | :end 79 | @rem End local scope for the variables with windows NT shell 80 | if %ERRORLEVEL% equ 0 goto mainEnd 81 | 82 | :fail 83 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 84 | rem the _cmd.exe /c_ return code! 85 | set EXIT_CODE=%ERRORLEVEL% 86 | if %EXIT_CODE% equ 0 set EXIT_CODE=1 87 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% 88 | exit /b %EXIT_CODE% 89 | 90 | :mainEnd 91 | if "%OS%"=="Windows_NT" endlocal 92 | 93 | :omega 94 | -------------------------------------------------------------------------------- /samples/kotlin-mcp-server/gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | @rem SPDX-License-Identifier: Apache-2.0 17 | @rem 18 | 19 | @if "%DEBUG%"=="" @echo off 20 | @rem ########################################################################## 21 | @rem 22 | @rem Gradle startup script for Windows 23 | @rem 24 | @rem ########################################################################## 25 | 26 | @rem Set local scope for the variables with windows NT shell 27 | if "%OS%"=="Windows_NT" setlocal 28 | 29 | set DIRNAME=%~dp0 30 | if "%DIRNAME%"=="" set DIRNAME=. 31 | @rem This is normally unused 32 | set APP_BASE_NAME=%~n0 33 | set APP_HOME=%DIRNAME% 34 | 35 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 36 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 37 | 38 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 39 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 40 | 41 | @rem Find java.exe 42 | if defined JAVA_HOME goto findJavaFromJavaHome 43 | 44 | set JAVA_EXE=java.exe 45 | %JAVA_EXE% -version >NUL 2>&1 46 | if %ERRORLEVEL% equ 0 goto execute 47 | 48 | echo. 1>&2 49 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 50 | echo. 1>&2 51 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2 52 | echo location of your Java installation. 1>&2 53 | 54 | goto fail 55 | 56 | :findJavaFromJavaHome 57 | set JAVA_HOME=%JAVA_HOME:"=% 58 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 59 | 60 | if exist "%JAVA_EXE%" goto execute 61 | 62 | echo. 1>&2 63 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 64 | echo. 1>&2 65 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2 66 | echo location of your Java installation. 1>&2 67 | 68 | goto fail 69 | 70 | :execute 71 | @rem Setup the command line 72 | 73 | 74 | 75 | @rem Execute Gradle 76 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* 77 | 78 | :end 79 | @rem End local scope for the variables with windows NT shell 80 | if %ERRORLEVEL% equ 0 goto mainEnd 81 | 82 | :fail 83 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 84 | rem the _cmd.exe /c_ return code! 85 | set EXIT_CODE=%ERRORLEVEL% 86 | if %EXIT_CODE% equ 0 set EXIT_CODE=1 87 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% 88 | exit /b %EXIT_CODE% 89 | 90 | :mainEnd 91 | if "%OS%"=="Windows_NT" endlocal 92 | 93 | :omega 94 | -------------------------------------------------------------------------------- /samples/weather-stdio-server/gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | @rem SPDX-License-Identifier: Apache-2.0 17 | @rem 18 | 19 | @if "%DEBUG%"=="" @echo off 20 | @rem ########################################################################## 21 | @rem 22 | @rem Gradle startup script for Windows 23 | @rem 24 | @rem ########################################################################## 25 | 26 | @rem Set local scope for the variables with windows NT shell 27 | if "%OS%"=="Windows_NT" setlocal 28 | 29 | set DIRNAME=%~dp0 30 | if "%DIRNAME%"=="" set DIRNAME=. 31 | @rem This is normally unused 32 | set APP_BASE_NAME=%~n0 33 | set APP_HOME=%DIRNAME% 34 | 35 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 36 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 37 | 38 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 39 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 40 | 41 | @rem Find java.exe 42 | if defined JAVA_HOME goto findJavaFromJavaHome 43 | 44 | set JAVA_EXE=java.exe 45 | %JAVA_EXE% -version >NUL 2>&1 46 | if %ERRORLEVEL% equ 0 goto execute 47 | 48 | echo. 1>&2 49 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 50 | echo. 1>&2 51 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2 52 | echo location of your Java installation. 1>&2 53 | 54 | goto fail 55 | 56 | :findJavaFromJavaHome 57 | set JAVA_HOME=%JAVA_HOME:"=% 58 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 59 | 60 | if exist "%JAVA_EXE%" goto execute 61 | 62 | echo. 1>&2 63 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 64 | echo. 1>&2 65 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2 66 | echo location of your Java installation. 1>&2 67 | 68 | goto fail 69 | 70 | :execute 71 | @rem Setup the command line 72 | 73 | 74 | 75 | @rem Execute Gradle 76 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* 77 | 78 | :end 79 | @rem End local scope for the variables with windows NT shell 80 | if %ERRORLEVEL% equ 0 goto mainEnd 81 | 82 | :fail 83 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 84 | rem the _cmd.exe /c_ return code! 85 | set EXIT_CODE=%ERRORLEVEL% 86 | if %EXIT_CODE% equ 0 set EXIT_CODE=1 87 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% 88 | exit /b %EXIT_CODE% 89 | 90 | :mainEnd 91 | if "%OS%"=="Windows_NT" endlocal 92 | 93 | :omega 94 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | @rem SPDX-License-Identifier: Apache-2.0 17 | @rem 18 | 19 | @if "%DEBUG%"=="" @echo off 20 | @rem ########################################################################## 21 | @rem 22 | @rem Gradle startup script for Windows 23 | @rem 24 | @rem ########################################################################## 25 | 26 | @rem Set local scope for the variables with windows NT shell 27 | if "%OS%"=="Windows_NT" setlocal 28 | 29 | set DIRNAME=%~dp0 30 | if "%DIRNAME%"=="" set DIRNAME=. 31 | @rem This is normally unused 32 | set APP_BASE_NAME=%~n0 33 | set APP_HOME=%DIRNAME% 34 | 35 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 36 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 37 | 38 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 39 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 40 | 41 | @rem Find java.exe 42 | if defined JAVA_HOME goto findJavaFromJavaHome 43 | 44 | set JAVA_EXE=java.exe 45 | %JAVA_EXE% -version >NUL 2>&1 46 | if %ERRORLEVEL% equ 0 goto execute 47 | 48 | echo. 1>&2 49 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 50 | echo. 1>&2 51 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2 52 | echo location of your Java installation. 1>&2 53 | 54 | goto fail 55 | 56 | :findJavaFromJavaHome 57 | set JAVA_HOME=%JAVA_HOME:"=% 58 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 59 | 60 | if exist "%JAVA_EXE%" goto execute 61 | 62 | echo. 1>&2 63 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 64 | echo. 1>&2 65 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2 66 | echo location of your Java installation. 1>&2 67 | 68 | goto fail 69 | 70 | :execute 71 | @rem Setup the command line 72 | 73 | 74 | 75 | @rem Execute Gradle 76 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* 77 | 78 | :end 79 | @rem End local scope for the variables with windows NT shell 80 | if %ERRORLEVEL% equ 0 goto mainEnd 81 | 82 | :fail 83 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 84 | rem the _cmd.exe /c_ return code! 85 | set EXIT_CODE=%ERRORLEVEL% 86 | if %EXIT_CODE% equ 0 set EXIT_CODE=1 87 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% 88 | exit /b %EXIT_CODE% 89 | 90 | :mainEnd 91 | if "%OS%"=="Windows_NT" endlocal 92 | 93 | :omega 94 | -------------------------------------------------------------------------------- /kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/stdio/simpleStdio.ts: -------------------------------------------------------------------------------- 1 | // @ts-nocheck 2 | import { z } from 'zod'; 3 | import path from 'node:path'; 4 | import { pathToFileURL } from 'node:url'; 5 | 6 | const SDK_DIR = process.env.TYPESCRIPT_SDK_DIR; 7 | if (!SDK_DIR) { 8 | throw new Error('TYPESCRIPT_SDK_DIR environment variable is not set. It should point to the cloned typescript-sdk directory.'); 9 | } 10 | 11 | async function importFromSdk(rel: string): Promise { 12 | const full = path.resolve(SDK_DIR!, rel); 13 | const url = pathToFileURL(full).href; 14 | return await import(url); 15 | } 16 | 17 | async function main() { 18 | const { McpServer } = await importFromSdk('src/server/mcp.ts'); 19 | const { StdioServerTransport } = await importFromSdk('src/server/stdio.ts'); 20 | 21 | const server = new McpServer({ 22 | name: 'simple-stdio-server', 23 | version: '1.0.0', 24 | }, { capabilities: { logging: {} } }); 25 | 26 | // Simple tools mirroring ones from HTTP test server 27 | server.registerTool('greet', { 28 | title: 'Greeting Tool', 29 | description: 'A simple greeting tool', 30 | inputSchema: { name: z.string().describe('Name to greet') }, 31 | }, async ({ name }): Promise => { 32 | return { content: [{ type: 'text', text: `Hello, ${name}!` }] }; 33 | }); 34 | 35 | server.tool('multi-greet', 'A tool that sends different greetings with delays between them', 36 | { name: z.string().describe('Name to greet') }, 37 | { title: 'Multiple Greeting Tool', readOnlyHint: true, openWorldHint: false }, 38 | async ({ name }, extra): Promise => { 39 | const sleep = (ms: number) => new Promise(r => setTimeout(r, ms)); 40 | await server.sendLoggingMessage({ level: 'debug', data: `Starting multi-greet for ${name}` }, extra.sessionId); 41 | await sleep(200); 42 | await server.sendLoggingMessage({ level: 'info', data: `Sending first greeting to ${name}` }, extra.sessionId); 43 | await sleep(200); 44 | await server.sendLoggingMessage({ level: 'info', data: `Sending second greeting to ${name}` }, extra.sessionId); 45 | return { content: [{ type: 'text', text: `Good morning, ${name}!` }] }; 46 | } 47 | ); 48 | 49 | server.registerPrompt('greeting-template', { 50 | title: 'Greeting Template', 51 | description: 'A simple greeting prompt template', 52 | argsSchema: { name: z.string().describe('Name to include in greeting') }, 53 | }, async ({ name }): Promise => { 54 | return { 55 | messages: [{ role: 'user', content: { type: 'text', text: `Please greet ${name} in a friendly manner.` } }], 56 | }; 57 | }); 58 | 59 | server.registerResource('greeting-resource', 'https://example.com/greetings/default', { 60 | title: 'Default Greeting', 61 | description: 'A simple greeting resource', 62 | mimeType: 'text/plain', 63 | }, async (): Promise => { 64 | return { contents: [{ uri: 'https://example.com/greetings/default', text: 'Hello, world!' }] }; 65 | }); 66 | 67 | const transport = new StdioServerTransport(); 68 | await server.connect(transport); 69 | } 70 | 71 | main().catch((err) => { 72 | console.error('Failed to start stdio server:', err); 73 | process.exit(1); 74 | }); 75 | -------------------------------------------------------------------------------- /samples/kotlin-mcp-server/README.md: -------------------------------------------------------------------------------- 1 | # MCP Kotlin Server Sample 2 | 3 | A sample implementation of an MCP (Model Context Protocol) server in Kotlin that demonstrates different server 4 | configurations and transport methods. 5 | 6 | ## Features 7 | 8 | - Multiple server operation modes: 9 | - Standard I/O server 10 | - SSE (Server-Sent Events) server with plain configuration 11 | - SSE server using Ktor plugin 12 | - Built-in capabilities for: 13 | - Prompts management 14 | - Resources handling 15 | - Tools integration 16 | 17 | ## Getting Started 18 | 19 | ### Running the Server 20 | 21 | The server defaults [STDIO transport](https://modelcontextprotocol.io/specification/2025-06-18/basic/transports#stdio). 22 | 23 | You can customize the behavior using command-line arguments. 24 | Logs are printed to [./build/stdout.log](./build/stdout.log) 25 | 26 | #### Standard I/O mode (STDIO): 27 | 28 | ```bash 29 | ./gradlew clean build 30 | ``` 31 | Use the [MCP inspector](https://modelcontextprotocol.io/docs/tools/inspector) 32 | to connect to MCP via STDIO (Click the "▶️ Connect" button): 33 | 34 | ```shell 35 | npx @modelcontextprotocol/inspector --config mcp-inspector-config.json --server stdio-server 36 | ``` 37 | 38 | #### SSE with plain configuration: 39 | 40 | **NB!: 🐞 This configuration may not work ATM** 41 | 42 | ```bash 43 | ./gradlew run --args="--sse-server 3001" 44 | ``` 45 | or 46 | ```shell 47 | ./gradlew clean build 48 | java -jar ./build/libs/kotlin-mcp-server-0.1.0-all.jar --sse-server 3001 49 | ``` 50 | 51 | Use the [MCP inspector](https://modelcontextprotocol.io/docs/tools/inspector) 52 | to connect to `http://localhost:3002/` via SSE Transport (Click the "▶️ Connect" button): 53 | ```shell 54 | npx @modelcontextprotocol/inspector --config mcp-inspector-config.json --server sse-server 55 | ``` 56 | 57 | #### SSE with Ktor plugin: 58 | 59 | ```bash 60 | ./gradlew run --args="--sse-server-ktor 3002" 61 | ``` 62 | or 63 | ```shell 64 | ./gradlew clean build 65 | java -jar ./build/libs/kotlin-mcp-server-0.1.0-all.jar --sse-server-ktor 3002 66 | ``` 67 | 68 | Use the [MCP inspector](https://modelcontextprotocol.io/docs/tools/inspector) 69 | to connect to `http://localhost:3002/` via SSE transport (Click the "▶️ Connect" button): 70 | ```shell 71 | npx @modelcontextprotocol/inspector --config mcp-inspector-config.json --server sse-ktor-server 72 | ``` 73 | 74 | ## Server Capabilities 75 | 76 | - **Prompts**: Supports prompt management with list change notifications 77 | - **Resources**: Includes subscription support and list change notifications 78 | - **Tools**: Supports tool management with list change notifications 79 | 80 | ## Implementation Details 81 | 82 | The server is implemented using: 83 | - Ktor for HTTP server functionality (SSE modes) 84 | - Kotlin coroutines for asynchronous operations 85 | - SSE for real-time communication in web contexts 86 | - Standard I/O for command-line interface and process-based communication 87 | 88 | ## Example Capabilities 89 | 90 | The sample server demonstrates: 91 | - **Prompt**: "Kotlin Developer" - helps develop small Kotlin applications with a configurable project name 92 | - **Tool**: "kotlin-sdk-tool" - a simple test tool that returns a greeting 93 | - **Resource**: "Web Search" - a placeholder resource demonstrating resource handling 94 | -------------------------------------------------------------------------------- /conformance-test/src/test/kotlin/io/modelcontextprotocol/kotlin/sdk/conformance/ConformanceClient.kt: -------------------------------------------------------------------------------- 1 | package io.modelcontextprotocol.kotlin.sdk.conformance 2 | 3 | import io.github.oshai.kotlinlogging.KotlinLogging 4 | import io.ktor.client.HttpClient 5 | import io.ktor.client.engine.cio.CIO 6 | import io.ktor.client.plugins.sse.SSE 7 | import io.modelcontextprotocol.kotlin.sdk.client.Client 8 | import io.modelcontextprotocol.kotlin.sdk.client.StreamableHttpClientTransport 9 | import io.modelcontextprotocol.kotlin.sdk.shared.Transport 10 | import io.modelcontextprotocol.kotlin.sdk.types.CallToolRequest 11 | import io.modelcontextprotocol.kotlin.sdk.types.CallToolRequestParams 12 | import io.modelcontextprotocol.kotlin.sdk.types.Implementation 13 | import kotlinx.coroutines.runBlocking 14 | import kotlinx.serialization.json.JsonPrimitive 15 | import kotlinx.serialization.json.buildJsonObject 16 | 17 | private val logger = KotlinLogging.logger {} 18 | 19 | fun main(args: Array) { 20 | require(args.isNotEmpty()) { 21 | "Server URL must be provided as an argument" 22 | } 23 | 24 | val serverUrl = args.last() 25 | logger.info { "Connecting to test server at: $serverUrl" } 26 | 27 | val httpClient = HttpClient(CIO) { 28 | install(SSE) 29 | } 30 | val transport: Transport = StreamableHttpClientTransport(httpClient, serverUrl) 31 | 32 | val client = Client( 33 | clientInfo = Implementation( 34 | name = "kotlin-conformance-client", 35 | version = "1.0.0", 36 | ), 37 | ) 38 | 39 | var exitCode = 0 40 | 41 | runBlocking { 42 | try { 43 | client.connect(transport) 44 | logger.info { "✅ Connected to server successfully" } 45 | 46 | try { 47 | val tools = client.listTools() 48 | logger.info { "Available tools: ${tools.tools.map { it.name }}" } 49 | 50 | if (tools.tools.isNotEmpty()) { 51 | val toolName = tools.tools.first().name 52 | logger.info { "Calling tool: $toolName" } 53 | 54 | val result = client.callTool( 55 | CallToolRequest( 56 | params = CallToolRequestParams( 57 | name = toolName, 58 | arguments = buildJsonObject { 59 | put("input", JsonPrimitive("test")) 60 | }, 61 | ), 62 | ), 63 | ) 64 | logger.info { "Tool result: ${result.content}" } 65 | } 66 | } catch (e: Exception) { 67 | logger.debug(e) { "Error during tool operations (may be expected for some scenarios)" } 68 | } 69 | 70 | logger.info { "✅ Client operations completed successfully" } 71 | } catch (e: Exception) { 72 | logger.error(e) { "❌ Client failed" } 73 | exitCode = 1 74 | } finally { 75 | try { 76 | transport.close() 77 | } catch (e: Exception) { 78 | logger.warn(e) { "Error closing transport" } 79 | } 80 | httpClient.close() 81 | } 82 | } 83 | 84 | kotlin.system.exitProcess(exitCode) 85 | } 86 | -------------------------------------------------------------------------------- /kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/OldSchemaServerInstructionsTest.kt: -------------------------------------------------------------------------------- 1 | package io.modelcontextprotocol.kotlin.sdk.server 2 | 3 | import io.modelcontextprotocol.kotlin.sdk.Implementation 4 | import io.modelcontextprotocol.kotlin.sdk.ServerCapabilities 5 | import io.modelcontextprotocol.kotlin.sdk.client.Client 6 | import io.modelcontextprotocol.kotlin.sdk.shared.InMemoryTransport 7 | import kotlinx.coroutines.test.runTest 8 | import org.junit.jupiter.api.Test 9 | import org.junit.jupiter.api.assertNull 10 | import kotlin.test.assertEquals 11 | 12 | class OldSchemaServerInstructionsTest { 13 | 14 | @Test 15 | fun `Server constructor should accept instructions provider parameter`() = runTest { 16 | val serverInfo = Implementation(name = "test server", version = "1.0") 17 | val serverOptions = ServerOptions(capabilities = ServerCapabilities()) 18 | val instructions = "This is a test server. Use it for testing purposes only." 19 | 20 | val server = Server(serverInfo, serverOptions, { instructions }) 21 | 22 | // The instructions should be stored internally and used in handleInitialize 23 | // We can't directly access the private field, but we can test it through initialization 24 | val (clientTransport, serverTransport) = InMemoryTransport.createLinkedPair() 25 | val client = Client(clientInfo = Implementation(name = "test client", version = "1.0")) 26 | 27 | server.createSession(serverTransport) 28 | client.connect(clientTransport) 29 | 30 | assertEquals(instructions, client.serverInstructions) 31 | } 32 | 33 | @Test 34 | fun `Server constructor should accept instructions parameter`() = runTest { 35 | val serverInfo = Implementation(name = "test server", version = "1.0") 36 | val serverOptions = ServerOptions(capabilities = ServerCapabilities()) 37 | val instructions = "This is a test server. Use it for testing purposes only." 38 | 39 | val server = Server(serverInfo, serverOptions, instructions) 40 | 41 | // The instructions should be stored internally and used in handleInitialize 42 | // We can't directly access the private field, but we can test it through initialization 43 | val (clientTransport, serverTransport) = InMemoryTransport.createLinkedPair() 44 | val client = Client(clientInfo = Implementation(name = "test client", version = "1.0")) 45 | 46 | server.createSession(serverTransport) 47 | client.connect(clientTransport) 48 | 49 | assertEquals(instructions, client.serverInstructions) 50 | } 51 | 52 | @Test 53 | fun `Server constructor should work without instructions parameter`() = runTest { 54 | val serverInfo = Implementation(name = "test server", version = "1.0") 55 | val serverOptions = ServerOptions(capabilities = ServerCapabilities()) 56 | 57 | // Test that server works when instructions parameter is omitted (defaults to null) 58 | val server = Server(serverInfo, serverOptions) 59 | 60 | val (clientTransport, serverTransport) = InMemoryTransport.createLinkedPair() 61 | val client = Client(clientInfo = Implementation(name = "test client", version = "1.0")) 62 | 63 | server.createSession(serverTransport) 64 | client.connect(clientTransport) 65 | 66 | assertNull(client.serverInstructions) 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /samples/weather-stdio-server/src/main/kotlin/io/modelcontextprotocol/sample/server/WeatherApi.kt: -------------------------------------------------------------------------------- 1 | package io.modelcontextprotocol.sample.server 2 | 3 | import io.ktor.client.HttpClient 4 | import io.ktor.client.call.body 5 | import io.ktor.client.request.get 6 | import kotlinx.serialization.Serializable 7 | import kotlinx.serialization.json.JsonObject 8 | 9 | // Extension function to fetch forecast information for given latitude and longitude 10 | suspend fun HttpClient.getForecast(latitude: Double, longitude: Double): List { 11 | // Build the URI using provided latitude and longitude 12 | val uri = "/points/$latitude,$longitude" 13 | // Request the points data from the API 14 | val points = this.get(uri).body() 15 | 16 | // Request the forecast using the URL provided in the points response 17 | val forecast = this.get(points.properties.forecast).body() 18 | 19 | // Map each forecast period to a formatted string 20 | return forecast.properties.periods.map { period -> 21 | """ 22 | ${period.name}: 23 | Temperature: ${period.temperature} ${period.temperatureUnit} 24 | Wind: ${period.windSpeed} ${period.windDirection} 25 | Forecast: ${period.detailedForecast} 26 | """.trimIndent() 27 | } 28 | } 29 | 30 | // Extension function to fetch weather alerts for a given state 31 | suspend fun HttpClient.getAlerts(state: String): List { 32 | // Build the URI using the given state code 33 | val uri = "/alerts/active/area/$state" 34 | // Request the alerts data from the API 35 | val alerts = this.get(uri).body() 36 | 37 | // Map each alert feature to a formatted string 38 | return alerts.features.map { feature -> 39 | """ 40 | Event: ${feature.properties.event} 41 | Area: ${feature.properties.areaDesc} 42 | Severity: ${feature.properties.severity} 43 | Description: ${feature.properties.description} 44 | Instruction: ${feature.properties.instruction} 45 | """.trimIndent() 46 | } 47 | } 48 | 49 | // Data class representing the points response from the API 50 | @Serializable 51 | data class Points( 52 | val properties: Properties 53 | ) { 54 | @Serializable 55 | data class Properties(val forecast: String) 56 | } 57 | 58 | // Data class representing the forecast response from the API 59 | @Serializable 60 | data class Forecast( 61 | val properties: Properties 62 | ) { 63 | @Serializable 64 | data class Properties(val periods: List) 65 | 66 | @Serializable 67 | data class Period( 68 | val number: Int, val name: String, val startTime: String, val endTime: String, 69 | val isDaytime: Boolean, val temperature: Int, val temperatureUnit: String, 70 | val temperatureTrend: String, val probabilityOfPrecipitation: JsonObject, 71 | val windSpeed: String, val windDirection: String, 72 | val shortForecast: String, val detailedForecast: String, 73 | ) 74 | } 75 | 76 | // Data class representing the alerts response from the API 77 | @Serializable 78 | data class Alert( 79 | val features: List 80 | ) { 81 | @Serializable 82 | data class Feature( 83 | val properties: Properties 84 | ) 85 | 86 | @Serializable 87 | data class Properties( 88 | val event: String, val areaDesc: String, val severity: String, 89 | val description: String, val instruction: String?, 90 | ) 91 | } 92 | -------------------------------------------------------------------------------- /kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/AbstractTsClientKotlinServerTest.kt: -------------------------------------------------------------------------------- 1 | package io.modelcontextprotocol.kotlin.sdk.integration.typescript 2 | 3 | import kotlinx.coroutines.test.runTest 4 | import org.junit.jupiter.api.Test 5 | import org.junit.jupiter.api.Timeout 6 | import java.util.concurrent.TimeUnit 7 | import kotlin.test.assertTrue 8 | 9 | abstract class AbstractTsClientKotlinServerTest : TsTestBase() { 10 | 11 | protected open fun beforeServer() {} 12 | protected open fun afterServer() {} 13 | 14 | /** 15 | * Run the TypeScript client against the prepared server and return its console output. 16 | */ 17 | protected abstract fun runClient(vararg args: String): String 18 | 19 | @Test 20 | @Timeout(30, unit = TimeUnit.SECONDS) 21 | fun toolCall() = runTest { 22 | beforeServer() 23 | try { 24 | val testName = "TestUser" 25 | val out = runClient("greet", testName) 26 | assertTrue(out.contains("Text content:"), "Output should contain the text content section.\n$out") 27 | assertTrue(out.contains("Hello, $testName!"), "Tool response should contain the greeting.\n$out") 28 | assertTrue( 29 | out.contains("Structured content:"), 30 | "Output should contain the structured content section.\n$out", 31 | ) 32 | assertTrue( 33 | out.contains( 34 | "\"greeting\": \"Hello, $testName!\"", 35 | ) || 36 | out.contains("greeting") || 37 | out.contains("greet"), 38 | "Structured content should contain the greeting.\n$out", 39 | ) 40 | } finally { 41 | afterServer() 42 | } 43 | } 44 | 45 | @Test 46 | @Timeout(60, unit = TimeUnit.SECONDS) 47 | fun notifications() = runTest { 48 | beforeServer() 49 | try { 50 | val name = "NotifUser" 51 | val out = runClient("multi-greet", name) 52 | assertTrue( 53 | out.contains("Multiple greetings") || out.contains("greeting"), 54 | "Tool response should contain greeting message.\n$out", 55 | ) 56 | assertTrue( 57 | out.contains("\"notificationCount\": 3") || out.contains("notificationCount: 3"), 58 | "Structured content should indicate that 3 notifications were emitted by the server.\nOutput:\n$out", 59 | ) 60 | } finally { 61 | afterServer() 62 | } 63 | } 64 | 65 | @Test 66 | @Timeout(120, unit = TimeUnit.SECONDS) 67 | fun multipleClientSequence() = runTest { 68 | beforeServer() 69 | try { 70 | val out1 = runClient("greet", "FirstClient") 71 | assertTrue(out1.contains("Hello, FirstClient!"), "Should greet first client.\n$out1") 72 | 73 | val out2 = runClient("multi-greet", "SecondClient") 74 | assertTrue( 75 | out2.contains("Multiple greetings") || out2.contains("greeting"), 76 | "Should respond for second client.\n$out2", 77 | ) 78 | 79 | val out3 = runClient() 80 | assertTrue(out3.contains("Available utils:"), "Should list available utils.\n$out3") 81 | assertTrue(out3.contains("greet"), "Greet tool should be available.\n$out3") 82 | assertTrue(out3.contains("multi-greet"), "Multi-greet tool should be available.\n$out3") 83 | } finally { 84 | afterServer() 85 | } 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /kotlin-sdk-core/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/types/logging.kt: -------------------------------------------------------------------------------- 1 | package io.modelcontextprotocol.kotlin.sdk.types 2 | 3 | import kotlinx.serialization.EncodeDefault 4 | import kotlinx.serialization.ExperimentalSerializationApi 5 | import kotlinx.serialization.SerialName 6 | import kotlinx.serialization.Serializable 7 | 8 | /** 9 | * The severity of a log message. 10 | * 11 | * These levels map to syslog message severities, as specified in 12 | * [RFC-5424](https://datatracker.ietf.org/doc/html/rfc5424#section-6.2.1). 13 | * 14 | * Levels are ordered from least to most severe: 15 | * [Debug] < [Info] < [Notice] < [Warning] < [Error] < [Critical] < [Alert] < [Emergency] 16 | */ 17 | @Serializable 18 | public enum class LoggingLevel { 19 | /** Detailed debug information for troubleshooting (RFC-5424: Debug) */ 20 | @SerialName("debug") 21 | Debug, 22 | 23 | /** Informational messages about normal operations (RFC-5424: Informational) */ 24 | @SerialName("info") 25 | Info, 26 | 27 | /** Normal but significant conditions (RFC-5424: Notice) */ 28 | @SerialName("notice") 29 | Notice, 30 | 31 | /** Warning conditions that may require attention (RFC-5424: Warning) */ 32 | @SerialName("warning") 33 | Warning, 34 | 35 | /** Error conditions that require attention (RFC-5424: Error) */ 36 | @SerialName("error") 37 | Error, 38 | 39 | /** Critical conditions requiring immediate action (RFC-5424: Critical) */ 40 | @SerialName("critical") 41 | Critical, 42 | 43 | /** Action must be taken immediately (RFC-5424: Alert) */ 44 | @SerialName("alert") 45 | Alert, 46 | 47 | /** System is unusable, highest severity (RFC-5424: Emergency) */ 48 | @SerialName("emergency") 49 | Emergency, 50 | } 51 | 52 | /** 53 | * A request from the client to the server to enable or adjust logging. 54 | * 55 | * After receiving this request, the server should send log messages at the specified 56 | * [level][SetLevelRequestParams.level] and higher (more severe) to the client as 57 | * notifications/message events. 58 | * 59 | * @property params The parameters specifying the desired logging level. 60 | */ 61 | @Serializable 62 | public data class SetLevelRequest(override val params: SetLevelRequestParams) : ClientRequest { 63 | @OptIn(ExperimentalSerializationApi::class) 64 | @EncodeDefault 65 | override val method: Method = Method.Defined.LoggingSetLevel 66 | 67 | /** 68 | * The minimum severity level of logging that the client wants to receive from the server. 69 | */ 70 | public val level: LoggingLevel 71 | get() = params.level 72 | 73 | /** 74 | * Metadata for this request. May include a progressToken for out-of-band progress notifications. 75 | */ 76 | public val meta: RequestMeta? 77 | get() = params.meta 78 | } 79 | 80 | /** 81 | * Parameters for a logging/setLevel request. 82 | * 83 | * @property level The minimum severity level of logging that the client wants to receive 84 | * from the server. The server should send all logs at this level and higher 85 | * (i.e., more severe) to the client as notifications/message. 86 | * For example, if [level] is [LoggingLevel.Warning], the server should send 87 | * warning, error, critical, alert, and emergency messages, but not info, notice, or debug. 88 | * @property meta Optional metadata for this request. May include a progressToken for 89 | * out-of-band progress notifications. 90 | */ 91 | @Serializable 92 | public data class SetLevelRequestParams( 93 | val level: LoggingLevel, 94 | @SerialName("_meta") 95 | override val meta: RequestMeta? = null, 96 | ) : RequestParams 97 | -------------------------------------------------------------------------------- /kotlin-sdk-test/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/OldSchemaInMemoryTransportTest.kt: -------------------------------------------------------------------------------- 1 | package io.modelcontextprotocol.kotlin.sdk.integration 2 | 3 | import io.modelcontextprotocol.kotlin.sdk.InitializedNotification 4 | import io.modelcontextprotocol.kotlin.sdk.JSONRPCMessage 5 | import io.modelcontextprotocol.kotlin.sdk.shared.InMemoryTransport 6 | import io.modelcontextprotocol.kotlin.sdk.toJSON 7 | import kotlinx.coroutines.test.runTest 8 | import kotlin.test.BeforeTest 9 | import kotlin.test.Test 10 | import kotlin.test.assertEquals 11 | import kotlin.test.assertFailsWith 12 | import kotlin.test.assertNotNull 13 | import kotlin.test.assertTrue 14 | 15 | class OldSchemaInMemoryTransportTest { 16 | private lateinit var clientTransport: InMemoryTransport 17 | private lateinit var serverTransport: InMemoryTransport 18 | 19 | @BeforeTest 20 | fun setUp() { 21 | val (client, server) = InMemoryTransport.createLinkedPair() 22 | clientTransport = client 23 | serverTransport = server 24 | } 25 | 26 | @Test 27 | fun `should create linked pair`() { 28 | assertNotNull(clientTransport) 29 | assertNotNull(serverTransport) 30 | } 31 | 32 | @Test 33 | fun `should start without error`() = runTest { 34 | clientTransport.start() 35 | serverTransport.start() 36 | // If no exception is thrown, the test passes 37 | } 38 | 39 | @Test 40 | fun `should send message from client to server`() = runTest { 41 | val message = InitializedNotification() 42 | 43 | var receivedMessage: JSONRPCMessage? = null 44 | serverTransport.onMessage { msg -> 45 | receivedMessage = msg 46 | } 47 | 48 | val rpcNotification = message.toJSON() 49 | clientTransport.send(rpcNotification) 50 | assertEquals(rpcNotification, receivedMessage) 51 | } 52 | 53 | @Test 54 | fun `should send message from server to client`() = runTest { 55 | val message = InitializedNotification() 56 | .toJSON() 57 | 58 | var receivedMessage: JSONRPCMessage? = null 59 | clientTransport.onMessage { msg -> 60 | receivedMessage = msg 61 | } 62 | 63 | serverTransport.send(message) 64 | assertEquals(message, receivedMessage) 65 | } 66 | 67 | @Test 68 | fun `should handle close`() = runTest { 69 | var clientClosed = false 70 | var serverClosed = false 71 | 72 | clientTransport.onClose { 73 | clientClosed = true 74 | } 75 | 76 | serverTransport.onClose { 77 | serverClosed = true 78 | } 79 | 80 | clientTransport.close() 81 | assertTrue(clientClosed) 82 | assertTrue(serverClosed) 83 | } 84 | 85 | @Test 86 | fun `should throw error when sending after close`() = runTest { 87 | clientTransport.close() 88 | 89 | assertFailsWith { 90 | clientTransport.send( 91 | InitializedNotification().toJSON(), 92 | ) 93 | } 94 | } 95 | 96 | @Test 97 | fun `should queue messages sent before start`() = runTest { 98 | val message = InitializedNotification() 99 | .toJSON() 100 | 101 | var receivedMessage: JSONRPCMessage? = null 102 | serverTransport.onMessage { msg -> 103 | receivedMessage = msg 104 | } 105 | 106 | clientTransport.send(message) 107 | serverTransport.start() 108 | assertEquals(message, receivedMessage) 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/OldSchemaServerToolsTest.kt: -------------------------------------------------------------------------------- 1 | package io.modelcontextprotocol.kotlin.sdk.server 2 | 3 | import io.modelcontextprotocol.kotlin.sdk.CallToolResult 4 | import io.modelcontextprotocol.kotlin.sdk.Implementation 5 | import io.modelcontextprotocol.kotlin.sdk.Method 6 | import io.modelcontextprotocol.kotlin.sdk.ServerCapabilities 7 | import io.modelcontextprotocol.kotlin.sdk.TextContent 8 | import io.modelcontextprotocol.kotlin.sdk.Tool 9 | import kotlinx.coroutines.CompletableDeferred 10 | import kotlinx.coroutines.test.runTest 11 | import org.junit.jupiter.api.Test 12 | import org.junit.jupiter.api.assertThrows 13 | import kotlin.test.assertEquals 14 | import kotlin.test.assertFalse 15 | import kotlin.test.assertTrue 16 | 17 | class OldSchemaServerToolsTest : OldSchemaAbstractServerFeaturesTest() { 18 | 19 | override fun getServerCapabilities(): io.modelcontextprotocol.kotlin.sdk.types.ServerCapabilities = 20 | ServerCapabilities( 21 | tools = ServerCapabilities.Tools(null), 22 | ) 23 | 24 | @Test 25 | fun `removeTool should remove a tool`() = runTest { 26 | // Add a tool 27 | server.addTool("test-tool", "Test Tool", Tool.Input()) { 28 | CallToolResult(listOf(TextContent("Test result"))) 29 | } 30 | 31 | // Remove the tool 32 | val result = server.removeTool("test-tool") 33 | 34 | // Verify the tool was removed 35 | assertTrue(result, "Tool should be removed successfully") 36 | } 37 | 38 | @Test 39 | fun `removeTool should return false when tool does not exist`() = runTest { 40 | // Track notifications 41 | var toolListChangedNotificationReceived = false 42 | client.setNotificationHandler( 43 | Method.Defined.NotificationsToolsListChanged, 44 | ) { 45 | toolListChangedNotificationReceived = true 46 | CompletableDeferred(Unit) 47 | } 48 | 49 | // Try to remove a non-existent tool 50 | val result = server.removeTool("non-existent-tool") 51 | 52 | // Verify the result 53 | assertFalse(result, "Removing non-existent tool should return false") 54 | assertFalse(toolListChangedNotificationReceived, "No notification should be sent when tool doesn't exist") 55 | } 56 | 57 | @Test 58 | fun `removeTool should throw when tools capability is not supported`() = runTest { 59 | // Create server without tools capability 60 | val serverOptions = ServerOptions( 61 | capabilities = ServerCapabilities(), 62 | ) 63 | val server = Server( 64 | Implementation(name = "test server", version = "1.0"), 65 | serverOptions, 66 | ) 67 | 68 | // Verify that removing a tool throws an exception 69 | val exception = assertThrows { 70 | server.removeTool("test-tool") 71 | } 72 | assertEquals("Server does not support tools capability.", exception.message) 73 | } 74 | 75 | @Test 76 | fun `removeTools should remove multiple tools`() = runTest { 77 | // Add tools 78 | server.addTool("test-tool-1", "Test Tool 1") { 79 | CallToolResult(listOf(TextContent("Test result 1"))) 80 | } 81 | server.addTool("test-tool-2", "Test Tool 2") { 82 | CallToolResult(listOf(TextContent("Test result 2"))) 83 | } 84 | 85 | // Remove the tools 86 | val result = server.removeTools(listOf("test-tool-1", "test-tool-2")) 87 | 88 | // Verify the tools were removed 89 | assertEquals(2, result, "Both tools should be removed") 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /docs/moduledoc.md: -------------------------------------------------------------------------------- 1 | # MCP Kotlin SDK 2 | 3 | Kotlin SDK for the Model Context Protocol (MCP). 4 | This is a Kotlin Multiplatform library that helps you build MCP clients and servers that speak the same protocol and 5 | share the same types. 6 | The SDK focuses on clarity, small building blocks, and first‑class coroutine support. 7 | 8 | Use the umbrella `kotlin-sdk` artifact when you want a single dependency that brings the core types plus both client and 9 | server toolkits. If you only need one side, depend on `kotlin-sdk-client` or `kotlin-sdk-server` directly. 10 | 11 | Gradle (Kotlin DSL): 12 | 13 | ```kotlin 14 | dependencies { 15 | // Convenience bundle with everything you need to start 16 | implementation("io.modelcontextprotocol:kotlin-sdk:") 17 | 18 | // Or pick modules explicitly 19 | implementation("io.modelcontextprotocol:kotlin-sdk-client:") 20 | implementation("io.modelcontextprotocol:kotlin-sdk-server:") 21 | } 22 | ``` 23 | 24 | --- 25 | 26 | ## Module kotlin-sdk-core 27 | 28 | Foundational, platform‑agnostic pieces: 29 | 30 | - Protocol data model and JSON serialization (kotlinx.serialization) 31 | - Request/response and notification types used by both sides of MCP 32 | - Coroutine‑friendly protocol engine and utilities 33 | - Transport abstractions shared by client and server 34 | 35 | You typically do not use `core` directly in application code; it is pulled in by the client/server modules. Use it 36 | explicitly if you only need the protocol types or plan to implement a custom transport. 37 | 38 | --- 39 | 40 | ## Module kotlin-sdk-client 41 | 42 | High‑level client API for connecting to an MCP server and invoking its tools, prompts, and resources. Ships with several 43 | transports: 44 | 45 | - WebSocketClientTransport – low latency, full‑duplex 46 | - SSEClientTransport – Server‑Sent Events over HTTP 47 | - StdioClientTransport – CLI‑friendly stdio bridge 48 | - StreamableHttpClientTransport – simple HTTP streaming 49 | 50 | A minimal client: 51 | 52 | ```kotlin 53 | val client = Client( 54 | clientInfo = Implementation(name = "sample-client", version = "1.0.0") 55 | ) 56 | 57 | client.connect(WebSocketClientTransport("ws://localhost:8080/mcp")) 58 | 59 | val tools = client.listTools() 60 | val result = client.callTool( 61 | name = "echo", 62 | arguments = mapOf("text" to "Hello, MCP!") 63 | ) 64 | ``` 65 | 66 | --- 67 | 68 | ## Module kotlin-sdk-server 69 | 70 | Lightweight server toolkit for hosting MCP tools, prompts, and resources. It provides a small, composable API and 71 | ready‑to‑use transports: 72 | 73 | - StdioServerTransport – integrates well with CLIs and editors 74 | - SSE/WebSocket helpers for Ktor – easy HTTP deployment 75 | 76 | Register tools and run over stdio: 77 | 78 | ```kotlin 79 | 80 | val server = Server( 81 | serverInfo = Implementation(name = "sample-server", version = "1.0.0"), 82 | options = ServerOptions(ServerCapabilities()) 83 | ) 84 | 85 | server.addTool( 86 | name = "echo", 87 | description = "Echoes the provided text" 88 | ) { request -> 89 | // Build and return a CallToolResult from request.arguments 90 | // (see CallToolResult and related types in kotlin-sdk-core) 91 | /* ... */ 92 | } 93 | 94 | // Bridge the protocol over stdio 95 | val transport = StdioServerTransport( 96 | inputStream = kotlinx.io.files.Path("/dev/stdin").source(), 97 | outputStream = kotlinx.io.files.Path("/dev/stdout").sink() 98 | ) 99 | // Start transport and wire it with the server using provided helpers in the SDK. 100 | ``` 101 | 102 | For HTTP deployments, use the Ktor extensions included in the module to expose an MCP WebSocket or SSE endpoint with a 103 | few lines of code. 104 | -------------------------------------------------------------------------------- /kotlin-sdk-client/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/OldSchemaMockTransport.kt: -------------------------------------------------------------------------------- 1 | package io.modelcontextprotocol.kotlin.sdk.client 2 | 3 | import io.modelcontextprotocol.kotlin.sdk.CallToolResult 4 | import io.modelcontextprotocol.kotlin.sdk.Implementation 5 | import io.modelcontextprotocol.kotlin.sdk.InitializeResult 6 | import io.modelcontextprotocol.kotlin.sdk.JSONRPCMessage 7 | import io.modelcontextprotocol.kotlin.sdk.JSONRPCRequest 8 | import io.modelcontextprotocol.kotlin.sdk.JSONRPCResponse 9 | import io.modelcontextprotocol.kotlin.sdk.ServerCapabilities 10 | import io.modelcontextprotocol.kotlin.sdk.shared.Transport 11 | import io.modelcontextprotocol.kotlin.sdk.shared.TransportSendOptions 12 | import kotlinx.coroutines.sync.Mutex 13 | import kotlinx.coroutines.sync.withLock 14 | 15 | class OldSchemaMockTransport : Transport { 16 | private val _sentMessages = mutableListOf() 17 | private val _receivedMessages = mutableListOf() 18 | private val mutex = Mutex() 19 | 20 | suspend fun getSentMessages() = mutex.withLock { _sentMessages.toList() } 21 | suspend fun getReceivedMessages() = mutex.withLock { _receivedMessages.toList() } 22 | 23 | private var onMessageBlock: (suspend (JSONRPCMessage) -> Unit)? = null 24 | private var onCloseBlock: (() -> Unit)? = null 25 | private var onErrorBlock: ((Throwable) -> Unit)? = null 26 | 27 | override suspend fun start() = Unit 28 | 29 | override suspend fun send(message: JSONRPCMessage, options: TransportSendOptions?) { 30 | mutex.withLock { 31 | _sentMessages += message 32 | } 33 | 34 | // Auto-respond to initialization and tool calls 35 | when (message) { 36 | is JSONRPCRequest -> { 37 | when (message.method) { 38 | "initialize" -> { 39 | val initResponse = JSONRPCResponse( 40 | id = message.id, 41 | result = InitializeResult( 42 | protocolVersion = "2024-11-05", 43 | capabilities = ServerCapabilities( 44 | tools = ServerCapabilities.Tools(listChanged = null), 45 | ), 46 | serverInfo = Implementation("mock-server", "1.0.0"), 47 | ), 48 | ) 49 | onMessageBlock?.invoke(initResponse) 50 | } 51 | 52 | "tools/call" -> { 53 | val toolResponse = JSONRPCResponse( 54 | id = message.id, 55 | result = CallToolResult( 56 | content = listOf(), 57 | isError = false, 58 | ), 59 | ) 60 | onMessageBlock?.invoke(toolResponse) 61 | } 62 | } 63 | } 64 | 65 | else -> { 66 | // Handle other message types if needed 67 | } 68 | } 69 | } 70 | 71 | override suspend fun close() { 72 | onCloseBlock?.invoke() 73 | } 74 | 75 | override fun onMessage(block: suspend (JSONRPCMessage) -> Unit) { 76 | onMessageBlock = { message -> 77 | mutex.withLock { 78 | _receivedMessages += message 79 | } 80 | block(message) 81 | } 82 | } 83 | 84 | override fun onClose(block: () -> Unit) { 85 | onCloseBlock = block 86 | } 87 | 88 | override fun onError(block: (Throwable) -> Unit) { 89 | onErrorBlock = block 90 | } 91 | 92 | fun setupInitializationResponse() { 93 | // This method helps set up the mock for proper initialization 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /samples/kotlin-mcp-server/src/test/kotlin/SseServerIntegrationTest.kt: -------------------------------------------------------------------------------- 1 | import io.modelcontextprotocol.kotlin.sdk.client.Client 2 | import io.modelcontextprotocol.kotlin.sdk.types.ReadResourceRequest 3 | import io.modelcontextprotocol.kotlin.sdk.types.ReadResourceRequestParams 4 | import io.modelcontextprotocol.kotlin.sdk.types.TextContent 5 | import io.modelcontextprotocol.kotlin.sdk.types.TextResourceContents 6 | import kotlinx.coroutines.runBlocking 7 | import kotlin.test.Test 8 | import kotlin.test.assertEquals 9 | import kotlin.test.assertIs 10 | import kotlin.test.assertNotNull 11 | import kotlin.test.assertNull 12 | import kotlin.test.assertTrue 13 | 14 | abstract class SseServerIntegrationTestBase { 15 | 16 | abstract val client: Client 17 | 18 | @Test 19 | fun `should list tools`(): Unit = runBlocking { 20 | // when 21 | val listToolsResult = client.listTools() 22 | 23 | // then 24 | assertNull(listToolsResult.meta) 25 | 26 | val tools = listToolsResult.tools 27 | assertEquals(actual = tools.size, expected = 1) 28 | assertEquals(expected = listOf("kotlin-sdk-tool"), actual = tools.map { it.name }) 29 | } 30 | 31 | @Test 32 | fun `should list prompts`(): Unit = runBlocking { 33 | // when 34 | val listPromptsResult = client.listPrompts() 35 | 36 | // then 37 | assertNull(listPromptsResult.meta) 38 | 39 | val prompts = listPromptsResult.prompts 40 | 41 | assertEquals(expected = listOf("Kotlin Developer"), actual = prompts.map { it.name }) 42 | } 43 | 44 | @Test 45 | fun `should list resources`(): Unit = runBlocking { 46 | val listResourcesResult = client.listResources() 47 | 48 | // then 49 | assertNull(listResourcesResult.meta) 50 | val resources = listResourcesResult.resources 51 | 52 | assertEquals(expected = listOf("Web Search"), actual = resources.map { it.name }) 53 | } 54 | 55 | @Test 56 | fun `should get resource`(): Unit = runBlocking { 57 | val testResourceUri = "https://search.com/" 58 | val getResourcesResult = client.readResource( 59 | ReadResourceRequest(ReadResourceRequestParams(uri = testResourceUri)), 60 | ) 61 | 62 | // then 63 | assertEquals(expected = null, actual = getResourcesResult.meta) 64 | val contents = getResourcesResult.contents 65 | assertEquals(expected = 1, actual = contents.size) 66 | assertTrue { 67 | contents.contains( 68 | TextResourceContents("Placeholder content for $testResourceUri", testResourceUri, "text/html"), 69 | ) 70 | } 71 | } 72 | 73 | @Test 74 | fun `should call tool`(): Unit = runBlocking { 75 | // when 76 | val toolResult = client.callTool( 77 | name = "kotlin-sdk-tool", 78 | arguments = emptyMap(), 79 | ) 80 | 81 | // then 82 | assertNotNull(toolResult) 83 | assertNull(toolResult.meta) 84 | val content = toolResult.content.single() 85 | assertIs(content, "Tool result should be a text content") 86 | 87 | assertEquals(expected = "Hello, world!", actual = content.text) 88 | assertEquals(expected = "text", actual = "${content.type}".lowercase()) 89 | } 90 | } 91 | 92 | class SseServerKtorPluginIntegrationTest : SseServerIntegrationTestBase() { 93 | private val testEnvironment = TestEnvironment(McpServerType.KTOR_PLUGIN) 94 | override val client: Client = testEnvironment.client 95 | } 96 | 97 | class SseServerPlainConfigurationIntegrationTest : SseServerIntegrationTestBase() { 98 | private val testEnvironment = TestEnvironment(McpServerType.PLAIN_CONFIGURATION) 99 | override val client: Client = testEnvironment.client 100 | } 101 | -------------------------------------------------------------------------------- /kotlin-sdk-client/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/MockTransport.kt: -------------------------------------------------------------------------------- 1 | package io.modelcontextprotocol.kotlin.sdk.client 2 | 3 | import io.modelcontextprotocol.kotlin.sdk.shared.Transport 4 | import io.modelcontextprotocol.kotlin.sdk.shared.TransportSendOptions 5 | import io.modelcontextprotocol.kotlin.sdk.types.CallToolResult 6 | import io.modelcontextprotocol.kotlin.sdk.types.Implementation 7 | import io.modelcontextprotocol.kotlin.sdk.types.InitializeResult 8 | import io.modelcontextprotocol.kotlin.sdk.types.JSONRPCMessage 9 | import io.modelcontextprotocol.kotlin.sdk.types.JSONRPCRequest 10 | import io.modelcontextprotocol.kotlin.sdk.types.JSONRPCResponse 11 | import io.modelcontextprotocol.kotlin.sdk.types.ServerCapabilities 12 | import kotlinx.coroutines.sync.Mutex 13 | import kotlinx.coroutines.sync.withLock 14 | 15 | class MockTransport : Transport { 16 | private val _sentMessages = mutableListOf() 17 | private val _receivedMessages = mutableListOf() 18 | private val mutex = Mutex() 19 | 20 | suspend fun getSentMessages() = mutex.withLock { _sentMessages.toList() } 21 | suspend fun getReceivedMessages() = mutex.withLock { _receivedMessages.toList() } 22 | 23 | private var onMessageBlock: (suspend (JSONRPCMessage) -> Unit)? = null 24 | private var onCloseBlock: (() -> Unit)? = null 25 | private var onErrorBlock: ((Throwable) -> Unit)? = null 26 | 27 | override suspend fun start() = Unit 28 | 29 | override suspend fun send(message: JSONRPCMessage, options: TransportSendOptions?) { 30 | mutex.withLock { 31 | _sentMessages += message 32 | } 33 | 34 | // Auto-respond to initialization and tool calls 35 | when (message) { 36 | is JSONRPCRequest -> { 37 | when (message.method) { 38 | "initialize" -> { 39 | val initResponse = JSONRPCResponse( 40 | id = message.id, 41 | result = InitializeResult( 42 | protocolVersion = "2024-11-05", 43 | capabilities = ServerCapabilities( 44 | tools = ServerCapabilities.Tools(listChanged = null), 45 | ), 46 | serverInfo = Implementation("mock-server", "1.0.0"), 47 | ), 48 | ) 49 | onMessageBlock?.invoke(initResponse) 50 | } 51 | 52 | "tools/call" -> { 53 | val toolResponse = JSONRPCResponse( 54 | id = message.id, 55 | result = CallToolResult( 56 | content = listOf(), 57 | isError = false, 58 | ), 59 | ) 60 | onMessageBlock?.invoke(toolResponse) 61 | } 62 | } 63 | } 64 | 65 | else -> { 66 | // Handle other message types if needed 67 | } 68 | } 69 | } 70 | 71 | override suspend fun close() { 72 | onCloseBlock?.invoke() 73 | } 74 | 75 | override fun onMessage(block: suspend (JSONRPCMessage) -> Unit) { 76 | onMessageBlock = { message -> 77 | mutex.withLock { 78 | _receivedMessages += message 79 | } 80 | block(message) 81 | } 82 | } 83 | 84 | override fun onClose(block: () -> Unit) { 85 | onCloseBlock = block 86 | } 87 | 88 | override fun onError(block: (Throwable) -> Unit) { 89 | onErrorBlock = block 90 | } 91 | 92 | fun setupInitializationResponse() { 93 | // This method helps set up the mock for proper initialization 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/AbstractKotlinClientTsServerTest.kt: -------------------------------------------------------------------------------- 1 | package io.modelcontextprotocol.kotlin.sdk.integration.typescript 2 | 3 | import io.modelcontextprotocol.kotlin.sdk.client.Client 4 | import io.modelcontextprotocol.kotlin.sdk.types.TextContent 5 | import kotlinx.coroutines.Dispatchers 6 | import kotlinx.coroutines.runBlocking 7 | import org.junit.jupiter.api.Test 8 | import org.junit.jupiter.api.Timeout 9 | import java.util.concurrent.TimeUnit 10 | import kotlin.test.assertEquals 11 | import kotlin.test.assertNotNull 12 | import kotlin.test.assertTrue 13 | 14 | abstract class AbstractKotlinClientTsServerTest : TsTestBase() { 15 | protected abstract suspend fun useClient(block: suspend (Client) -> T): T 16 | 17 | @Test 18 | @Timeout(30, unit = TimeUnit.SECONDS) 19 | fun connectsAndPings() = runBlocking(Dispatchers.IO) { 20 | useClient { client -> 21 | assertNotNull(client, "Client should be initialized") 22 | val ping = client.ping() 23 | assertNotNull(ping, "Ping result should not be null") 24 | val serverImpl = client.serverVersion 25 | assertNotNull(serverImpl, "Server implementation should not be null") 26 | println("Connected to TypeScript server: ${serverImpl.name} v${serverImpl.version}") 27 | } 28 | } 29 | 30 | @Test 31 | @Timeout(30, unit = TimeUnit.SECONDS) 32 | fun listsTools() = runBlocking(Dispatchers.IO) { 33 | useClient { client -> 34 | val result = client.listTools() 35 | assertNotNull(result, "Tools list should not be null") 36 | assertTrue(result.tools.isNotEmpty(), "Tools list should not be empty") 37 | val toolNames = result.tools.map { it.name } 38 | assertTrue("greet" in toolNames, "Greet tool should be available") 39 | assertTrue("multi-greet" in toolNames, "Multi-greet tool should be available") 40 | // Some tests also check collect-user-info; keep base minimal and non-breaking 41 | } 42 | } 43 | 44 | @Test 45 | @Timeout(30, unit = TimeUnit.SECONDS) 46 | fun callGreet() = runBlocking(Dispatchers.IO) { 47 | useClient { client -> 48 | val testName = "TestUser" 49 | val arguments = mapOf("name" to testName) 50 | val result = client.callTool("greet", arguments) 51 | assertNotNull(result, "Tool call result should not be null") 52 | val callResult = result 53 | val textContent = callResult.content.firstOrNull { it is TextContent } as? TextContent 54 | assertNotNull(textContent, "Text content should be present in the result") 55 | assertEquals("Hello, $testName!", textContent.text) 56 | } 57 | } 58 | 59 | @Test 60 | @Timeout(30, unit = TimeUnit.SECONDS) 61 | fun multipleClients() = runBlocking(Dispatchers.IO) { 62 | useClient { client1 -> 63 | useClient { client2 -> 64 | val tools1 = client1.listTools() 65 | val tools2 = client2.listTools() 66 | assertTrue(tools1.tools.isNotEmpty(), "Tools list for first client should not be empty") 67 | assertTrue(tools2.tools.isNotEmpty(), "Tools list for second client should not be empty") 68 | val toolNames1 = tools1.tools.map { it.name } 69 | val toolNames2 = tools2.tools.map { it.name } 70 | assertTrue("greet" in toolNames1, "Greet tool should be available to first client") 71 | assertTrue("multi-greet" in toolNames1, "Multi-greet tool should be available to first client") 72 | assertTrue("greet" in toolNames2, "Greet tool should be available to second client") 73 | assertTrue("multi-greet" in toolNames2, "Multi-greet tool should be available to second client") 74 | } 75 | } 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/server/ServerInstructionsTest.kt: -------------------------------------------------------------------------------- 1 | package io.modelcontextprotocol.kotlin.sdk.server 2 | 3 | import io.modelcontextprotocol.kotlin.sdk.ExperimentalMcpApi 4 | import io.modelcontextprotocol.kotlin.sdk.Implementation 5 | import io.modelcontextprotocol.kotlin.sdk.ServerCapabilities 6 | import io.modelcontextprotocol.kotlin.sdk.client.ClientOptions 7 | import io.modelcontextprotocol.kotlin.sdk.client.mcpClient 8 | import io.modelcontextprotocol.kotlin.sdk.shared.InMemoryTransport 9 | import io.modelcontextprotocol.kotlin.sdk.types.ClientCapabilities 10 | import kotlinx.coroutines.test.runTest 11 | import org.junit.jupiter.api.Test 12 | import org.junit.jupiter.api.assertNull 13 | import kotlin.test.assertEquals 14 | 15 | @OptIn(ExperimentalMcpApi::class) 16 | class ServerInstructionsTest { 17 | 18 | @Test 19 | fun `Server constructor should accept instructions provider parameter`() = runTest { 20 | val serverInfo = Implementation(name = "test server", version = "1.0") 21 | val serverOptions = ServerOptions(capabilities = ServerCapabilities()) 22 | val instructions = "This is a test server. Use it for testing purposes only." 23 | 24 | val server = Server(serverInfo, serverOptions, { instructions }) 25 | 26 | // The instructions should be stored internally and used in handleInitialize 27 | // We can't directly access the private field, but we can test it through initialization 28 | val (clientTransport, serverTransport) = InMemoryTransport.createLinkedPair() 29 | server.createSession(serverTransport) 30 | 31 | val client = mcpClient( 32 | clientInfo = Implementation(name = "test client", version = "1.0"), 33 | clientOptions = ClientOptions( 34 | capabilities = ClientCapabilities( 35 | roots = ClientCapabilities.Roots(listChanged = false), 36 | ), 37 | ), 38 | transport = clientTransport, 39 | ) 40 | 41 | assertEquals(instructions, client.serverInstructions) 42 | } 43 | 44 | @Test 45 | fun `Server constructor should accept instructions parameter`() = runTest { 46 | val serverInfo = Implementation(name = "test server", version = "1.0") 47 | val serverOptions = ServerOptions(capabilities = ServerCapabilities()) 48 | val instructions = "This is a test server. Use it for testing purposes only." 49 | 50 | val server = Server(serverInfo, serverOptions, instructions) 51 | 52 | // The instructions should be stored internally and used in handleInitialize 53 | // We can't directly access the private field, but we can test it through initialization 54 | val (clientTransport, serverTransport) = InMemoryTransport.createLinkedPair() 55 | server.createSession(serverTransport) 56 | 57 | val client = mcpClient( 58 | clientInfo = Implementation(name = "test client", version = "1.0"), 59 | transport = clientTransport, 60 | ) 61 | 62 | assertEquals(instructions, client.serverInstructions) 63 | } 64 | 65 | @Test 66 | fun `Server constructor should work without instructions parameter`() = runTest { 67 | val serverInfo = Implementation(name = "test server", version = "1.0") 68 | val serverOptions = ServerOptions(capabilities = ServerCapabilities()) 69 | 70 | // Test that server works when instructions parameter is omitted (defaults to null) 71 | val server = Server(serverInfo, serverOptions) 72 | 73 | val (clientTransport, serverTransport) = InMemoryTransport.createLinkedPair() 74 | 75 | server.createSession(serverTransport) 76 | 77 | val client = mcpClient( 78 | clientInfo = Implementation(name = "test client", version = "1.0"), 79 | transport = clientTransport, 80 | ) 81 | 82 | assertNull(client.serverInstructions) 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /kotlin-sdk-test/src/jvmTest/kotlin/io/modelcontextprotocol/kotlin/sdk/integration/typescript/OldSchemaAbstractKotlinClientTsServerTest.kt: -------------------------------------------------------------------------------- 1 | package io.modelcontextprotocol.kotlin.sdk.integration.typescript 2 | 3 | import io.modelcontextprotocol.kotlin.sdk.TextContent 4 | import io.modelcontextprotocol.kotlin.sdk.client.Client 5 | import kotlinx.coroutines.Dispatchers 6 | import kotlinx.coroutines.runBlocking 7 | import org.junit.jupiter.api.Test 8 | import org.junit.jupiter.api.Timeout 9 | import java.util.concurrent.TimeUnit 10 | import kotlin.test.assertEquals 11 | import kotlin.test.assertNotNull 12 | import kotlin.test.assertTrue 13 | 14 | abstract class OldSchemaAbstractKotlinClientTsServerTest : OldSchemaTsTestBase() { 15 | protected abstract suspend fun useClient(block: suspend (Client) -> T): T 16 | 17 | @Test 18 | @Timeout(30, unit = TimeUnit.SECONDS) 19 | fun connectsAndPings() = runBlocking(Dispatchers.IO) { 20 | useClient { client -> 21 | assertNotNull(client, "Client should be initialized") 22 | val ping = client.ping() 23 | assertNotNull(ping, "Ping result should not be null") 24 | val serverImpl = client.serverVersion 25 | assertNotNull(serverImpl, "Server implementation should not be null") 26 | println("Connected to TypeScript server: ${serverImpl.name} v${serverImpl.version}") 27 | } 28 | } 29 | 30 | @Test 31 | @Timeout(30, unit = TimeUnit.SECONDS) 32 | fun listsTools() = runBlocking(Dispatchers.IO) { 33 | useClient { client -> 34 | val result = client.listTools() 35 | assertNotNull(result, "Tools list should not be null") 36 | assertTrue(result.tools.isNotEmpty(), "Tools list should not be empty") 37 | val toolNames = result.tools.map { it.name } 38 | assertTrue("greet" in toolNames, "Greet tool should be available") 39 | assertTrue("multi-greet" in toolNames, "Multi-greet tool should be available") 40 | // Some tests also check collect-user-info; keep base minimal and non-breaking 41 | } 42 | } 43 | 44 | @Test 45 | @Timeout(30, unit = TimeUnit.SECONDS) 46 | fun callGreet() = runBlocking(Dispatchers.IO) { 47 | useClient { client -> 48 | val testName = "TestUser" 49 | val arguments = mapOf("name" to testName) 50 | val result = client.callTool("greet", arguments) 51 | assertNotNull(result, "Tool call result should not be null") 52 | val callResult = result as io.modelcontextprotocol.kotlin.sdk.types.CallToolResult 53 | val textContent = callResult.content.firstOrNull { it is TextContent } as? TextContent 54 | assertNotNull(textContent, "Text content should be present in the result") 55 | assertEquals("Hello, $testName!", textContent.text) 56 | } 57 | } 58 | 59 | @Test 60 | @Timeout(30, unit = TimeUnit.SECONDS) 61 | fun multipleClients() = runBlocking(Dispatchers.IO) { 62 | useClient { client1 -> 63 | useClient { client2 -> 64 | val tools1 = client1.listTools() 65 | val tools2 = client2.listTools() 66 | assertTrue(tools1.tools.isNotEmpty(), "Tools list for first client should not be empty") 67 | assertTrue(tools2.tools.isNotEmpty(), "Tools list for second client should not be empty") 68 | val toolNames1 = tools1.tools.map { it.name } 69 | val toolNames2 = tools2.tools.map { it.name } 70 | assertTrue("greet" in toolNames1, "Greet tool should be available to first client") 71 | assertTrue("multi-greet" in toolNames1, "Multi-greet tool should be available to first client") 72 | assertTrue("greet" in toolNames2, "Greet tool should be available to second client") 73 | assertTrue("multi-greet" in toolNames2, "Multi-greet tool should be available to second client") 74 | } 75 | } 76 | } 77 | } 78 | --------------------------------------------------------------------------------