Skip to content

Commit ab255f1

Browse files
authored
Merge branch 'main' into fix/test-calculator-format-and-tags
2 parents 2752a52 + 407df7a commit ab255f1

File tree

42 files changed

+1771
-93
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

42 files changed

+1771
-93
lines changed

.github/workflows/build.yml

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -36,24 +36,26 @@ jobs:
3636

3737
- name: Build with Gradle
3838
run: |-
39-
./gradlew --no-daemon --rerun-tasks \
39+
./gradlew --no-daemon \
40+
--rerun-tasks \
4041
clean \
4142
ktlintCheck \
4243
build \
4344
koverLog koverHtmlReport \
44-
publishToMavenLocal
45+
publishToMavenLocal \
46+
-Pversion=0.0.1-SNAPSHOT
4547
4648
- name: Build Kotlin-MCP-Client Sample
4749
working-directory: ./samples/kotlin-mcp-client
48-
run: ./../../gradlew --no-daemon clean build
50+
run: ./gradlew --no-daemon clean build -Pmcp.kotlin.overrideVersion=0.0.1-SNAPSHOT
4951

50-
- name: Build Kotlin-MCP-Server Sample
51-
working-directory: ./samples/kotlin-mcp-server
52-
run: ./../../gradlew --no-daemon clean build
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
5355

5456
- name: Build Weather-Stdio-Server Sample
5557
working-directory: ./samples/weather-stdio-server
56-
run: ./../../gradlew --no-daemon clean build
58+
run: ./gradlew --no-daemon clean build -Pmcp.kotlin.overrideVersion=0.0.1-SNAPSHOT
5759

5860
- name: Upload Reports
5961
if: ${{ !cancelled() }}

.github/workflows/samples.yml

Lines changed: 2 additions & 2 deletions
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
@@ -47,7 +47,7 @@ jobs:
4747

4848
- name: "Build Sample: ${{ matrix.sample }}"
4949
working-directory: ./samples/${{ matrix.sample }}
50-
run: ./../../gradlew --no-daemon clean build
50+
run: ./gradlew --no-daemon clean build
5151

5252
- name: Upload Reports
5353
if: ${{ !cancelled() }}

build.gradle.kts

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,6 @@ plugins {
44
alias(libs.plugins.kover)
55
}
66

