Skip to content

Commit 127b270

Browse files
author
MAERYO
committed
refactor: simplify validation and JSON conversion
1 parent 2b957a9 commit 127b270

File tree

1 file changed

+54
-87
lines changed
  • kotlin-sdk-client/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client

1 file changed

+54
-87
lines changed

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

Lines changed: 54 additions & 87 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,8 @@ import kotlinx.serialization.json.JsonElement
5656
import kotlinx.serialization.json.JsonNull
5757
import kotlinx.serialization.json.JsonObject
5858
import kotlinx.serialization.json.JsonPrimitive
59+
import kotlinx.serialization.json.buildJsonArray
60+
import kotlinx.serialization.json.buildJsonObject
5961
import kotlin.coroutines.cancellation.CancellationException
6062

6163
private val logger = KotlinLogging.logger {}
@@ -600,61 +602,38 @@ public open class Client(private val clientInfo: Implementation, options: Client
600602
* - Name: alphanumeric start/end, may contain hyphens, underscores, dots (empty allowed)
601603
*/
602604
private fun validateMetaKeys(keys: Set<String>) {
603-
for (key in keys) {
604-
if (!isValidMetaKey(key)) {
605-
throw Error("Invalid _meta key '$key'. Must follow format [prefix/]name with valid labels.")
605+
val labelPattern = Regex("[a-zA-Z]([a-zA-Z0-9-]*[a-zA-Z0-9])?")
606+
val namePattern = Regex("[a-zA-Z0-9]([a-zA-Z0-9._-]*[a-zA-Z0-9])?")
607+
608+
keys.forEach { key ->
609+
require(key.isNotEmpty()) { "Meta key cannot be empty" }
610+
611+
val (prefix, name) = key.split('/', limit = 2).let { parts ->
612+
when (parts.size) {
613+
1 -> null to parts[0]
614+
else -> parts[0] to parts[1]
615+
}
606616
}
607-
}
608-
}
609-
610-
private fun isValidMetaKey(key: String): Boolean {
611-
if (key.isEmpty()) return false
612-
val parts = key.split('/', limit = 2)
613-
return when (parts.size) {
614-
1 -> {
615-
// No prefix, just validate name
616-
isValidMetaName(parts[0])
617+
618+
// Validate prefix if present
619+
prefix?.let {
620+
require(it.isNotEmpty()) { "Invalid _meta key '$key': prefix cannot be empty" }
621+
622+
val labels = it.split('.')
623+
require(labels.all { label -> label.matches(labelPattern) }) {
624+
"Invalid _meta key '$key': prefix labels must start with a letter, end with letter/digit, and contain only letters, digits, or hyphens"
625+
}
626+
627+
require(labels.none { label -> label.equals("modelcontextprotocol", ignoreCase = true) || label.equals("mcp", ignoreCase = true) }) {
628+
"Invalid _meta key '$key': prefix cannot contain reserved labels 'modelcontextprotocol' or 'mcp'"
629+
}
617630
}
618-
619-
2 -> {
620-
val (prefix, name) = parts
621-
isValidMetaPrefix(prefix) && isValidMetaName(name)
631+
632+
// Validate name (empty allowed)
633+
require(name.isEmpty() || name.matches(namePattern)) {
634+
"Invalid _meta key '$key': name must start and end with alphanumeric characters, and contain only alphanumerics, hyphens, underscores, or dots"
622635
}
623-
624-
else -> false
625-
}
626-
}
627-
628-
private fun isValidMetaPrefix(prefix: String): Boolean {
629-
if (prefix.isEmpty()) return false
630-
val labels = prefix.split('.')
631-
632-
if (!labels.all { isValidLabel(it) }) {
633-
return false
634-
}
635-
636-
return !labels.any { label ->
637-
label.equals("modelcontextprotocol", ignoreCase = true) ||
638-
label.equals("mcp", ignoreCase = true)
639-
}
640-
}
641-
642-
private fun isValidLabel(label: String): Boolean {
643-
if (label.isEmpty()) return false
644-
if (!label.first().isLetter() || !label.last().let { it.isLetter() || it.isDigit() }) {
645-
return false
646636
}
647-
return label.all { it.isLetter() || it.isDigit() || it == '-' }
648-
}
649-
650-
private fun isValidMetaName(name: String): Boolean {
651-
// Empty names are allowed per MCP specification
652-
if (name.isEmpty()) return true
653-
654-
if (!name.first().isLetterOrDigit() || !name.last().isLetterOrDigit()) {
655-
return false
656-
}
657-
return name.all { it.isLetterOrDigit() || it in setOf('-', '_', '.') }
658637
}
659638

660639
private fun convertToJsonMap(map: Map<String, Any?>): Map<String, JsonElement> = map.mapValues { (key, value) ->
@@ -669,54 +648,42 @@ public open class Client(private val clientInfo: Implementation, options: Client
669648
@OptIn(ExperimentalUnsignedTypes::class, ExperimentalSerializationApi::class)
670649
private fun convertToJsonElement(value: Any?): JsonElement = when (value) {
671650
null -> JsonNull
672-
673-
is Map<*, *> -> {
674-
val jsonMap = value.entries.associate { (k, v) ->
675-
k.toString() to convertToJsonElement(v)
676-
}
677-
JsonObject(jsonMap)
678-
}
679-
680651
is JsonElement -> value
681-
682652
is String -> JsonPrimitive(value)
683-
684653
is Number -> JsonPrimitive(value)
685-
686654
is Boolean -> JsonPrimitive(value)
687-
688655
is Char -> JsonPrimitive(value.toString())
689-
690656
is Enum<*> -> JsonPrimitive(value.name)
691657

692-
is Collection<*> -> JsonArray(value.map { convertToJsonElement(it) })
693-
694-
is Array<*> -> JsonArray(value.map { convertToJsonElement(it) })
695-
696-
is IntArray -> JsonArray(value.map { JsonPrimitive(it) })
697-
698-
is LongArray -> JsonArray(value.map { JsonPrimitive(it) })
699-
700-
is FloatArray -> JsonArray(value.map { JsonPrimitive(it) })
701-
702-
is DoubleArray -> JsonArray(value.map { JsonPrimitive(it) })
703-
704-
is BooleanArray -> JsonArray(value.map { JsonPrimitive(it) })
658+
is Map<*, *> -> buildJsonObject {
659+
value.forEach { (k, v) ->
660+
put(k.toString(), convertToJsonElement(v))
661+
}
662+
}
705663

706-
is ShortArray -> JsonArray(value.map { JsonPrimitive(it) })
664+
is Collection<*> -> buildJsonArray {
665+
value.forEach { add(convertToJsonElement(it)) }
666+
}
707667

708-
is ByteArray -> JsonArray(value.map { JsonPrimitive(it) })
668+
is Array<*> -> buildJsonArray {
669+
value.forEach { add(convertToJsonElement(it)) }
670+
}
709671

710-
is CharArray -> JsonArray(value.map { JsonPrimitive(it.toString()) })
672+
// Primitive arrays - use iterator for unified handling
673+
is IntArray -> buildJsonArray { value.forEach { add(it) } }
674+
is LongArray -> buildJsonArray { value.forEach { add(it) } }
675+
is FloatArray -> buildJsonArray { value.forEach { add(it) } }
676+
is DoubleArray -> buildJsonArray { value.forEach { add(it) } }
677+
is BooleanArray -> buildJsonArray { value.forEach { add(it) } }
678+
is ShortArray -> buildJsonArray { value.forEach { add(it) } }
679+
is ByteArray -> buildJsonArray { value.forEach { add(it) } }
680+
is CharArray -> buildJsonArray { value.forEach { add(it.toString()) } }
711681

712682
// ExperimentalUnsignedTypes
713-
is UIntArray -> JsonArray(value.map { JsonPrimitive(it) })
714-
715-
is ULongArray -> JsonArray(value.map { JsonPrimitive(it) })
716-
717-
is UShortArray -> JsonArray(value.map { JsonPrimitive(it) })
718-
719-
is UByteArray -> JsonArray(value.map { JsonPrimitive(it) })
683+
is UIntArray -> buildJsonArray { value.forEach { add(it) } }
684+
is ULongArray -> buildJsonArray { value.forEach { add(it) } }
685+
is UShortArray -> buildJsonArray { value.forEach { add(it) } }
686+
is UByteArray -> buildJsonArray { value.forEach { add(it) } }
720687

721688
else -> {
722689
logger.debug { "Converting unknown type ${value::class.simpleName} to string: $value" }

0 commit comments

Comments
 (0)