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 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 {
|
companion object {
|
||||||
private const val MAX_TOOL_LOOPS = 16
|
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.JsonElement
|
||||||
import kotlinx.serialization.json.JsonObject
|
import kotlinx.serialization.json.JsonObject
|
||||||
import kotlinx.serialization.json.JsonPrimitive
|
import kotlinx.serialization.json.JsonPrimitive
|
||||||
|
import kotlinx.serialization.json.booleanOrNull
|
||||||
import kotlinx.serialization.json.buildJsonObject
|
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 kotlinx.serialization.json.put
|
||||||
import pw.binom.agentik.standalone.agent.NamedTool
|
import pw.binom.agentik.standalone.agent.NamedTool
|
||||||
import pw.binom.litert.LiteTool
|
import pw.binom.litert.LiteTool
|
||||||
@@ -234,10 +238,22 @@ internal class McpLiteToolAdapter(
|
|||||||
private fun JsonObject.toAnyMap(): Map<String, Any?> =
|
private fun JsonObject.toAnyMap(): Map<String, Any?> =
|
||||||
entries.associate { (k, v) -> k to jsonElementToAny(v) }
|
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) {
|
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 JsonArray -> el.map { jsonElementToAny(it) }
|
||||||
is JsonObject -> el.toAnyMap()
|
is JsonObject -> el.toAnyMap()
|
||||||
|
else -> null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user