# mcp-valid-json MCP configuration must use valid syntax and a host-readable server structure | | | |---|---| | **Severity** | error | | **Autofix** | - | | **Since** | v0.1.0 | | **Category** | [MCP (Model Context Protocol)](mcp.md) | ## Why MCP (Model Context Protocol) configuration must use syntax and a server map the host can actually read. Standalone files and manifests use JSON; GitHub Copilot custom agents embed the same server shape in YAML frontmatter. Invalid syntax or the wrong structure means no MCP servers load, and tools that depend on them silently fail. ## Which key, which file The server map has two spellings, and each host reads exactly one: | File | Key | Wrapper required? | | --- | --- | --- | | `.mcp.json` | `mcpServers` | No — see below | | plugin manifests | `mcpServers` | Yes | | `.cursor/mcp.json` | `mcpServers` | Yes | | `.vscode/mcp.json` | `servers` | Yes | | `opencode.json`, `opencode.jsonc` | `mcp`, or `mcp.servers` in 2.0 | Yes | | `.github/agents/**/*.md` (cloud or shared) | `mcp-servers` | Yes, in YAML frontmatter | | `mcp_config.json` (Google Antigravity) | `mcpServers` | Yes | A file using the other host's key is reported as such — the servers are present but will not load. VS Code's documented siblings `inputs` and `sandbox` are not servers and are left alone. OpenCode accepts both of its own layouts at once and this rule reads both, but the *shape* of an OpenCode server is checked elsewhere — see below. GitHub Copilot custom-agent YAML accepts `type: local` as the local-process spelling of `stdio`; both require a non-empty string `command`. Ecosystem-specific MCP schemas are validated by their dedicated rules: - Agent Plugins `mcp.json` is validated by [`agent-plugin-mcp-valid`](agent-plugin-mcp-valid.md). - OpenCode `opencode.json` configuration is validated by [`opencode-config-valid`](opencode-config-valid.md). - Google Antigravity `mcp_config.json` is validated by [`antigravity-mcp-valid`](antigravity-mcp-valid.md), whose dialect spells a remote server `serverUrl` and accepts a server with no connection field at all. A standalone `.mcp.json` accepts a **wrapperless** map as well: a file whose top level is the server map itself, with no `mcpServers` key, is valid and is not reported. Everywhere else the wrapper is the only form. Cursor and VS Code document one shape each, so a bare map there loads nothing and is reported; in a plugin manifest the servers live under the manifest's own `mcpServers` field, and bare keys beside it are ordinary manifest data that neither the host nor this rule reads as servers. ```json {"my-server": {"command": "node", "args": ["server.js"]}} ``` Transport is inferred when a server does not declare `type`: a `command` means stdio, and a bare `url` means a remote server. Declaring `type` explicitly overrides the inference, and an unknown value is reported. GitHub Copilot agent YAML may spell the local transport `local` instead. Credential values do not belong in committed MCP configuration. The rule reports structured secrets in `env` and `headers` mappings and rejects URL userinfo such as `https://user:token@example.com` without copying the credential into its diagnostic. Use host-supported environment substitution or a clearly recognizable placeholder instead. ## Examples **Bad** — an unknown transport, which no host can connect over: ```json {"mcpServers": {"my-server": {"type": "gopher", "command": "x"}}} ``` An empty `command` is a narrower case: the key is present, so the presence-only check passes it in a Claude-family file. The editor files (`.cursor/mcp.json`, `.vscode/mcp.json`), Codex-only plugins, and every Grok plugin surface require the value to name something spawnable: a non-empty `command` string or `url`. Grok's surfaces are its manifest's `mcpServers` path and inline map, which only Grok reads whatever else claims the directory, and a Grok-only plugin's conventional `.mcp.json`. **Good:** ```json { "mcpServers": { "my-server": { "command": "npx", "args": ["my-server"] } } } ``` **Good in a GitHub Copilot custom agent:** ```yaml --- description: Reviews changes using repository metadata mcp-servers: repository: type: local command: node args: [scripts/repository-server.js] --- ``` ## How to fix Fix the JSON or YAML syntax error, or move the servers under the key this host reads (see the table above). Each stdio/local server needs a `command` and each remote server a `url`; when `type` is declared, the field must match it. Inside an OpenAI Codex-only plugin (Codex-claimed, with neither a `.claude-plugin` marker nor a Claude marketplace listing — either one counts as a Claude declaration), `command` and `url` must also be **non-empty strings** — Codex resolves servers through the manifest, and an empty value produces a server that silently never starts. A plugin that ships both manifests keeps the Claude requirements on its conventional `.mcp.json`, where presence alone satisfies the rule. Grok Build requires the same, and on one more file: a path its manifest names in `mcpServers` is Grok's whatever else claims the directory, so a dual-manifest plugin's declared file is held to it too. Grok also refuses a document carrying a duplicated key or a bare `NaN`/`Infinity`, which is reported as invalid JSON rather than as a field's type. Avoid naming a server after one of Claude Code's built-in servers (`workspace`, `claude-in-chrome`, `computer-use`, `Claude Preview`, `Claude Browser`) — those names are reserved and a user server that shadows one is ignored. The reserved names apply wherever Claude Code reads the file, which includes the repository-root `.mcp.json` of a repository that is also a Grok plugin. ## Configuration ```yaml rules: mcp-valid-json: enabled: true # true | false | auto severity: error ``` | Parameter | Description | Default | |-----------|-------------|---------| | `additional-placeholders` | Extra case-insensitive substrings that mark a generic credential value as a placeholder (suppressing the violation) | `[]` | *Run `skillsaw explain mcp-valid-json` to see this documentation and the rule's effective configuration in your terminal.*