MCP tool arguments with array-or-string union schema (anyOf) are stringified before reaching the server
- Lenguaje dominante
- Shell
- Estrellas
- 11.2k
- Forks
- 1.9k
- Merge medio
- 14 h 16 min
- PR fusionados (30 d)
- 6
Descripción
### Describe the bug
When an MCP tool parameter's JSON Schema declares a union that includes both `array` and `string` (e.g. `anyOf: [array, string]` or a Zod `z.union([z.array(z.string()), z.string(), z.null()])`), Copilot CLI's MCP client flattens/stringifies the argument before sending it to the server -- even when a genuine JSON array is supplied. The MCP server (which correctly expects and validates an array) then rejects the call, because it receives a bare string instead.
This reproduces against the Obsidian Local REST API plugin's MCP server (`obsidian-mcp`), specifically its `vault_patch` and `vault_read` tools' `target` parameter, used for heading-path addressing (e.g. `["Parent Heading", "Child Heading"]`).
### Affected version
GitHub Copilot CLI 1.0.76 (Windows)
### Steps to reproduce the behavior
1. Register an MCP server exposing a tool whose parameter schema is `z.union([z.array(z.string()), z.string(), z.null()])` (or the JSON-Schema equivalent `anyOf: [{type: array, items: {type: string}}, {type: string}, {type: null}]`).
2. From Copilot CLI, call that tool with a genuine array value, e.g. `target: ["Top", "Child"]`.
3. Observe the server rejects the call as if a bare string were sent (e.g. `A heading target must be an array of heading texts, not a bare string`), even though the client was given a proper array.
4. Direct/bypass test: calling the identical tool with the identical array directly against the server's `/mcp` JSON-RPC endpoint (bypassing the Copilot CLI client) succeeds and returns the correct result -- confirming the server is correct and the array is valid.
### Root cause (isolated via a controlled A/B test)
We edited the MCP server's own schema for the affected parameter, removing the `string` option so the union became `array | null` only (no code changes elsewhere; identical Copilot CLI client, identical server, identical call). With that one change:
- The exact same array argument (`["Top", "Child"]`) that previously failed now succeeded and correctly resolved a two-level nested heading.
- A single-element array (`["Top"]`) also succeeded.
This isolates the bug precisely: **Copilot CLI's MCP client only mis-serializes array arguments when the parameter's declared schema is a union that also includes `string`.** Parameters that are plain/unconstrained JSON (no such union), or presumably schemas of `array | null` with no `string` sibling, pass arrays through untouched -- we independently confirmed a plain JSON-typed parameter (no union) on the same server correctly accepted and round-tripped a real array in the same session.
### Reproduction table
| Channel | Argument | Parameter schema | Result |
|---|---|---|---|
| Copilot CLI MCP tool call | `target: ["Top","Child"]` | `anyOf: [array, string, null]` | Fails -- arrives as a stringified value, rejected as "not a bare string" |
| Copilot CLI MCP tool call | `target: ["Top"]` | `anyOf: [array, string, null]` | Fails -- same error |
| Copilot CLI MCP tool call | `target: []` (empty array) | `anyOf: [array, string, null]` | Fails -- same error |
| Copilot CLI MCP tool call | `target: null` | `anyOf: [array, string, null]` | Fails -- `null` also arrives stringified |
| Direct `/mcp` JSON-RPC call (bypassing Copilot CLI) | `target: ["Top","Child"]` (real array) | same schema | Succeeds |
| Copilot CLI MCP tool call, schema patched to remove `string` | `target: ["Top","Child"]` | `union([array, null])` | Succeeds -- correct nested-heading resolution |
| Copilot CLI MCP tool call, schema patched to remove `string` | `target: ["Top"]` | `union([array, null])` | Succeeds |
| Copilot CLI MCP tool call | plain JSON-typed parameter (no union), given `["a","b"]` | no union constraint | Succeeds -- array passed through untouched |
### Expected behavior
A genuine JSON array argument should be transmitted to the MCP server as an array, regardless of whether the parameter's schema also permits a `string` (or other) alternative in the same union/`anyOf`.
### Additional context
- This is likely related to #2634, which reports Copilot CLI's MCP tool-schema handling losing/narrowing fields and unions (including an `anyOf` mixing `array` and `object`) between what an MCP server advertises and what the model/client actually uses. That report is about schema-definition loss at tool-discovery time; this report is about argument-*value* corruption at call time for a union schema. Both point at the same general area of Copilot CLI's MCP layer mishandling `anyOf`/union types that include `array`, so they may share a root cause.
- This is scoped narrowly: it is **not** "Copilot CLI can't send array arguments" -- arrays work fine for parameters with a plain array (or non-union) schema. The bug is specific to a schema union that pairs `array` with `string` (and possibly other non-array types).
Guía de contribución
Línea de trabajo
Comienza en la ruta del cliente MCP de Copilot CLI que convierte los esquemas de herramientas anunciados en argumentos de llamada, usando la reproducción documentada de /mcp JSON-RPC para comparar las solicitudes directas y las mediadas por CLI. Verifica que los valores de array y null sigan siendo valores tipados cuando anyOf incluye array, string y null, mientras que los casos existentes de arrays simples y sin unión continúen funcionando.
Escrito por el modelo de indexación a partir del texto del issue.
Evaluación
- Stack tecnológico
- shell
- Área
- api, cli
- Tipo de issue
- Error
- Dificultad
- 4/5
- Tiempo estimado
- 3-5 días
- Estado de actividad
- Tranquilo
- Claridad
- Bien especificado
- Aptitud para principiantes
- 67/100