You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Re-encode the JSON Schema type identifiers ReflectorNet emits for MCP tool inputs so they use delimiters that are simultaneously illegal in C# identifiers, legal unescaped in a URI fragment, and absent from every LLM provider's content-firewall blocklist. This supersedes PR #77, which fixed OpenAI but broke Google Gemini.
Context / Motivation
ReflectorNet builds JSON Schema for MCP tool inputs. Non-primitive C# types become $defs keys and $ref values via TypeUtils.GetSchemaTypeId / GetTypeId. Generic, array, and nested C# type-ids contain < > [ ] +, which OpenAI's content firewall rejects raw.
PR #77 (merged) percent-encoded those characters in the $ref values (%5B, %3C, %2B, ...). This fixed OpenAI but broke Google Gemini, whose firewall rejects the % character, so Gemini-CLI can no longer call the tools (see Unity-MCP issue #780). The two providers have disjoint blocklists. Percent-encoding also risks OpenAI strict-structured-output's documented intolerance of escaped $ref pointers, and PR #77 left raw < > [ ] + in the $defs keys — it only encoded the $ref values, leaving the schema asymmetric.
The fix is to drop escaping entirely and re-derive the schema type-id with delimiters that are (a) illegal in C# identifiers (collision-proof — no data-model name can ever clash), (b) legal in a URI fragment unescaped (valid JSON Schema 2020-12), and (c) absent from both firewalls' blocklists.
keep namespace . and generic-arg separator , (both already pass both firewalls)
Disambiguation: after -, a digit denotes array rank and a letter/underscore denotes a nested class. Since C# identifiers cannot start with a digit, this is unambiguous in every nesting order. The $defs key equals the $ref value (symmetric — no encode/decode asymmetry).
Verified against the current source: GetSchemaTypeId / GetTypeId live in ReflectorNet/src/Utils/TypeUtils.Name.cs, GetSchemaRef / EncodeForJsonSchemaRef in ReflectorNet/src/Utils/Json/JsonSchema.cs, and DecodeSchemaRefChars / TryResolveArrayType / TryResolveCSharpGenericType / TryResolveClassicGenericType in ReflectorNet/src/Utils/TypeUtils.GetType.cs.
Proposed approach
ReflectorNet/src/Utils/TypeUtils.Name.cs -> GetTypeId: change the four structural format strings (nested +, generic <>, array [] / [,]) to emit -, ( ), and -<rank>.
ReflectorNet/src/Utils/Json/JsonSchema.cs -> GetSchemaRef: drop EncodeForJsonSchemaRef; emit [Ref] = RefValue + typeId raw so the key equals the ref.
ReflectorNet/src/Utils/TypeUtils.GetType.cs: replace DecodeSchemaRefChars (percent-decode) with a safe-form -> C#-canonical converter ((-><, )->>, -<digits>->[+commas+], -<ident>->+<ident>) run at the top of each GetType overload so the existing TryResolveArrayType / TryResolveCSharpGenericType / TryResolveClassicGenericType parsers keep working. Mind the assembly-qualified-name edge (a hyphen can appear in assembly names).
Tests: update TestSchemaTypeId to the new readable forms; replace TestSchemaRefEncoding's % assertions with assertions that $defs keys AND $ref values contain none of < > [ ] + %; keep TestGetTypeDecodeSchemaRef's round-trip intent (raw id and safe-form resolve to the same Type) for arrays, generics, nested classes, and the combo IList<Outer+Nested[]>.
$defs keys and $ref values never contain any of < > [ ] + %.
Encode/decode round-trips for arbitrary type names with zero collisions (covering arrays, generics, nested classes, and the combo IList<Outer+Nested[]>).
All ReflectorNet tests are green on net8.0 and net9.0.
Consumer verifiers MCP-Plugin-dotnet and Unity-MCP compile against the change.
Generated schema validates against the latest Anthropic, Google (Gemini), and OpenAI (strict structured outputs) models.
Summary
Re-encode the JSON Schema type identifiers ReflectorNet emits for MCP tool inputs so they use delimiters that are simultaneously illegal in C# identifiers, legal unescaped in a URI fragment, and absent from every LLM provider's content-firewall blocklist. This supersedes PR #77, which fixed OpenAI but broke Google Gemini.
Context / Motivation
ReflectorNet builds JSON Schema for MCP tool inputs. Non-primitive C# types become
$defskeys and$refvalues viaTypeUtils.GetSchemaTypeId/GetTypeId. Generic, array, and nested C# type-ids contain< > [ ] +, which OpenAI's content firewall rejects raw.PR #77 (merged) percent-encoded those characters in the
$refvalues (%5B,%3C,%2B, ...). This fixed OpenAI but broke Google Gemini, whose firewall rejects the%character, so Gemini-CLI can no longer call the tools (see Unity-MCP issue #780). The two providers have disjoint blocklists. Percent-encoding also risks OpenAI strict-structured-output's documented intolerance of escaped$refpointers, and PR #77 left raw< > [ ] +in the$defskeys — it only encoded the$refvalues, leaving the schema asymmetric.The fix is to drop escaping entirely and re-derive the schema type-id with delimiters that are (a) illegal in C# identifiers (collision-proof — no data-model name can ever clash), (b) legal in a URI fragment unescaped (valid JSON Schema 2020-12), and (c) absent from both firewalls' blocklists.
Delimiter mapping:
<>->()+->-[]rank-1 ->-1; multi-dim rank N ->-N; jagged repeats suffixes (int[][]->...-1-1).and generic-arg separator,(both already pass both firewalls)Disambiguation: after
-, a digit denotes array rank and a letter/underscore denotes a nested class. Since C# identifiers cannot start with a digit, this is unambiguous in every nesting order. The$defskey equals the$refvalue (symmetric — no encode/decode asymmetry).Verified against the current source:
GetSchemaTypeId/GetTypeIdlive inReflectorNet/src/Utils/TypeUtils.Name.cs,GetSchemaRef/EncodeForJsonSchemaRefinReflectorNet/src/Utils/Json/JsonSchema.cs, andDecodeSchemaRefChars/TryResolveArrayType/TryResolveCSharpGenericType/TryResolveClassicGenericTypeinReflectorNet/src/Utils/TypeUtils.GetType.cs.Proposed approach
ReflectorNet/src/Utils/TypeUtils.Name.cs->GetTypeId: change the four structural format strings (nested+, generic<>, array[]/[,]) to emit-,( ), and-<rank>.ReflectorNet/src/Utils/Json/JsonSchema.cs->GetSchemaRef: dropEncodeForJsonSchemaRef; emit[Ref] = RefValue + typeIdraw so the key equals the ref.ReflectorNet/src/Utils/TypeUtils.GetType.cs: replaceDecodeSchemaRefChars(percent-decode) with a safe-form -> C#-canonical converter ((-><,)->>,-<digits>->[+commas+],-<ident>->+<ident>) run at the top of eachGetTypeoverload so the existingTryResolveArrayType/TryResolveCSharpGenericType/TryResolveClassicGenericTypeparsers keep working. Mind the assembly-qualified-name edge (a hyphen can appear in assembly names).TestSchemaTypeIdto the new readable forms; replaceTestSchemaRefEncoding's%assertions with assertions that$defskeys AND$refvalues contain none of< > [ ] + %; keepTestGetTypeDecodeSchemaRef's round-trip intent (raw id and safe-form resolve to the same Type) for arrays, generics, nested classes, and the comboIList<Outer+Nested[]>.Acceptance criteria
$defskeys and$refvalues never contain any of< > [ ] + %.IList<Outer+Nested[]>).Related