@
Summary
MCP tool parameters that carry a "stringified JSON blob" (e.g. jsonPatch on gameobject-component-modify) are declared as C# string. ReflectorNet emits a flat {"type":"string"} input schema for them, so when an LLM sends the value as a raw JSON object instead of a JSON string, the call is rejected up front with:
MCP error -32602: Input validation error: ... { "expected": "string", "code": "invalid_type", "path": ["jsonPatch"], "message": "Invalid input: expected string, received object" }
The model then has to redo the call. We want to accept both forms transparently.
Root cause (verified)
- Schema too strict.
GeneratePrimitiveSchema emits {"type":"string"} for string params — ReflectorNet/src/Utils/Json/JsonSchema.Internal.cs:268-269. The MCP layer validates the call against this schema and rejects an object before binding runs.
- Binder would also fail.
string is IsPrimitive==true (TypeUtils.Helpers.cs:82), so in MethodWrapper.GetParameterValue (both overloads, ~L260 and ~L343) the un-stringify branch is skipped and it calls jsonElement.Deserialize<string>(...), which throws on an object. JsonUtils.TryUnstringifyJson already handles string->object; the reverse (object->string) is missing.
Proposed approach
- Schema (opt-in): add a parameter attribute (proposed
[JsonStringOrObject]) that makes the emitted schema {"anyOf":[{"type":"string"},{"type":"object","additionalProperties":true}]} (consider also array). Reuse the existing JsonArrayJsonConverter.JsonAnySchema pattern (Converter/Json/JsonArrayJsonConverter.cs:21-33). Keep it opt-in — do not loosen every string schema globally.
- Binder (unconditional, safe): in both
MethodWrapper.GetParameterValue overloads, when the target type is string and the incoming JsonElement.ValueKind is Object/Array, bind jsonElement.GetRawText() instead of attempting Deserialize<string>.
Acceptance criteria
Reported via tool gameobject-component-modify (jsonPatch sent as a nested object).
@
@
Summary
MCP tool parameters that carry a "stringified JSON blob" (e.g.
jsonPatchongameobject-component-modify) are declared as C#string. ReflectorNet emits a flat{"type":"string"}input schema for them, so when an LLM sends the value as a raw JSON object instead of a JSON string, the call is rejected up front with:The model then has to redo the call. We want to accept both forms transparently.
Root cause (verified)
GeneratePrimitiveSchemaemits{"type":"string"}forstringparams —ReflectorNet/src/Utils/Json/JsonSchema.Internal.cs:268-269. The MCP layer validates the call against this schema and rejects an object before binding runs.stringisIsPrimitive==true(TypeUtils.Helpers.cs:82), so inMethodWrapper.GetParameterValue(both overloads, ~L260 and ~L343) the un-stringify branch is skipped and it callsjsonElement.Deserialize<string>(...), which throws on an object.JsonUtils.TryUnstringifyJsonalready handles string->object; the reverse (object->string) is missing.Proposed approach
[JsonStringOrObject]) that makes the emitted schema{"anyOf":[{"type":"string"},{"type":"object","additionalProperties":true}]}(consider alsoarray). Reuse the existingJsonArrayJsonConverter.JsonAnySchemapattern (Converter/Json/JsonArrayJsonConverter.cs:21-33). Keep it opt-in — do not loosen everystringschema globally.MethodWrapper.GetParameterValueoverloads, when the target type isstringand the incomingJsonElement.ValueKindisObject/Array, bindjsonElement.GetRawText()instead of attemptingDeserialize<string>.Acceptance criteria
[JsonStringOrObject]attribute exists; applied to astringparam it makes the schema ananyOfof string + object.JsonElementinto raw JSON text forstringparams (both overloads).dotnet test --configuration Releasegreen on net8.0 + net9.0.Reported via tool
gameobject-component-modify(jsonPatchsent as a nested object).@