How-to › Use AI to do the integration

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.

Audience
Platform team
Level
intermediate
Topic
Connect MCP clients to servers
Languages
TypeScript
Verified

Someone adds a server to .cursor/mcp.json and the Claude Code users never see it. Someone else copies .vscode/mcp.json over .mcp.json to fix that, and Claude Code loads zero servers without a word of complaint. Four hand-edited files describe the same three servers in four different ways, and only the last one edited is current.

What you get

You will end up with one source file, a script that renders .mcp.json, .cursor/mcp.json, .vscode/mcp.json, and cline_mcp_settings.json from it, and a CI check that fails when any of the four drifts. This is for you if one repository is opened by more than one editor.

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, Cursor, VS Code, and Cline. No editor was driven by a script here: the loader below reads each file the way its documentation says the editor does.

Approaches compared

ApproachWhen it fitsWhat it costs youWhen to pick something else
A generator script from one source of truthSeveral editors on one repository, secrets that must stay out of git, and a CI job to run it inA script to own, and a renderer to revisit whenever an editor changes its format, which VS Code and Cline have both doneOne editor, or a team that will not run a script before committing
A shared gateway endpointMany servers on many machines, and credentials and tool access to manage in one placeA 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 URLTwo servers and one laptop
Per-editor config filesOne or two servers, one editor, and a file edited twice a yearFour files that part company the first time someone adds a server to one of them, and a copy between editors that loads nothingMore 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 or MetaMCP 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.

{
  "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:

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:

{
  "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:

node demo.mjs
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”:

node demo.mjs
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:

node generate.mjs --out generated --check
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:

node demo.mjs
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.

node --test generate.test.mjs
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 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.

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.

Footnotes

  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. 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. ↩︎ Back to text

  2. VS Code’s configuration reference 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. ↩︎ Back to text

  3. Cline’s MCP page 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 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. ↩︎ Back to text

Read this page as markdown · All how-to guides

Generate the client instead of writing it#

Retries, timeouts, pagination and auth are the same problems in every client. Voxgig generates them from your OpenAPI description, in 23 languages, from one model.

Get the Voxgig dispatch

Short notes on building SDKs, CLIs, REPLs, and MCPs for API-first teams, plus the occasional Fireside episode pick.

By signing up you agree to our Terms and Conditions.