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) |
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.
- OpenCode opencode.json configuration is validated by opencode-config-valid.
- Google Antigravity mcp_config.json is validated by antigravity-mcp-valid, 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.
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:
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:
Good in a GitHub Copilot custom agent:
---
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¶
| 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.