7-
allprojects {
8-
group = "io.modelcontextprotocol"
9-
version = "0.7.4-SNAPSHOT"
10-
}
11-
127
dependencies {
138
dokka(project(":kotlin-sdk-core"))
149
dokka(project(":kotlin-sdk-client"))

gradle.properties

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,3 +12,6 @@ kotlin.js.yarn=false
1212
# MPP
1313
kotlin.mpp.enableCInteropCommonization=true
1414
kotlin.native.ignoreDisabledTargets=true
15+
16+
group=io.modelcontextprotocol
17+
version=0.7.4-SNAPSHOT

gradle/libs.versions.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ mokksy = "0.6.1"
2424
# Samples
2525
mcp-kotlin = "0.7.3"
2626
anthropic = "2.9.0"
27-
shadow = "8.1.1"
27+
shadow = "9.2.2"
2828

2929
[libraries]
3030
# Plugins
@@ -82,4 +82,4 @@ kover = { id = "org.jetbrains.kotlinx.kover", version.ref = "kover" }
8282
kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" }
8383
kotlin-multiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" }
8484
kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" }
85-
shadow = { id = "com.github.johnrengelman.shadow", version.ref = "shadow" }
85+
shadow = { id = "com.gradleup.shadow", version.ref = "shadow" }

kotlin-sdk-client/api/kotlin-sdk-client.api

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,9 @@ public class io/modelcontextprotocol/kotlin/sdk/client/Client : io/modelcontextp
88
protected fun assertNotificationCapability (Lio/modelcontextprotocol/kotlin/sdk/Method;)V
99
public fun assertRequestHandlerCapability (Lio/modelcontextprotocol/kotlin/sdk/Method;)V
1010
public final fun callTool (Lio/modelcontextprotocol/kotlin/sdk/CallToolRequest;ZLio/modelcontextprotocol/kotlin/sdk/shared/RequestOptions;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
11-
public final fun callTool (Ljava/lang/String;Ljava/util/Map;ZLio/modelcontextprotocol/kotlin/sdk/shared/RequestOptions;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
11+
public final fun callTool (Ljava/lang/String;Ljava/util/Map;Ljava/util/Map;ZLio/modelcontextprotocol/kotlin/sdk/shared/RequestOptions;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
1212
public static synthetic fun callTool$default (Lio/modelcontextprotocol/kotlin/sdk/client/Client;Lio/modelcontextprotocol/kotlin/sdk/CallToolRequest;ZLio/modelcontextprotocol/kotlin/sdk/shared/RequestOptions;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object;
13-
public static synthetic fun callTool$default (Lio/modelcontextprotocol/kotlin/sdk/client/Client;Ljava/lang/String;Ljava/util/Map;ZLio/modelcontextprotocol/kotlin/sdk/shared/RequestOptions;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object;
13+
public static synthetic fun callTool$default (Lio/modelcontextprotocol/kotlin/sdk/client/Client;Ljava/lang/String;Ljava/util/Map;Ljava/util/Map;ZLio/modelcontextprotocol/kotlin/sdk/shared/RequestOptions;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object;
1414
public final fun complete (Lio/modelcontextprotocol/kotlin/sdk/CompleteRequest;Lio/modelcontextprotocol/kotlin/sdk/shared/RequestOptions;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
1515
public static synthetic fun complete$default (Lio/modelcontextprotocol/kotlin/sdk/client/Client;Lio/modelcontextprotocol/kotlin/sdk/CompleteRequest;Lio/modelcontextprotocol/kotlin/sdk/shared/RequestOptions;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object;
1616
public fun connect (Lio/modelcontextprotocol/kotlin/sdk/shared/Transport;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;

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

Lines changed: 129 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -50,10 +50,14 @@ import kotlinx.atomicfu.update
5050
import kotlinx.collections.immutable.minus
5151
import kotlinx.collections.immutable.persistentMapOf
5252
import kotlinx.collections.immutable.toPersistentSet
53+
import kotlinx.serialization.ExperimentalSerializationApi
5354
import kotlinx.serialization.json.JsonElement
5455
import kotlinx.serialization.json.JsonNull
5556
import kotlinx.serialization.json.JsonObject
5657
import kotlinx.serialization.json.JsonPrimitive
58+
import kotlinx.serialization.json.add
59+
import kotlinx.serialization.json.buildJsonArray
60+
import kotlinx.serialization.json.buildJsonObject
5761
import kotlin.coroutines.cancellation.CancellationException
5862

5963
private val logger = KotlinLogging.logger {}
@@ -210,17 +214,13 @@ public open class Client(private val clientInfo: Implementation, options: Client
210214
}
211215
}
212216

213-
Method.Defined.ToolsCall,
214-
Method.Defined.ToolsList,
215-
-> {
217+
Method.Defined.ToolsCall, Method.Defined.ToolsList -> {
216218
if (serverCapabilities?.tools == null) {
217219
throw IllegalStateException("Server does not support tools (required for $method)")
218220
}
219221
}
220222

221-
Method.Defined.Initialize,
222-
Method.Defined.Ping,
223-
-> {
223+
Method.Defined.Initialize, Method.Defined.Ping -> {
224224
// No specific capability required
225225
}
226226

@@ -405,10 +405,14 @@ public open class Client(private val clientInfo: Implementation, options: Client
405405
): EmptyRequestResult = request(request, options)
406406

407407
/**
408-
* Calls a tool on the server by name, passing the specified arguments.
408+
* Calls a tool on the server by name, passing the specified arguments and metadata.
409409
*
410410
* @param name The name of the tool to call.
411411
* @param arguments A map of argument names to values for the tool.
412+
* @param meta A map of metadata key-value pairs. Keys must follow MCP specification format.
413+
* - Optional prefix: dot-separated labels followed by slash (e.g., "api.example.com/")
414+
* - Name: alphanumeric start/end, may contain hyphens, underscores, dots, alphanumerics
415+
* - Reserved prefixes starting with "mcp" or "modelcontextprotocol" are forbidden
412416
* @param compatibility Whether to use compatibility mode for older protocol versions.
413417
* @param options Optional request options.
414418
* @return The result of the tool call, or `null` if none.
@@ -417,23 +421,19 @@ public open class Client(private val clientInfo: Implementation, options: Client
417421
public suspend fun callTool(
418422
name: String,
419423
arguments: Map<String, Any?>,
424+
meta: Map<String, Any?> = emptyMap(),
420425
compatibility: Boolean = false,
421426
options: RequestOptions? = null,
422427
): CallToolResultBase? {
423-
val jsonArguments = arguments.mapValues { (_, value) ->
424-
when (value) {
425-
is String -> JsonPrimitive(value)
426-
is Number -> JsonPrimitive(value)
427-
is Boolean -> JsonPrimitive(value)
428-
is JsonElement -> value
429-
null -> JsonNull
430-
else -> JsonPrimitive(value.toString())
431-
}
432-
}
428+
validateMetaKeys(meta.keys)
429+
430+
val jsonArguments = convertToJsonMap(arguments)
431+
val jsonMeta = convertToJsonMap(meta)
433432

434433
val request = CallToolRequest(
435434
name = name,
436435
arguments = JsonObject(jsonArguments),
436+
_meta = JsonObject(jsonMeta),
437437
)
438438
return callTool(request, compatibility, options)
439439
}
@@ -588,4 +588,116 @@ public open class Client(private val clientInfo: Implementation, options: Client
588588
val rootList = roots.value.values.toList()
589589
return ListRootsResult(rootList)
590590
}
591+
592+
/**
593+
* Validates meta keys according to MCP specification.
594+
*
595+
* Key format: [prefix/]name
596+
* - Prefix (optional): dot-separated labels + slash
597+
* - Reserved prefixes contain "modelcontextprotocol" or "mcp" as complete labels
598+
* - Name: alphanumeric start/end, may contain hyphens, underscores, dots (empty allowed)
599+
*/
600+
private fun validateMetaKeys(keys: Set<String>) {
601+
val labelPattern = Regex("[a-zA-Z]([a-zA-Z0-9-]*[a-zA-Z0-9])?")
602+
val namePattern = Regex("[a-zA-Z0-9]([a-zA-Z0-9._-]*[a-zA-Z0-9])?")
603+
604+
keys.forEach { key ->
605+
require(key.isNotEmpty()) { "Meta key cannot be empty" }
606+
607+
val (prefix, name) = key.split('/', limit = 2).let { parts ->
608+
when (parts.size) {
609+
1 -> null to parts[0]
610+
2 -> parts[0] to parts[1]
611+
else -> throw IllegalArgumentException("Unexpected split result for key: $key")
612+
}
613+
}
614+
615+
// Validate prefix if present
616+
prefix?.let {
617+
require(it.isNotEmpty()) { "Invalid _meta key '$key': prefix cannot be empty" }
618+
619+
val labels = it.split('.')
620+
require(labels.all { label -> label.matches(labelPattern) }) {
621+
"Invalid _meta key '$key': prefix labels must start with a letter, end with letter/digit, and contain only letters, digits, or hyphens"
622+
}
623+
624+
require(
625+
labels.none { label ->
626+
label.equals("modelcontextprotocol", ignoreCase = true) ||
627+
label.equals("mcp", ignoreCase = true)
628+
},
629+
) {
630+
"Invalid _meta key '$key': prefix cannot contain reserved labels 'modelcontextprotocol' or 'mcp'"
631+
}
632+
}
633+
634+
// Validate name (empty allowed)
635+
require(name.isEmpty() || name.matches(namePattern)) {
636+
"Invalid _meta key '$key': name must start and end with alphanumeric characters, and contain only alphanumerics, hyphens, underscores, or dots"
637+
}
638+
}
639+
}
640+
641+
private fun convertToJsonMap(map: Map<String, Any?>): Map<String, JsonElement> = map.mapValues { (key, value) ->
642+
try {
643+
convertToJsonElement(value)
644+
} catch (e: Exception) {
645+
logger.warn { "Failed to convert value for key '$key': ${e.message}. Using string representation." }
646+
JsonPrimitive(value.toString())
647+
}
648+
}
649+
650+
@OptIn(ExperimentalUnsignedTypes::class, ExperimentalSerializationApi::class)
651+
private fun convertToJsonElement(value: Any?): JsonElement = when (value) {
652+
null -> JsonNull
653+
654+
is JsonElement -> value
655+
656+
is String -> JsonPrimitive(value)
657+
658+
is Number -> JsonPrimitive(value)
659+
660+
is Boolean -> JsonPrimitive(value)
661+
662+
is Char -> JsonPrimitive(value.toString())
663+
664+
is Enum<*> -> JsonPrimitive(value.name)
665+
666+
is Map<*, *> -> buildJsonObject { value.forEach { (k, v) -> put(k.toString(), convertToJsonElement(v)) } }
667+
668+
is Collection<*> -> buildJsonArray { value.forEach { add(convertToJsonElement(it)) } }
669+
670+
is Array<*> -> buildJsonArray { value.forEach { add(convertToJsonElement(it)) } }
671+
672+
// Primitive arrays
673+
is IntArray -> buildJsonArray { value.forEach { add(it) } }
674+
675+
is LongArray -> buildJsonArray { value.forEach { add(it) } }
676+
677+
is FloatArray -> buildJsonArray { value.forEach { add(it) } }
678+
679+
is DoubleArray -> buildJsonArray { value.forEach { add(it) } }
680+
681+
is BooleanArray -> buildJsonArray { value.forEach { add(it) } }
682+
683+
is ShortArray -> buildJsonArray { value.forEach { add(it) } }
684+
685+
is ByteArray -> buildJsonArray { value.forEach { add(it) } }
686+
687+
is CharArray -> buildJsonArray { value.forEach { add(it.toString()) } }
688+
689+
// Unsigned arrays
690+
is UIntArray -> buildJsonArray { value.forEach { add(JsonPrimitive(it)) } }
691+
692+
is ULongArray -> buildJsonArray { value.forEach { add(JsonPrimitive(it)) } }
693+
694+
is UShortArray -> buildJsonArray { value.forEach { add(JsonPrimitive(it)) } }
695+
696+
is UByteArray -> buildJsonArray { value.forEach { add(JsonPrimitive(it)) } }
697+
698+
else -> {
699+
logger.debug { "Converting unknown type ${value::class} to string: $value" }
700+
JsonPrimitive(value.toString())
701+
}
702+
}
591703
}

0 commit comments

Comments
 (0)