@mcp-layer/config
v1.0.1
Published
Discover, normalize, and update MCP server configurations across popular tools.
Maintainers
Readme
@mcp-layer/config
@mcp-layer/config discovers MCP server configuration files produced by supported clients and normalizes their contents into a single, predictable structure. It is the entry point for finding and updating MCP server definitions across popular tools.
MCP server configuration is fragmented across tools, file formats, and locations. This package gives you one API that:
- Finds those files reliably.
- Parses each tool's schema correctly.
- Normalizes everything into
{ servers, metadata }and aConfigmap keyed by server name. - Lets you update or remove entries while preserving the host tool's expected format.
Table of Contents
- Installation
- Core concepts
- Connectors (discovery + parsing)
- Usage
- API (authoritative)
- Validation rules
- Responsibilities & lifecycle
- Testing
- Security & safety
- Runtime Error Reference
Installation
pnpm add @mcp-layer/config
# or
npm install @mcp-layer/config
# or
yarn add @mcp-layer/configCore concepts
- Connector: A tool-specific adapter that knows where configs live and how to parse/write them.
- Server entry:
{ name, config }, whereconfigincludescommand,args,cwd,env,url,endpoint, etc. - Metadata: Tool-specific extras like VS Code
inputsor Cline flags. - Config: The normalized container returned by
load. It exposes:.map(Map of server entries by name).list(discovered files + connector metadata).get(name)convenience method.add(...)/.remove(name)to update the original file
Connectors (discovery + parsing)
Each connector exposes (and is accessible via @mcp-layer/config/<connector-name>):
project(dir)/home(ctx)discoveryparse(raw, file)returning{ servers, metadata }write(...)helpers used byConfig.addto preserve formatting and metadata
Supported connectors:
- Claude Code -- project
.mcp.json, user~/.mcp.json(including~expansion), managed enterprise files on macOS (/Library/Application Support/ClaudeCode/managed-mcp.json), Windows (C:/ProgramData/ClaudeCode/managed-mcp.json), and Linux (/etc/claude-code/managed-mcp.json), plus explicit overrides viaMCP_CONFIG_PATH. Parses JSONmcpServersblocks. - Cursor --
.cursor/mcp.jsonin workspace ancestry + user home. Parses JSONmcpServersblocks. - Codex --
config.tomlunder${CODEX_HOME}or~/.codex/. Parses[mcp_servers.*]TOML tables. - VS Code --
.vscode/mcp.json, workspacemcp.json, and~/.vscode/mcp.json(including Insiders/VSCodium variants). Parses JSONserversarrays and preservesinputsmetadata. - Cline -- user-level
cline_mcp_settings.json(auto-discovered in VS Code / VSCodium storage, overridable viaCLINE_MCP_SETTINGS_PATH). ParsesmcpServersand Cline flags. - Gemini CLI --
.gemini/settings.jsonunder workspace ancestry and user home. - Windsurf --
~/.codeium/windsurf/mcp_config.json. - Claude Desktop --
~/.claude/settings.jsonand platform-specificclaude_desktop_config.json(macOS~/Library/Application Support/Claude/, Windows%APPDATA%/Claude/). - Neovim --
~/.config/nvim/mcp.json. - Helix --
~/.config/helix/mcp.json. - Zed --
~/.config/zed/mcp.json. - Generic -- globs for
mcp*.json,*.mcp.json,mcp*.yaml,*.mcp.yamlin ancestor directories and fallback user paths like~/.config/mcp/servers.jsonor~/.config/mcp.(json|yaml). Parses JSON/YAML formcpServers,servers, or top-level maps and preserves shared metadata likeinputsanddefaultMode.
Usage
Load discovered configuration
import { load } from '@mcp-layer/config';
const config = await load(undefined, process.cwd());
const server = config.get('demo');Load inline configuration (no discovery)
const config = await load(
{
servers: {
manual: {
command: '/usr/local/bin/manual-server',
args: ['--flag']
}
},
inputs: [{ id: 'token', type: 'promptString', password: true }]
},
'/virtual/config'
);Update or remove servers
await config.add(
{
name: 'new-server',
config: {
command: '/usr/local/bin/new-server',
args: ['--stdio']
}
},
{
connector: 'claude-code',
scope: 'project'
}
);
await config.remove('new-server');API (authoritative)
load(doc?, optionsOrStart?)
Loads configuration and returns a Config instance.
doc(optional): inline config object. When present, discovery is skipped.optionsOrStart: either a string start path or an options object:start: directory to begin upward search (defaults toprocess.cwd())homeDir: override for the user home directoryenv: environment variables (defaults toprocess.env)platform: operating system (process.platformwhen omitted)connector: (inline documents only) connector name to associate with the document
locate(options)
Returns the list of discovered configuration files with connector metadata.
Config methods
get(name)-- returns a server entry by name.add(entry, options)-- add or update a server entry in the appropriate file.remove(name)-- remove a server entry from its original file.
add options:
connector(required for new servers) -- connector name to use for a new entry.scope--'project' | 'home'when both scopes are supported.file-- explicit file path override if discovery did not cover the desired target.metadata-- tool-specific metadata to persist (ex: VS Codeinputs).
Validation rules
Every server entry must declare at least one of:
command(stdio transport)url(HTTP transport)endpoint(HTTP transport)
Invalid entries are rejected during parsing so downstream tooling only receives runnable definitions.
Responsibilities & lifecycle
- This package does not open transports or connect to servers.
- It is responsible only for discovery, parsing, normalization, and updates.
- Use
@mcp-layer/connectfor live connections and@mcp-layer/schemato extract schemas.
Testing
pnpm test --filter @mcp-layer/configSecurity & safety
- Never commit real MCP credentials or server binaries; use
.env.examplewhere needed. - Prefer
MCP_CONFIG_PATHfor temporary overrides rather than mutating user files.
Runtime Error Reference
This section is written for high-pressure debugging moments. Each entry maps to a concrete parser/connector path in @mcp-layer/config.
Failed to parse JSON file "{file}": {reason}
Thrown from: cline.parse
This happens when Cline connector parsing fails for cline_mcp_settings.json (usually in VS Code global storage or CLINE_MCP_SETTINGS_PATH). Invalid JSON syntax is the common cause.
Step-by-step resolution:
- Confirm which Cline settings file was loaded (
CLINE_MCP_SETTINGS_PATHor VS Code global storage path). - Validate that exact file as strict JSON (no comments, no trailing commas).
- Ensure the file contains a top-level object (typically with
mcpServers). - Re-run config load and add a fixture with malformed JSON to keep parser failure coverage.
const raw = await fs.readFile(file, 'utf8');
JSON.parse(raw); // throws immediately with concrete syntax location
const config = await load(undefined, process.cwd());
console.log(config.map.size);Failed to parse TOML file "{file}": {reason}
Thrown from: codex.parse
This happens when Codex connector parsing fails for config.toml (usually under $CODEX_HOME or ~/.codex). Invalid TOML syntax or malformed [mcp_servers] blocks are the usual cause.
Step-by-step resolution:
- Confirm the resolved Codex config path (
$CODEX_HOME/config.tomlif set). - Validate TOML syntax with a parser before loading config.
- Verify server definitions are nested under
mcp_servers. - Add a failing fixture for malformed TOML and a passing fixture for the corrected document.
import { parse as parseToml } from '@iarna/toml';
const raw = await fs.readFile(file, 'utf8');
parseToml(raw);
const config = await load(undefined, process.cwd());
console.log(config.map.size);Connector "{connector}" does not support write operations.
Thrown from: Config.add
This happens when Config.add(...) resolved a connector name but that connector does not provide a write(file, entry, metadata) function. Add/remove paths require writable connectors.
Step-by-step resolution:
- Confirm the connector name passed in
options.connector. - Check the connector definition and verify it implements
write. - Switch to a writable connector (
vscode,codex,cline, etc.) or addwriteto your custom connector. - Add tests that
addfails on read-only connectors and succeeds on writable ones.
await config.add(
{ name: 'local-dev', config: { command: 'node', args: ['./server.js'] } },
{ connector: 'vscode', file: '/workspace/.vscode/mcp.json' }
);A connector is required before adding server "{server}".
Thrown from: Config.add
This happens when Config.add(...) cannot infer which connector should write the server entry. For new servers, you must pass options.connector.
Step-by-step resolution:
- Provide
options.connectorexplicitly when adding a new server. - If updating an existing server, verify it already has connector metadata.
- Keep connector names aligned with
findConnector(...)registrations. - Add tests for "missing connector" and "valid connector" add scenarios.
await config.add(
{ name: 'integration', config: { command: 'node', args: ['./mcp.js'] } },
{ connector: 'codex' }
);A file path is required before adding server "{server}".
Thrown from: Config.add
This happens when Config.add(...) has a connector but no target file path can be derived. It could not resolve options.file, existing server source, or connector-scoped candidate files.
Step-by-step resolution:
- Pass
options.filewhen adding a server that does not already exist in config. - Verify
options.scope/connector combination points to an existing discovered document if you rely on auto-selection. - Prefer explicit file paths for deterministic writes in CI and tooling.
- Add tests for file inference and explicit file override behavior.
await config.add(
{ name: 'local-dev', config: { command: 'node', args: ['./server.js'] } },
{ connector: 'vscode', file: '/workspace/.vscode/mcp.json' }
);No parser or inline data was supplied for "{path}".
Thrown from: Config.consume
This happens when Config.consume(candidate) receives a candidate that has neither parsed data nor a parse(raw, file) function. The config loader cannot ingest the candidate.
Step-by-step resolution:
- If creating custom candidates, provide either
dataorparse. - Confirm candidate objects coming from
locate()preserve theparsefunction. - Use inline
dataonly when you already normalized{ servers, metadata }. - Add candidate-shape tests for both parse-based and data-based ingestion.
await config.consume({
path: '/workspace/.vscode/mcp.json',
parse(raw, file) {
return parseDocument(raw, file);
}
});Connector "{connector}" does not support write operations.
Thrown from: Config.remove
This happens when removing a server whose connector cannot write back changes. Config.remove requires connector write(...) support to persist the updated document.
Step-by-step resolution:
- Check which connector is attached to the existing server entry.
- Verify that connector exports a
writefunction. - Migrate the server definition to a writable connector/file if necessary.
- Add remove-path tests for writable and non-writable connector cases.
const before = config.get('integration');
if (!before?.connector)
throw new Error('Cannot remove server without connector metadata.');
await config.remove('integration');Configuration document "{file}" must contain an object with server definitions.
Thrown from: extractServers
This happens when the parsed document is not an object (for example null, array, number, or string). Server extraction only supports object-based config documents.
Step-by-step resolution:
- Inspect parser output before
extractServers(...). - Ensure the config file root is a JSON/TOML/YAML object, not a list/scalar.
- Keep server definitions under
mcpServers,servers, or top-level keyed objects. - Add fixtures that assert invalid root types fail fast.
{
"mcpServers": {
"local-dev": {
"command": "node",
"args": ["./server.js"]
}
}
}Server "{server}" in "{file}" must declare "command", "url", or "endpoint".
Thrown from: extractServers
This happens when a strict server node (mcpServers or servers) contains an entry without any connection primitive. Each server must provide at least one of command, url, or endpoint.
Step-by-step resolution:
- Locate the
{server}entry in{file}. - Add a valid connection shape for that server.
- Re-run discovery and verify the server appears in
config.map. - Add fixtures for each supported connection mode (
command,url,endpoint).
{
"servers": {
"staging": {
"url": "https://mcp.example.com/sse"
}
}
}Failed to parse {format} configuration document "{file}": {reason}
Thrown from: parseDocument
This happens when parseDocument(raw, file) fails to parse according to file extension (.yaml/.yml => YAML, everything else => JSON).
Step-by-step resolution:
- Confirm
{format}matches the file extension you passed. - Validate content with the matching parser (JSON vs YAML), not a different one.
- Check for format mismatches (YAML content in
.jsonfile, or vice versa). - Add fixtures for invalid JSON and invalid YAML parse failures.
import YAML from 'yaml';
const file = '/workspace/mcp.yaml';
const raw = await fs.readFile(file, 'utf8');
YAML.parse(raw);
const config = await load(undefined, '/workspace');
console.log(config.map.size);Inline configuration must declare at least one server using "mcpServers", "servers", or top-level objects with connection settings
Thrown from: parseInlineDocument
This happens when you call load(document, opts) with an inline object that declares zero usable servers. Inline mode still requires at least one valid server definition.
Step-by-step resolution:
- Ensure inline document uses
mcpServers,servers, or valid top-level server entries. - Verify at least one server has
command,url, orendpoint. - Remove placeholder/empty objects from inline docs used in tests.
- Add inline config tests for empty and valid shapes.
const inline = {
mcpServers: {
local: { command: 'node', args: ['./server.js'] }
}
};
const config = await load(inline);
console.log(config.get('local'));Failed to parse JSON file "{file}": {reason}
Thrown from: vscode.parse
This happens when VS Code connector parsing fails for .vscode/mcp.json (workspace or user-scoped). The file must be strict JSON.
Step-by-step resolution:
- Confirm which
.vscode/mcp.jsonpath was discovered (project vs home). - Validate JSON strictly (comments and trailing commas are invalid).
- Verify
serversis an object and optionalinputsis an array. - Re-run discovery and add fixtures for malformed and corrected VS Code docs.
const raw = await fs.readFile(file, 'utf8');
JSON.parse(raw); // throws immediately with concrete syntax location
const config = await load(undefined, process.cwd());
console.log(config.map.size);License
MIT
