mech.app
Dev Tools

Cross-Client MCP Config Installers: Why Server Discovery Needs More Than JSON Files

Building automated config installers for MCP servers to solve the missing discovery and setup layer in Anthropic's Model Context Protocol.

Source: dev.to
Cross-Client MCP Config Installers: Why Server Discovery Needs More Than JSON Files

Anthropic’s Model Context Protocol shipped without package management or auto-configuration. Every MCP server requires manual JSON editing in client-specific config files scattered across platform-dependent paths. Claude Desktop, Cline, Cursor, Continue, and Zed each maintain separate configs with overlapping but non-identical schemas.

The mcpr project built an installer to collapse this friction. The implementation exposes five problems that any MCP tooling layer will hit: npm resolution from a registry, cross-OS path discovery, JSON deep-merge without clobbering sibling servers, atomic writes with backups, and file-mode preservation.

The Manual Setup Tax

Current MCP server integration requires:

  1. Find the server on GitHub
  2. Read the README for its launch command
  3. Copy a JSON fragment
  4. Locate the correct config file for your client
  5. Paste and restart

Step 4 is the maze. Claude Desktop reads from ~/Library/Application Support/Claude/claude_desktop_config.json on macOS and %APPDATA%\Claude\claude_desktop_config.json on Windows. Cline (VS Code extension) reads from ~/Library/Application Support/Code/User/globalStorage/saoudrizwan.claude-dev/settings/cline_mcp_settings.json. Each client has its own path and schema quirks.

The mcpr install command collapses this into:

npx -y @incultnitollc/mcpr search filesystem
npx -y @incultnitollc/mcpr install npm-agent-infra-mcp-server-filesystem --client claude-desktop

The slug (npm-agent-infra-mcp-server-filesystem) comes from the registry, not user input. This prevents typos and ensures the installer can resolve package metadata.

Architecture: Five Layers of State Reconciliation

1. npm Resolution from Registry

The installer queries a registry (currently a JSON file, eventually a real package index) to map slugs to npm package names and default launch configs. This layer handles:

  • Package version resolution
  • Default command templates
  • Required environment variables
  • Platform-specific launch scripts

The registry returns a config fragment. The installer never guesses command syntax.

2. Cross-OS Path Matrix

Config file discovery requires platform-specific logic:

ClientmacOS PathWindows PathLinux Path
Claude Desktop~/Library/Application Support/Claude/claude_desktop_config.json%APPDATA%\Claude\claude_desktop_config.json~/.config/Claude/claude_desktop_config.json
Cline~/Library/Application Support/Code/User/globalStorage/saoudrizwan.claude-dev/settings/cline_mcp_settings.jsonSimilar under %APPDATA%\CodeSimilar under ~/.config/Code
Cursor~/Library/Application Support/Cursor/User/globalStorage/...%APPDATA%\Cursor\User\globalStorage\...~/.config/Cursor/User/globalStorage/...

The installer probes each path in order, falling back if the client isn’t installed. It creates missing directories but errors if the parent application directory doesn’t exist (indicating the client isn’t installed).

3. JSON Deep-Merge Without Clobbering

The config file contains a mcpServers object with one key per server. The installer must:

  • Preserve existing servers
  • Update the target server’s config if it already exists
  • Avoid reordering keys (to minimize diff noise)
  • Handle missing files (create with valid JSON structure)

The merge logic:

function mergeServerConfig(existing: Config, newServer: ServerConfig, serverKey: string): Config {
  const merged = { ...existing };
  merged.mcpServers = merged.mcpServers || {};
  merged.mcpServers[serverKey] = newServer;
  return merged;
}

This is naive. Real implementations need conflict resolution: what if the existing config has a different command or args? The installer currently overwrites. A production version would diff and prompt, or use a --force flag.

4. Atomic Writes with Backups

The installer writes to a temp file, then renames it over the original. This prevents corruption if the process crashes mid-write. Before writing, it copies the original to <config>.backup.

const tempPath = `${configPath}.tmp`;
fs.writeFileSync(tempPath, JSON.stringify(merged, null, 2));
fs.copyFileSync(configPath, `${configPath}.backup`);
fs.renameSync(tempPath, configPath);

The backup is timestamped in later versions to allow rollback across multiple installs.

5. File-Mode Preservation

The first version broke file permissions. On Unix systems, config files are often 0600 (user read/write only). The installer was creating temp files with default umask, then renaming them, which changed permissions to 0644.

Fix: copy the original file’s mode before writing:

const stats = fs.statSync(configPath);
fs.writeFileSync(tempPath, JSON.stringify(merged, null, 2), { mode: stats.mode });

This preserves executable bits and restrictive permissions.

Credential Injection Problem

The installer doesn’t handle secrets. If a server requires an API key, the registry can specify an environment variable name (OPENAI_API_KEY), but the installer can’t inject the value. Options:

  • Prompt the user during install (breaks non-interactive flows)
  • Read from a keychain (platform-specific, requires user consent)
  • Defer to a separate mcpr configure command that runs post-install
  • Document the manual step in the output

Current implementation: print a warning if the server config references an env var that isn’t set.

Idempotency and Version Conflicts

Running install twice with the same slug should be safe. Current behavior: overwrite the existing config. This breaks if the user customized args.

Better approach:

  • Detect if the server is already configured
  • Diff the existing config against the registry default
  • If they match, no-op
  • If they differ, prompt or use --update flag

Version conflicts are harder. If the registry updates a server’s default command, should install overwrite the user’s config? Probably not without explicit consent.

Trade-Offs and Failure Modes

ConcernCurrent ApproachRisk
Config schema driftHardcoded per-client schemasBreaks when clients update their JSON structure
Credential managementPrint warning, require manual setupPoor UX for servers with required secrets
Concurrent installsNo lockingTwo install commands can corrupt the config file
Registry availabilityFetch from GitHub on every installNetwork dependency, no offline mode
Server version pinningUses latest by defaultNo way to install a specific version

The locking problem is real. If two terminal windows run mcpr install simultaneously, both read the config, merge their changes, and write. Last write wins. Fix: use fs.open with O_EXCL to create a lockfile, or use a library like proper-lockfile.

Technical Verdict

Use this pattern when:

  • You’re building tooling for a protocol that lacks native package management
  • You need to support multiple clients with incompatible config formats
  • Manual JSON editing is a documented pain point in your user feedback

Avoid when:

  • The protocol has a native discovery mechanism (don’t reinvent)
  • Clients use a shared config store (no need for cross-client logic)
  • Your users are comfortable with manual config (don’t add abstraction tax)

The mcpr installer is a stopgap. The real fix is for MCP clients to implement a shared config API or for the protocol to standardize a discovery mechanism. Until then, this layer is necessary plumbing.

Tags

agentic-ai orchestration infrastructure

Primary Source

dev.to