Skip to content

Commit 2cab6cf

Browse files
authored
Merge branch 'main' into jclyne.10-23-adds_optional__meta_field_to_tool
2 parents a9ff1fc + f462019 commit 2cab6cf

File tree

30 files changed

+958
-757
lines changed

30 files changed

+958
-757
lines changed

.github/workflows/build.yml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -49,9 +49,9 @@ jobs:
4949
working-directory: ./samples/kotlin-mcp-client
5050
run: ./gradlew --no-daemon clean build -Pmcp.kotlin.overrideVersion=0.0.1-SNAPSHOT
5151

52-
# - name: Build Kotlin-MCP-Server Sample
53-
# working-directory: ./samples/kotlin-mcp-server
54-
# run: ./gradlew --no-daemon clean build -Pmcp.kotlin.overrideVersion=0.0.1-SNAPSHOT
52+
- name: Build Kotlin-MCP-Server Sample
53+
working-directory: ./samples/kotlin-mcp-server
54+
run: ./gradlew --no-daemon clean build -Pmcp.kotlin.overrideVersion=0.0.1-SNAPSHOT
5555

5656
- name: Build Weather-Stdio-Server Sample
5757
working-directory: ./samples/weather-stdio-server

.github/workflows/samples.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ jobs:
2323
matrix:
2424
sample:
2525
- kotlin-mcp-client
26-
# - kotlin-mcp-server
26+
- kotlin-mcp-server
2727
- weather-stdio-server
2828

2929
name: Build Sample

gradle/libs.versions.toml

Lines changed: 17 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ kover = "0.9.3"
88
netty = "4.2.7.Final"
99
mavenPublish = "0.34.0"
1010
binaryCompatibilityValidatorPlugin = "0.18.1"
11+
openapi-generator = "7.17.0"
1112

1213
# libraries version
1314
serialization = "1.9.0"
@@ -23,45 +24,46 @@ mokksy = "0.6.1"
2324

2425
[libraries]
2526
# Plugins
27+
dokka-gradle = { module = "org.jetbrains.dokka:dokka-gradle-plugin", version.ref = "dokka" }
2628
kotlin-gradle = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin" }
2729
kotlin-serialization = { module = "org.jetbrains.kotlin:kotlin-serialization", version.ref = "kotlin" }
2830
kotlinx-atomicfu-gradle = { module = "org.jetbrains.kotlinx:atomicfu-gradle-plugin", version.ref = "atomicfu" }
29-
dokka-gradle = { module = "org.jetbrains.dokka:dokka-gradle-plugin", version.ref = "dokka" }
3031
maven-publish = { module = "com.vanniktech:gradle-maven-publish-plugin", version.ref = "mavenPublish" }
3132

3233
# Kotlinx libraries
33-
kotlinx-serialization-json = { group = "org.jetbrains.kotlinx", name = "kotlinx-serialization-json", version.ref = "serialization" }
34+
kotlin-logging = { group = "io.github.oshai", name = "kotlin-logging", version.ref = "logging" }
35+
kotlinx-collections-immutable = { group = "org.jetbrains.kotlinx", name = "kotlinx-collections-immutable", version.ref = "collections-immutable" }
3436
kotlinx-coroutines-core = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-core", version.ref = "coroutines" }
3537
kotlinx-coroutines-core-wasm = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-core-wasm-js", version.ref = "coroutines" }
3638
kotlinx-io-core = { group = "org.jetbrains.kotlinx", name = "kotlinx-io-core", version.ref = "kotlinx-io" }
37-
kotlinx-collections-immutable = { group = "org.jetbrains.kotlinx", name = "kotlinx-collections-immutable", version.ref = "collections-immutable" }
38-
kotlin-logging = { group = "io.github.oshai", name = "kotlin-logging", version.ref = "logging" }
39+
kotlinx-serialization-json = { group = "org.jetbrains.kotlinx", name = "kotlinx-serialization-json", version.ref = "serialization" }
3940

