MCP servers
mcps/ connects agents to outside tools and data through the Model Context Protocol: a browser, a database, GitHub, an issue tracker. Each tool keeps servers in its own file and format, such as .mcp.json, .cursor/mcp.json, or [mcp_servers.*] in .codex/config.toml. One YAML file per server feeds all of them.
agnostic-ai compare claude cursor reports the connection fields each tool gets and the references it leaves out, without printing values or connecting to servers.
Write one
agnostic-ai new mcp filesystem creates mcps/filesystem.yaml, plain YAML with no body. Replace its placeholder with:
name: filesystem
description: Local filesystem access for the model.
type: stdio
command: npx
args:
- -y
- "@modelcontextprotocol/server-filesystem"
- /path/to/allowed/dir
Replace the path with a real directory, sync, and check that the server connects in your coding tool. A remote server for two tools, defined but switched off:
name: docs-search
description: Search the internal docs.
type: http
url: https://mcp.example.com/mcp
disabled: true
targets: [claude, codex]
MCP recipes has real servers to copy.
Fields
A server needs command (stdio) or url (remote). Only agnostic-ai lint reports a missing one, or an unknown type (LINT008), so run it before sync.
| Field | Required | Default | Description |
|---|---|---|---|
name | yes | none | Server identifier and key in the generated config, not the filename. Package-style slashes, such as npm:@modelcontextprotocol/server-sequential.thinking, are percent-encoded in filenames and kept in configs. On Windows, import refuses a server whose spec file name would hold a colon, which a name with a colon and no slash does; see path semantics. |
description | no | empty | Free-form notes. Dropped for Codex, Grok, Junie, and Warp. |
type | no | stdio | stdio, http, sse, or ws. local means stdio; streamable-http and remote mean http. Remote types write an explicit type; stdio does not. Augment, Factory, and Qoder get no server for ws, and Ellipsis none for sse or ws. |
command | stdio only | none | Executable to launch. |
args | no | empty | Argument list for the command. An element can hold a reference. |
env | no | empty | Environment variables for the server. Each value is a reference or a plain setting marked !literal. |
url | http/sse/ws only | none | Endpoint URL. It can hold a reference. |
headers | no | empty | HTTP headers for http/sse. Each value is a reference or a plain setting marked !literal. |
cwd | no | empty | Working directory for a stdio server, where the tool supports it. |
timeout | no | empty | Milliseconds on most tools, including Junie. Kiro agent files leave it out with a note; set x-kiro.timeout or x-kiro.requestTimeout there. Crush and Pi read seconds, so sync divides by 1000; Crush rounds up to whole seconds. |
oauth | no | empty | OAuth settings. The format differs by tool. |
disabled | no | false | See disabled support by target. |
roots | no | empty | List of {uri, name} objects, for tools with MCP roots; uri is required. Lint, validate, and sync refuse any other item, such as a plain string (LINT060). Dropped for Codex. |
agent-only | no | false | Keep the server out of every MCP file. Only the agents that name it can reach it. See Agent-only servers. |
Agent-only servers
Set agent-only: true to give a server to some agents and keep its tools out of the main session's context.
# mcps/github.yaml
name: github
agent-only: true
command: npx
args: ["-y", "@modelcontextprotocol/server-github"]
env:
GITHUB_TOKEN: ${GITHUB_TOKEN}
# agents/triager.md frontmatter
mcpServers: [github]
Sync leaves the server out of every project and user MCP file and writes its full definition into each agent file that names it, on Claude Code, Qoder, Kiro, and OpenHands. Every other tool gets no copy, with a note (mcpServers support by target).
- An agent file is committed, so a server written there is checked more strictly: a credential in
url,args, anx-<target>block'senvorheaders, or a${NAME:-default}default is an error (LINT035), even from a pack, instead of the LINT049 warning. - Sync and
validaterefuse anagent-onlyvalue other thantrueorfalse, such asyes. - A server no agent names reaches no tool. Sync prints a note.
- A
disabled: trueserver is left out of the agent file. sync --globalwrites no agent-only server.- Import does not create agent-only servers yet.
import claudekeeps an inline server in the agent'smcpServerslist, and sync writes it back as it was.
Secrets and plain settings
A value in env or headers is a reference by default: ${NAME}, ${NAME:-default}, or $${NAME} text. A header may put one word before it, as in Bearer ${API_KEY}. Mark a plain setting that is not a secret with !literal:
env:
GITHUB_TOKEN: ${GITHUB_TOKEN}
NODE_ENV: !literal production
Sync writes NODE_ENV: production to every tool. An editor with the YAML language server needs !literal scalar in its yaml.customTags setting to accept the tag.
Any other env or headers value, including text around a reference such as postgres://u:pw@${HOST}/db, is LINT035: sync refuses it unless the spec comes from a pack. The message names the server, field, and key, never the value. Empty values, numbers, and booleans are not checked.
A credential in url or args, in an x-<target> block's env or headers, or in a ${NAME:-default} default gets a LINT049 warning, and sync still runs. The check reads the value's shape, since those places take no !literal mark.
agnostic-ai migrate --only secrets rewrites existing specs. A value that looks like a credential becomes a reference, such as GITHUB_TOKEN: ${GITHUB_TOKEN}, and the command lists each variable to set, never the value. Every other value gets !literal. It leaves two cases to you: a key with a credential name whose value does not look like a credential, such as API_KEY: sk-live-abc, and a credential around a reference.
Environment references
Write a token or key as ${NAME}, whole or in part, in an env or headers value, a url, or an args element:
env:
GITHUB_TOKEN: ${GITHUB_TOKEN}
headers:
Authorization: Bearer ${API_KEY}
url: https://${API_HOST}/mcp
args: [--token, "${GH_TOKEN}"]
${NAME:-default} uses default when NAME is unset. Only Claude Code, Crush, OpenHands, and Gemini support that form. Any other ${...}, such as ${env:NAME} or ${input:id}, is not a reference.
Sync writes each tool's own form, and never writes a reference as plain text. Where a tool cannot read a reference in that field, including a ${NAME:-default} outside the four tools above, the note names the server, the key, and the variable, and:
- in
envorheaders, sync leaves the key out, as it does for any other${...}there; - in
urlorargs, sync leaves the whole server out, because dropping one argument would change the command.
| Target | env and headers | url and args |
|---|---|---|
| Claude Code, Crush, OpenHands, Gemini | ${NAME}, ${NAME:-default} | ${NAME}, ${NAME:-default} |
| Cursor, Windsurf | ${env:NAME} | ${env:NAME} |
| OpenCode | {env:NAME} | {env:NAME} |
| Continue | ${{ secrets.NAME }}, in env on stdio servers and under requestOptions.headers | ${{ secrets.NAME }} |
| Amp | headers: ${NAME}. env: key left out | url: ${NAME}. args: server left out |
| Factory, Kiro, Ellipsis | ${NAME} | Server left out |
| Codex | env: KEY: ${KEY} adds KEY to env_vars. headers: Authorization: Bearer ${NAME} sets bearer_token_env_var, and another header of exactly ${NAME} joins env_http_headers. Any other reference: key left out | Server left out |
| Antigravity, Augment, Copilot, Junie, Kilo Code, Qoder, Trae, Warp, Zed | Key left out | Server left out |
Kiro expands only variables approved under Mcp Approved Env Vars in its settings. Factory fails the connection when a referenced variable is unset. Continue's IDE extensions read secrets from project .env, .continue/.env, or ~/.continue/.env, and its CLI also reads environment variables (secret resolution). Keep .env files out of Git.
agnostic-ai doctor lists each referenced variable unset in your shell, including in x-<target> blocks and disabled servers. A tool started from a desktop launcher may see a different environment.
References in url and args
url and args take the same references, with the stricter rule above: a tool that cannot read one there gets no server.
- A tool's own form at the top level, such as
${env:NAME}, is copied as text to every other tool, andlintreports it as LINT028. Write${NAME}, or put the tool's own text underx-<target>:. ${workspaceFolder},${workspaceFolderBasename},${userHome},${pathSeparator}, and any other${...}, such as${input:id}, are not references and stay as written.
Literal ${NAME} text
Some servers expand ${NAME} themselves, such as mcp-remote in a header argument. Write $${NAME} to pass the text on unchanged. It works in url, args, env, and headers:
command: npx
args: ["-y", "mcp-remote", "https://mcp.example.com", "--header", "x-chroma-token: $${X_CHROMA_TOKEN}"]
Sync writes ${X_CHROMA_TOKEN} to every built-in tool, with no note; an external adapter gets the spec as written. Quote it inside a [...] list, as you would ${NAME}.
A tool whose column in the table above shows ${NAME} for that field expands the text itself, from its own environment rather than the server's env block, before the server sees it. None of them can write a literal ${, so sync still writes the server. A value expanded in args shows in the process list. When the variable is unset and has no default, Claude Code keeps the ${NAME} text and warns in claude mcp list.
What import writes
import turns native references and literal credentials into ${NAME} references, and lists each variable to set. MCP import credentials describes the rewrite rules, credential names, and detection limits.
Credential names
Target-only fields
| Target | Extra fields |
|---|---|
| Codex | env_vars, env_http_headers, http_headers_helper, auth, required, startup_timeout_sec, startup_timeout_ms, tool_timeout_sec, default_tools_approval_mode, scopes, oauth_resource, environment_id, enabled_tools, disabled_tools |
| Crush | enabled_tools, disabled_tools, sessionless, channel_enabled, channel_reply |
| Gemini | trust, includeTools, excludeTools |
| Qoder | trust, includeTools, excludeTools, alwaysAllow |
| Kiro | autoApprove, disabledTools, oauthScopes |
| Factory | x-factory.enabledTools, disabledTools, connectTimeout |
| Windsurf / Devin Desktop | disabledTools, oauthScopes (remote servers) |
| Claude Code | alwaysLoad, bareElicitationCapability, headersHelper |
| Cursor | envFile, auth |
| Copilot / VS Code | envFile, dev, sandboxEnabled and sandbox (VS Code file only), tools (Copilot CLI files only) |
| Continue | connectionTimeout, requestOptions |
| OpenHands | auth: oauth, or a truthy oauth, which sets auth: "oauth" on a remote server in an agent's mcp_servers |
Codex environment_id is the ID of a configured executor and defaults to local. The older experimental_environment still loads: sync writes local as environment_id = "local" and leaves remote out, since Codex no longer reads that key. lint and project sync say so. agnostic-ai migrate --only mcps rewrites local.
The Crush and Claude Code fields also work under x-crush or x-claude, which wins, and other tools leave them out. On Amp, set x-amp.includeTools. x-factory, x-kilo, and x-continue override the matching top-level options for that target. Each target page gives the details: Factory tool lists, Crush channels, and Claude Code flags.
disabled support by target
This table covers project files. For user files written by sync --global, see global MCP servers.
| Target | Behavior |
|---|---|
| Antigravity, Crush, Factory, Kiro, Qoder, Windsurf | Native disabled |
| Codex | Mapped to enabled = false |
| Kilo Code, OpenCode, Zed | Mapped to "enabled": false |
| Copilot | A disabledMcpServers entry in .github/copilot/settings.json for Copilot CLI. The key is removed from both MCP files with a note. Disable the server in VS Code yourself |
| Claude Code | Mapped to project disabledMcpjsonServers in .claude/settings.json for servers written to .mcp.json. Import restores it |
| Cursor, Augment, Junie, Trae, Warp | The key is removed with a note, and the server stays in the file. Disable the server in the tool itself; Warp starts a project server only when you start it |
| Ellipsis | Server left out, since a listed server is always available. Written into the environment file, not an MCP file |