mcp+tools: preserve JSON primitive types across the wire
McpLiteToolAdapter.parseArgsJson() used JsonPrimitive.content for every value
(always String), so MCP servers saw e.g. max_length:"500" and rejected
calls with '500' is not of type 'integer'. Replaced with kotlinx.serialization
booleanOrNull/intOrNull/longOrNull/doubleOrNull ladder that mirrors the
litert-openai parser.
ChatConversation.encodeArgsJson was also still calling encodeToolArgs
(stub from earlier pass). Inlined a small Any?.toJsonElement() helper
that preserves Boolean/Number/String/Map/List types when re-serializing
the LiteToolCall.arguments map into the argsJson string fed to
tool.invoke().
E2E re-verified: litellm local/codding + mcp-server-fetch now invokes
fetch__fetch({max_length:2000,url:...}) once, MCP gets a real
'Failed to fetch robots.txt' from the actual network sandbox instead
of rejecting the schema, LLM reports the real cause to the user.
This commit is contained in:
+21
-1
@@ -359,7 +359,27 @@ class ChatConversation(
|
||||
|
||||
private fun newId(prefix: String): String = "$prefix-${java.util.UUID.randomUUID()}"
|
||||
|
||||
private fun encodeArgsJson(arguments: Map<String, Any?>): String = encodeToolArgs(arguments)
|
||||
private fun encodeArgsJson(arguments: Map<String, Any?>): String {
|
||||
val el = kotlinx.serialization.json.JsonElement.serializer()
|
||||
val obj = kotlinx.serialization.json.buildJsonObject {
|
||||
arguments.forEach { (k, v) -> put(k, v.toJsonElement()) }
|
||||
}
|
||||
return kotlinx.serialization.json.Json.encodeToString(el, obj)
|
||||
}
|
||||
|
||||
private fun Any?.toJsonElement(): kotlinx.serialization.json.JsonElement = when (this) {
|
||||
null -> kotlinx.serialization.json.JsonNull
|
||||
is Boolean -> kotlinx.serialization.json.JsonPrimitive(this)
|
||||
is Number -> kotlinx.serialization.json.JsonPrimitive(this)
|
||||
is String -> kotlinx.serialization.json.JsonPrimitive(this)
|
||||
is Map<*, *> -> kotlinx.serialization.json.buildJsonObject {
|
||||
this@toJsonElement.forEach { (k, v) ->
|
||||
put(k.toString(), v.toJsonElement())
|
||||
}
|
||||
}
|
||||
is List<*> -> kotlinx.serialization.json.JsonArray(this.map { it.toJsonElement() })
|
||||
else -> kotlinx.serialization.json.JsonPrimitive(toString())
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val MAX_TOOL_LOOPS = 16
|
||||
|
||||
@@ -22,7 +22,11 @@ import kotlinx.serialization.json.JsonArray
|
||||
import kotlinx.serialization.json.JsonElement
|
||||
import kotlinx.serialization.json.JsonObject
|
||||
import kotlinx.serialization.json.JsonPrimitive
|
||||
import kotlinx.serialization.json.booleanOrNull
|
||||
import kotlinx.serialization.json.buildJsonObject
|
||||
import kotlinx.serialization.json.doubleOrNull
|
||||
import kotlinx.serialization.json.intOrNull
|
||||
import kotlinx.serialization.json.longOrNull
|
||||
import kotlinx.serialization.json.put
|
||||
import pw.binom.agentik.standalone.agent.NamedTool
|
||||
import pw.binom.litert.LiteTool
|
||||
@@ -234,10 +238,22 @@ internal class McpLiteToolAdapter(
|
||||
private fun JsonObject.toAnyMap(): Map<String, Any?> =
|
||||
entries.associate { (k, v) -> k to jsonElementToAny(v) }
|
||||
|
||||
/**
|
||||
* Преобразует [JsonPrimitive] в типизированное значение (Boolean/Int/Long/Float/Double),
|
||||
* и только в крайнем случае — в String. Без этого MCP-сервер получает все аргументы
|
||||
* как строки и отвергает их по JSON-schema (например, `max_length:500` → `"500"` →
|
||||
* «'500' is not of type 'integer'»).
|
||||
*/
|
||||
private fun jsonElementToAny(el: JsonElement): Any? = when (el) {
|
||||
is JsonPrimitive -> el.content
|
||||
is JsonPrimitive ->
|
||||
el.booleanOrNull
|
||||
?: el.intOrNull
|
||||
?: el.longOrNull
|
||||
?: el.doubleOrNull
|
||||
?: if (el.isString) el.content else el.content
|
||||
is JsonArray -> el.map { jsonElementToAny(it) }
|
||||
is JsonObject -> el.toAnyMap()
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user