4041
# Ktor
4142
ktor-bom = { group = "io.ktor", name = "ktor-bom", version.ref = "ktor" }
42-
ktor-client-core = { group = "io.ktor", name = "ktor-client-core", version.ref = "ktor" }
43-
ktor-client-logging = { group = "io.ktor", name = "ktor-client-logging"}
4443
ktor-client-apache5 = { group = "io.ktor", name = "ktor-client-apache5" }
45-
ktor-server-sse = { group = "io.ktor", name = "ktor-server-sse", version.ref = "ktor" }
46-
ktor-server-websockets = { group = "io.ktor", name = "ktor-server-websockets", version.ref = "ktor" }
47-
ktor-server-core = { group = "io.ktor", name = "ktor-server-core", version.ref = "ktor" }
44+
ktor-client-core = { group = "io.ktor", name = "ktor-client-core" }
45+
ktor-client-logging = { group = "io.ktor", name = "ktor-client-logging" }
46+
ktor-server-core = { group = "io.ktor", name = "ktor-server-core" }
47+
ktor-server-sse = { group = "io.ktor", name = "ktor-server-sse" }
48+
ktor-server-websockets = { group = "io.ktor", name = "ktor-server-websockets" }
4849

4950
# Testing
5051
awaitility = { group = "org.awaitility", name = "awaitility-kotlin", version.ref = "awaitility" }
5152
kotest-assertions-core = { group = "io.kotest", name = "kotest-assertions-core", version.ref = "kotest" }
5253
kotest-assertions-json = { group = "io.kotest", name = "kotest-assertions-json", version.ref = "kotest" }
5354
kotlinx-coroutines-test = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-test", version.ref = "coroutines" }
54-
ktor-client-mock = { group = "io.ktor", name = "ktor-client-mock", version.ref = "ktor" }
55-
ktor-server-test-host = { group = "io.ktor", name = "ktor-server-test-host", version.ref = "ktor" }
55+
ktor-client-mock = { group = "io.ktor", name = "ktor-client-mock" }
56+
ktor-server-test-host = { group = "io.ktor", name = "ktor-server-test-host" }
5657
mokksy = { group = "dev.mokksy", name = "mokksy", version.ref = "mokksy" }
57-
slf4j-simple = { group = "org.slf4j", name = "slf4j-simple", version.ref = "slf4j" }
5858
netty-bom = { group = "io.netty", name = "netty-bom", version.ref = "netty" }
59+
slf4j-simple = { group = "org.slf4j", name = "slf4j-simple", version.ref = "slf4j" }
5960

6061
# Samples
61-
ktor-client-cio = { group = "io.ktor", name = "ktor-client-cio", version.ref = "ktor" }
62-
ktor-server-cio = { group = "io.ktor", name = "ktor-server-cio", version.ref = "ktor" }
62+
ktor-client-cio = { group = "io.ktor", name = "ktor-client-cio" }
63+
ktor-server-cio = { group = "io.ktor", name = "ktor-server-cio" }
6364

6465
[plugins]
6566
kotlinx-binary-compatibility-validator = { id = "org.jetbrains.kotlinx.binary-compatibility-validator", version.ref = "binaryCompatibilityValidatorPlugin" }
66-
ktlint = { id = "org.jlleitschuh.gradle.ktlint", version.ref = "ktlint" }
6767
kover = { id = "org.jetbrains.kotlinx.kover", version.ref = "kover" }
68+
ktlint = { id = "org.jlleitschuh.gradle.ktlint", version.ref = "ktlint" }
69+
openapi-generator = { id = "org.openapi.generator", version.ref = "openapi-generator" }

