@@ -56,6 +56,8 @@ import kotlinx.serialization.json.JsonElement
5656import kotlinx.serialization.json.JsonNull
5757import kotlinx.serialization.json.JsonObject
5858import kotlinx.serialization.json.JsonPrimitive
59+ import kotlinx.serialization.json.buildJsonArray
60+ import kotlinx.serialization.json.buildJsonObject
5961import kotlin.coroutines.cancellation.CancellationException
6062
6163private 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