# How to share one MCP server config across several editors

> Keep Claude Code, Cursor, VS Code, and Cline on the same MCP servers by rendering four editor files from one source, and see why a copied file loads zero servers.

Source: https://voxgig.com/howto/share-one-mcp-config-across-editors

- Audience: platform-team
- Level: intermediate
- Languages: typescript
- Verified: 2026-09-25
- Published: 2026-09-25

## Short answer

Keep one source file that lists every server and every secret, and render the four editor files from it with a script that CI runs as a drift check. Each editor wants a different shape: Claude Code, Cursor, and Cline read `mcpServers`, VS Code reads `servers` with typed `inputs`, and each refers to a secret in its own syntax. A file copied from one editor to another loads silently with zero servers or the wrong transport.

---
## You will need

Node 22 or later, and a repository that at least two editors open. Verified 2026-09-25
against Node 22.22.2. The four file formats come from each editor's own documentation:
[Claude Code](https://code.claude.com/docs/en/mcp), [Cursor](https://cursor.com/docs/mcp),
[VS Code](https://code.visualstudio.com/docs/agents/reference/mcp-configuration), and
[Cline](https://docs.cline.bot/mcp/mcp-overview). No editor was driven by a script here: the
loader below reads each file the way its documentation says the editor does.

## Approaches compared

| Approach | When it fits | What it costs you | When to pick something else |
| --- | --- | --- | --- |
| [A generator script from one source of truth](https://code.claude.com/docs/en/mcp#environment-variable-expansion-in-mcp-json) | Several editors on one repository, secrets that must stay out of git, and a CI job to run it in | A script to own, and a renderer to revisit whenever an editor changes its format, which VS Code and Cline have both done | One editor, or a team that will not run a script before committing |
| [A shared gateway endpoint](https://docs.docker.com/ai/mcp-catalog-and-toolkit/mcp-gateway/) | Many servers on many machines, and credentials and tool access to manage in one place | A service to run and keep up, an extra hop on every tool call, and the per-editor controls such as VS Code inputs and Cline auto-approval flattened behind one URL | Two servers and one laptop |
| [Per-editor config files](https://code.visualstudio.com/docs/agents/reference/mcp-configuration) | One or two servers, one editor, and a file edited twice a year | Four files that part company the first time someone adds a server to one of them, and a copy between editors that loads nothing | More than one editor opens the repository |

The generator keeps each editor's own features, VS Code's password inputs and Cline's
auto-approval list among them, and in exchange it is code that has to follow four formats.
A gateway such as the [Docker MCP Gateway](https://docs.docker.com/ai/mcp-catalog-and-toolkit/mcp-gateway/)
or [MetaMCP](https://docs.metamcp.com/en/concepts/endpoints) is one URL in every editor and
one place credentials live. Every tool call pays for the hop, and the gateway is one more
service someone runs. Per-editor files cost nothing until the second editor arrives.

## Start from one source file

The source is not any editor's format. It names each server, its transport, and the
placeholders for secrets, and it lists the secrets so a renderer can refuse a placeholder
nobody declared.

```json title="mcp-servers.json"
{
  "servers": {
    "acme-docs": {
      "transport": "http",
      "url": "https://docs.example.com/mcp",
      "headers": { "Authorization": "Bearer ${DOCS_TOKEN}" }
    },
    "meters": {
      "transport": "stdio",
      "command": "node",
      "args": ["tools/meter-mcp.mjs", "--read-only"],
      "env": { "METERS_API_KEY": "${METERS_API_KEY}" }
    }
  },
  "secrets": {
    "DOCS_TOKEN": "Token for the Acme documentation server",
    "METERS_API_KEY": "API key for the meters service, read-only account"
  }
}
```

`${DOCS_TOKEN}` is a placeholder, never a value. The source is committed, so a token in it is
a token in git history. Each renderer turns the placeholder into whatever its editor uses to
read a secret at run time.

## Render each editor's file in its own shape

The four files look interchangeable and are not. Claude Code reads `mcpServers`, wants
`"type": "http"` on a remote entry, and expands `${VAR}` in `command`, `args`, `env`, `url`,
and `headers`; its documentation says an entry with a `url` and no `type` is skipped and
reported.[^1] Cursor reads `mcpServers`, takes a bare `url` for a remote server, and
interpolates `${env:NAME}`. VS Code reads a top-level `servers` object, a `type` on each
entry, and an `inputs` array that turns a secret into a prompt the editor shows once and
stores.[^2] Cline reads `mcpServers`, needs `"type": "streamableHttp"` because an entry with
no `type` is read as the legacy SSE transport, carries `disabled` and `autoApprove`, and
documents no variable expansion at all.[^3]

Each renderer is a few lines. VS Code's is the longest, because the secrets become a second
top-level section:

```ts title="generate.mjs"
function renderVsCode(src) {
  const inputs = Object.entries(src.secrets ?? {}).map(([name, description]) => ({ type: 'promptString', id: inputId(name), description, password: true }))
  const servers = {}
  for (const [name, s] of Object.entries(src.servers)) {
    servers[name] = substitute(s.transport === 'stdio' ? { type: 'stdio', ...stdioFields(s) } : { type: 'http', ...httpFields(s) }, (v) => `\${input:${inputId(v)}}`)
  }
  return { inputs, servers }
}
```

What it writes:

```json title="generated/.vscode/mcp.json"
{
  "inputs": [
    {
      "type": "promptString",
      "id": "docs-token",
      "description": "Token for the Acme documentation server",
      "password": true
    },
    {
      "type": "promptString",
      "id": "meters-api-key",
      "description": "API key for the meters service, read-only account",
      "password": true
    }
  ],
  "servers": {
    "acme-docs": {
      "type": "http",
      "url": "https://docs.example.com/mcp",
      "headers": {
        "Authorization": "Bearer ${input:docs-token}"
      }
    },
    "meters": {
      "type": "stdio",
      "command": "node",
      "args": [
        "tools/meter-mcp.mjs",
        "--read-only"
      ],
      "env": {
        "METERS_API_KEY": "${input:meters-api-key}"
      }
    }
  }
}
```

The demo renders all four into a scratch directory and then reads each one back as its
editor would:

```bash
node demo.mjs
```

```text output
rendered 4 files from mcp-servers.json, and what each editor loads
  Claude Code  .mcp.json                2 servers: acme-docs http, meters stdio
  Cursor       .cursor/mcp.json         2 servers: acme-docs remote, meters stdio
  VS Code      .vscode/mcp.json         2 servers: acme-docs http, meters stdio
  Cline        cline_mcp_settings.json  2 servers: acme-docs streamable http (fill in by hand: DOCS_TOKEN), meters stdio (fill in by hand: METERS_API_KEY)
```

The Cline line is the one cost the generator cannot remove. With no documented expansion
syntax, its file has to carry either the placeholder or the value. The loader reports the
placeholder, and someone types the value into the editor's own settings once.

## Copy a file between editors and watch what loads

The same demo then copies each rendered file into another editor's place, which is what a
person does when a colleague's editor "just needs the same servers":

```bash
node demo.mjs
```

```text output
the same files copied between editors
  .vscode/mcp.json         as Claude Code  .mcp.json                0 servers
  .cursor/mcp.json         as Claude Code  .mcp.json                1 servers: meters stdio; acme-docs: has a url but no type; skipped
  .cursor/mcp.json         as Cline        cline_mcp_settings.json  2 servers: acme-docs sse (legacy), meters stdio
  .mcp.json                as VS Code      .vscode/mcp.json         0 servers
```

Each line is a different failure. The VS Code file under Claude Code's name has no
`mcpServers` key, so it is a valid JSON file describing no servers, and no error is the
correct behavior. The Cursor file under Claude Code's name loses its remote server, because
`url` without `type` is the documented configuration error. The Cursor file under Cline's
name keeps both servers and connects the remote one over the deprecated transport, which a
Streamable HTTP server answers with a 405. The Claude Code file under VS Code's name is the
first line in reverse.

## Run the drift check in CI

The committed files are outputs, and outputs drift the first time someone edits one by
hand. `--check` renders the source and compares it with what is on disk, byte for byte:

```bash
node generate.mjs --out generated --check
```

```text output
4 files match the source
```

Run that on every pull request, and run `node generate.mjs` to regenerate after a change to
the source. The last part of the demo edits one rendered file by hand and runs the check
again:

```bash
node demo.mjs
```

```text output
the drift check, before and after a hand edit
  all four files match the source
  .cursor/mcp.json: differs from what the source renders
```

The check names the file and nothing else, which is enough: the fix is always to edit the
source and regenerate, never to edit the file.

## Check it worked

Ten tests hold each renderer to its editor's documented format and each loader to the
documented behavior. The literal to look for is `acme-docs streamable http` on the Cline line, which
means the remote entry carries the `type` the legacy default would otherwise swallow.

```bash
node --test generate.test.mjs
```

```text output
1..10
# tests 10
# suites 0
# pass 10
# fail 0
# cancelled 0
# skipped 0
# todo 0
# duration_ms 175.040672
```

One test asserts that no rendered file contains a secret value and that each carries its own
reference syntax, `${DOCS_TOKEN}`, `${env:DOCS_TOKEN}`, and `${input:docs-token}`. Another
copies the VS Code file to Claude Code's name and asserts zero servers and zero problems,
because a check that only fails on errors would never see that case. A third runs `--check`
from a directory with a space in its path and expects exit status 1. A script that compared
its own path with a URL there would skip its main block and exit 0, having checked nothing.

## When it goes wrong

Claude Code lists the server with a warning about a missing variable. The `${VAR}` in the
rendered file names an environment variable that is not set on that machine. The
[documentation](https://code.claude.com/docs/en/mcp#environment-variable-expansion-in-mcp-json)
says the config still loads with the text unexpanded, so set the variable on that machine.
The source cannot carry Claude Code's `${VAR:-default}` instead: Cursor and VS Code have no
form for it, so the generator refuses it rather than write it into their files as text.

Claude Code shows the server as pending approval. Project-scoped servers in `.mcp.json` need
a one-time approval in an interactive session, and `claude mcp reset-project-choices` clears
earlier answers.

VS Code prompts for the token on every start. The input's `id` in `inputs` does not match
the `${input:id}` in the server entry, so the stored value is never found. The loader reports
this as an undefined input; the generator derives both from the same secret name so they
cannot differ.

Cline connects and every tool call fails. The remote entry has no `type`, so Cline used the
legacy SSE transport against a Streamable HTTP server. Regenerate; the renderer always
writes `streamableHttp`.

The drift check fails on a machine where nothing changed. Line endings or a formatter
rewrote the file. The check compares bytes, so commit the generator's output untouched and
exclude the four files from any formatter that runs on JSON.

## When not to do this

Do not render a file for an editor nobody on the team uses. Each target is a format to
track, and Cline's has already moved once, from a bare `url` to a typed one. Delete a target
the day its last user leaves.

Do not put a token in the source file to make the Cline file complete. The source is
committed, and a value there is in every clone. Leave the placeholder, and let the loader's
"fill in by hand" line be the reminder it is.

Do not run a gateway to solve a four-file problem. A gateway earns its hop when the servers
outnumber the machines and credentials need one owner, not when two developers want the
same three servers. Start with the generator, and add the gateway when the operating cost of
the servers themselves is the problem.

Do not hand-edit a rendered file, even to try something. The check will fail, and the
teammate who sees it fail will regenerate and lose your edit. Change the source, regenerate,
and commit both together.

## Related how-tos

- [Connect a remote MCP server to Claude Desktop](/howto/connect-a-remote-mcp-server-to-claude-desktop)

- [Choose between stdio and Streamable HTTP for an MCP server](/howto/choose-an-mcp-transport)

- [Let a coding agent probe a live API through an MCP server](/howto/probe-live-api-responses-through-an-mcp-server)

## Last verified

Verified 2026-09-25 against Node 22.22.2. Every output block is what the command preceding
it printed. No editor was run. The four loaders follow the documented behavior of each
editor, and the copies in the output are what those loaders make of a file written for
another editor.

[^1]: The Claude Code documentation names the exact message for a `url` without a `type`.
It adds that the `type` field takes `streamable-http` as an alias for `http`, so that
[configurations copied from server documentation work without modification](https://code.claude.com/docs/en/mcp).
The protocol calls the transport Streamable HTTP, the editor calls it `http`, and the alias
exists because a copied block travels further than either name.

[^2]: VS Code's
[configuration reference](https://code.visualstudio.com/docs/agents/reference/mcp-configuration)
describes two formats and reads both: its own, `.vscode/mcp.json` with a `servers` object,
and what it calls the portable format, `.mcp.json` with `mcpServers`. The editor with the odd
shape is the one editor here that also reads the common one. That leaves a workspace with
two possible files and one word, portable, to say which of them the other editors will
understand.

[^3]: Cline's [MCP page](https://docs.cline.bot/mcp/mcp-overview) says that omitting
`type` "defaults to the legacy `sse` transport for backward compatibility", and in the same
sentence tells you to set `streamableHttp` explicitly. The MCP specification's
[deprecated features registry](https://modelcontextprotocol.io/specification/2026-07-28/deprecated)
lists that transport for removal. A default is a decision made for everyone who did not
make one, and here it points at the transport the protocol is retiring.