kotlin-sdk-client/build.gradle.kts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ kotlin {
3232
sourceSets {
3333
commonMain {
3434
dependencies {
35+
implementation(dependencies.platform(libs.ktor.bom))
3536
api(project(":kotlin-sdk-core"))
3637
api(libs.ktor.client.core)
3738
implementation(libs.kotlin.logging)
@@ -41,7 +42,6 @@ kotlin {
4142
commonTest {
4243
dependencies {
4344
implementation(kotlin("test"))
44-
implementation(dependencies.platform(libs.ktor.bom))
4545
implementation(libs.ktor.client.mock)
4646
implementation(libs.ktor.server.websockets)
4747
implementation(libs.kotlinx.coroutines.test)

kotlin-sdk-client/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/Client.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -315,7 +315,7 @@ public open class Client(private val clientInfo: Implementation, options: Client
315315
* @throws IllegalStateException If the server does not support logging.
316316
*/
317317
public suspend fun setLoggingLevel(level: LoggingLevel, options: RequestOptions? = null): EmptyRequestResult =
318-
request<EmptyRequestResult>(SetLevelRequest(level), options)
318+
request(SetLevelRequest(level), options)
319319

320320
/**
321321
* Retrieves a prompt by name from the server.

kotlin-sdk-core/build.gradle.kts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ plugins {
77
id("mcp.publishing")
88
id("mcp.dokka")
99
alias(libs.plugins.kotlinx.binary.compatibility.validator)
10-
id("org.openapi.generator") version "7.16.0"
10+
alias(libs.plugins.openapi.generator)
1111
}
1212

1313
/*
@@ -110,6 +110,7 @@ kotlin {
110110
commonMain {
111111
kotlin.srcDir(generateLibVersion)
112112
dependencies {
113+
implementation(dependencies.platform(libs.ktor.bom))
113114
api(libs.kotlinx.serialization.json)
114115
api(libs.kotlinx.coroutines.core)
115116
api(libs.kotlinx.io.core)

kotlin-sdk-server/api/kotlin-sdk-server.api

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,40 +5,43 @@ public final class io/modelcontextprotocol/kotlin/sdk/server/KtorServerKt {
55
public static final fun mcp (Lio/ktor/server/routing/Routing;Lkotlin/jvm/functions/Function1;)V
66
}
77

8-
public final class io/modelcontextprotocol/kotlin/sdk/server/RegisteredPrompt {
8+
public final class io/modelcontextprotocol/kotlin/sdk/server/RegisteredPrompt : io/modelcontextprotocol/kotlin/sdk/server/Feature {
99
public fun <init> (Lio/modelcontextprotocol/kotlin/sdk/Prompt;Lkotlin/jvm/functions/Function2;)V
1010
public final fun component1 ()Lio/modelcontextprotocol/kotlin/sdk/Prompt;
1111
public final fun component2 ()Lkotlin/jvm/functions/Function2;
1212
public final fun copy (Lio/modelcontextprotocol/kotlin/sdk/Prompt;Lkotlin/jvm/functions/Function2;)Lio/modelcontextprotocol/kotlin/sdk/server/RegisteredPrompt;
1313
public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/server/RegisteredPrompt;Lio/modelcontextprotocol/kotlin/sdk/Prompt;Lkotlin/jvm/functions/Function2;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/server/RegisteredPrompt;
1414
public fun equals (Ljava/lang/Object;)Z
15+
public fun getKey ()Ljava/lang/String;
1516
public final fun getMessageProvider ()Lkotlin/jvm/functions/Function2;
1617
public final fun getPrompt ()Lio/modelcontextprotocol/kotlin/sdk/Prompt;
1718
public fun hashCode ()I
1819
public fun toString ()Ljava/lang/String;
1920
}
2021

21-
public final class io/modelcontextprotocol/kotlin/sdk/server/RegisteredResource {
22+
public final class io/modelcontextprotocol/kotlin/sdk/server/RegisteredResource : io/modelcontextprotocol/kotlin/sdk/server/Feature {
2223
public fun <init> (Lio/modelcontextprotocol/kotlin/sdk/Resource;Lkotlin/jvm/functions/Function2;)V
2324
public final fun component1 ()Lio/modelcontextprotocol/kotlin/sdk/Resource;
2425
public final fun component2 ()Lkotlin/jvm/functions/Function2;
2526
public final fun copy (Lio/modelcontextprotocol/kotlin/sdk/Resource;Lkotlin/jvm/functions/Function2;)Lio/modelcontextprotocol/kotlin/sdk/server/RegisteredResource;
2627
public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/server/RegisteredResource;Lio/modelcontextprotocol/kotlin/sdk/Resource;Lkotlin/jvm/functions/Function2;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/server/RegisteredResource;
2728
public fun equals (Ljava/lang/Object;)Z
29+
public fun getKey ()Ljava/lang/String;
2830
public final fun getReadHandler ()Lkotlin/jvm/functions/Function2;
2931
public final fun getResource ()Lio/modelcontextprotocol/kotlin/sdk/Resource;
3032
public fun hashCode ()I
3133
public fun toString ()Ljava/lang/String;
3234
}
3335

34-
public final class io/modelcontextprotocol/kotlin/sdk/server/RegisteredTool {
36+
public final class io/modelcontextprotocol/kotlin/sdk/server/RegisteredTool : io/modelcontextprotocol/kotlin/sdk/server/Feature {
3537
public fun <init> (Lio/modelcontextprotocol/kotlin/sdk/Tool;Lkotlin/jvm/functions/Function2;)V
3638
public final fun component1 ()Lio/modelcontextprotocol/kotlin/sdk/Tool;
3739
public final fun component2 ()Lkotlin/jvm/functions/Function2;
3840
public final fun copy (Lio/modelcontextprotocol/kotlin/sdk/Tool;Lkotlin/jvm/functions/Function2;)Lio/modelcontextprotocol/kotlin/sdk/server/RegisteredTool;
3941
public static synthetic fun copy$default (Lio/modelcontextprotocol/kotlin/sdk/server/RegisteredTool;Lio/modelcontextprotocol/kotlin/sdk/Tool;Lkotlin/jvm/functions/Function2;ILjava/lang/Object;)Lio/modelcontextprotocol/kotlin/sdk/server/RegisteredTool;
4042
public fun equals (Ljava/lang/Object;)Z
4143
public final fun getHandler ()Lkotlin/jvm/functions/Function2;
44+
public fun getKey ()Ljava/lang/String;
4245
public final fun getTool ()Lio/modelcontextprotocol/kotlin/sdk/Tool;
4346
public fun hashCode ()I
4447
public fun toString ()Ljava/lang/String;

kotlin-sdk-server/build.gradle.kts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ kotlin {
99
sourceSets {
1010
commonMain {
1111
dependencies {
12+
implementation(dependencies.platform(libs.ktor.bom))
1213
api(project(":kotlin-sdk-core"))
1314
api(libs.ktor.server.core)
1415
api(libs.ktor.server.sse)
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
package io.modelcontextprotocol.kotlin.sdk.server
2+
3+
import io.github.oshai.kotlinlogging.KotlinLogging
4+
import kotlinx.atomicfu.atomic
5+
import kotlinx.atomicfu.getAndUpdate
6+
import kotlinx.atomicfu.update
7+
import kotlinx.collections.immutable.minus
8+
import kotlinx.collections.immutable.persistentMapOf
9+
import kotlinx.collections.immutable.toPersistentSet
10+
11+
/**
12+
* A generic registry for managing features of a specified type. This class provides thread-safe
13+
* operations for adding, removing, and retrieving features from the registry.
14+
*
15+
* @param T The type of the feature, constrained to implement the [Feature] interface.
16+
* @param featureType A string description of the type of feature being managed.
17+
* Used primarily for logging purposes.
18+
*/
19+
internal class FeatureRegistry<T : Feature>(private val featureType: String) {
20+
21+
private val logger = KotlinLogging.logger(name = "FeatureRegistry[$featureType]")
22+
23+
/**
24+
* Atomic variable used to maintain a thread-safe registry of features.
25+
* Stores a persistent map where each feature is identified by a unique key.
26+
*/
27+
private val registry = atomic(persistentMapOf<FeatureKey, T>())
28+
29+
/**
30+
* Provides a snapshot of all features currently registered in the registry.
31+
* Keys represent unique identifiers for each feature, and values represent the associated features.
32+
*/
33+
internal val values: Map<FeatureKey, T>
34+
get() = registry.value
35+
36+
/**
37+
* Adds the specified feature to the registry.
38+
*
39+
* @param feature The feature to be added to the registry.
40+
*/
41+
internal fun add(feature: T) {
42+
logger.info { "Adding $featureType: \"${feature.key}\"" }
43+
registry.update { current -> current.put(feature.key, feature) }
44+
logger.info { "Added $featureType: \"${feature.key}\"" }
45+
}
46+
47+
/**
48+
* Adds the given list of features to the registry. Each feature is mapped by its key
49+
* and added to the current registry.
50+
*
51+
* @param features The list of features to add to the registry.
52+
*/
53+
internal fun addAll(features: List<T>) {
54+
logger.info { "Adding ${featureType}s: ${features.size}" }
55+
registry.update { current -> current.putAll(features.associateBy { it.key }) }
56+
logger.info { "Added ${featureType}s: ${features.size}" }
57+
}
58+
59+
/**
60+
* Removes the feature associated with the given key from the registry.
61+
*
62+
* @param key The key of the feature to be removed.
63+
* @return `true` if the feature was successfully removed, or `false` if the feature was not found.
64+
*/
65+
internal fun remove(key: FeatureKey): Boolean {
66+
logger.info { "Removing $featureType: \"$key\"" }
67+
val oldMap = registry.getAndUpdate { current -> current.remove(key) }
68+
69+
val removed = key in oldMap
70+
logger.info {
71+
if (removed) {
72+
"Removed $featureType: \"$key\""
73+
} else {
74+
"$featureType not found: \"$key\""
75+
}
76+
}
77+
return key in oldMap
78+
}
79+
80+
/**
81+
* Removes the features associated with the given keys from the registry.
82+
*
83+
* @param keys The list of keys whose associated features are to be removed.
84+
* @return The number of features that were successfully removed.
85+
*/
86+
internal fun removeAll(keys: List<FeatureKey>): Int {
87+
logger.info { "Removing ${featureType}s: ${keys.size}" }
88+
val oldMap = registry.getAndUpdate { current -> current - keys.toPersistentSet() }
89+
90+
val removedCount = keys.count { it in oldMap }
91+
logger.info {
92+
if (removedCount > 0) {
93+
"Removed ${featureType}s: $removedCount"
94+
} else {
95+
"No $featureType were removed"
96+
}
97+
}
98+
99+
return removedCount
100+
}
101+
102+
/**
103+
* Retrieves the feature associated with the given key from the registry.
104+
*
105+
* @param key The key of the feature to retrieve.
106+
* @return The feature associated with the given key, or `null` if no such feature exists in the registry.
107+
*/
108+
internal fun get(key: FeatureKey): T? {
109+
logger.info { "Getting $featureType: \"$key\"" }
110+
val feature = registry.value[key]
111+
logger.info {
112+
if (feature != null) {
113+
"Got $featureType: \"$key\""
114+
} else {
115+
"$featureType not found: \"$key\""
116+
}
117+
}
118+
return feature
119+
}
120+
}
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
package io.modelcontextprotocol.kotlin.sdk.server
2+
3+
import io.modelcontextprotocol.kotlin.sdk.CallToolRequest
4+
import io.modelcontextprotocol.kotlin.sdk.CallToolResult
5+
import io.modelcontextprotocol.kotlin.sdk.GetPromptRequest
6+
import io.modelcontextprotocol.kotlin.sdk.GetPromptResult
7+
import io.modelcontextprotocol.kotlin.sdk.Prompt
8+
import io.modelcontextprotocol.kotlin.sdk.ReadResourceRequest
9+
import io.modelcontextprotocol.kotlin.sdk.ReadResourceResult
10+
import io.modelcontextprotocol.kotlin.sdk.Resource
11+
import io.modelcontextprotocol.kotlin.sdk.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+
}

0 commit comments

Comments
 (0)