mech.app
Dev Tools

Cross-Client MCP Config Installers: Why Agent Tool Discovery Needs Package-Manager Plumbing

How developers build config installers to solve MCP's missing package layer, detecting Claude Desktop, Cline, and Zed, then writing JSON configs without...

Source: dev.to
Cross-Client MCP Config Installers: Why Agent Tool Discovery Needs Package-Manager Plumbing

Anthropic’s Model Context Protocol shipped without automated installation tooling. Every MCP server author writes manual setup docs that tell users to copy JSON fragments into config files scattered across operating systems and clients. Developers immediately started building cross-client installers to handle config detection, path resolution, and JSON merging.

The mcpr project built a TypeScript installer that collapses the five-step manual flow (find server, read docs, copy JSON, locate config file, restart client) into two commands: search the registry for a server slug, then install it with client detection. The plumbing exposes what MCP left out: a package-manager layer that knows where clients live and how to merge configs safely.

The Config File Maze

MCP clients store server configs in JSON files at different paths with slightly different schemas. No standard exists.

Client config locations:

ClientmacOS PathWindows PathSchema Notes
Claude Desktop~/Library/Application Support/Claude/claude_desktop_config.json%APPDATA%\Claude\claude_desktop_config.jsonTop-level mcpServers object
Cline (VS Code)~/Library/Application Support/Code/User/globalStorage/saoudrizwan.claude-dev/settings/cline_mcp_settings.json%APPDATA%\Code\User\globalStorage\saoudrizwan.claude-dev\settings\cline_mcp_settings.jsonNested under mcpServers
Zed~/.config/zed/settings.json%APPDATA%\Zed\settings.jsonInline context_servers array
Cursor~/Library/Application Support/Cursor/User/globalStorage/...%APPDATA%\Cursor\User\globalStorage\...Varies by extension

An installer must detect which clients exist, resolve the right path per OS, and handle schema differences without breaking existing configs.

Architecture: Five Plumbing Layers

The mcpr install command implements five operations:

  1. Registry resolution: Validate the server slug against a registry, fetch npm package name and launch command
  2. Path detection: Probe filesystem for client install directories, construct config file paths per OS
  3. JSON deep-merge: Load existing config, insert new server entry without clobbering sibling servers or user customizations
  4. Atomic write with backup: Write to temp file, copy original to .bak, rename temp to live config
  5. File mode preservation: Copy original permissions to new file (critical on Unix systems where configs may be user-only readable)

The flow looks like this:

async function installServer(slug: string, clientName: string) {
  // 1. Registry lookup
  const serverMeta = await registry.resolve(slug);
  if (!serverMeta) throw new Error(`Unknown slug: ${slug}`);
  
  // 2. Path detection
  const configPath = detectConfigPath(clientName, process.platform);
  if (!configPath) throw new Error(`Client ${clientName} not found`);
  
  // 3. Load and merge
  const existingConfig = await fs.readJSON(configPath);
  const newEntry = buildServerEntry(serverMeta);
  const merged = deepMerge(existingConfig, { mcpServers: { [slug]: newEntry } });
  
  // 4. Atomic write
  const tempPath = `${configPath}.tmp`;
  const backupPath = `${configPath}.bak`;
  await fs.writeJSON(tempPath, merged, { spaces: 2 });
  await fs.copy(configPath, backupPath);
  await fs.rename(tempPath, configPath);
  
  // 5. Preserve permissions
  const stats = await fs.stat(backupPath);
  await fs.chmod(configPath, stats.mode);
}

The registry slug acts as a key, not a free-form string. Users can’t guess server names. They must search first, which returns a validated slug the installer can resolve.

Deep Merge Without Clobbering

The hardest part is merging new server entries into existing configs without destroying user customizations. A naive Object.assign overwrites nested objects. A recursive merge must:

  • Preserve sibling servers in the mcpServers object
  • Keep user-added environment variables or args
  • Avoid duplicating the new server if it already exists
  • Handle missing keys (first install creates the mcpServers object)

The merge logic uses a recursive function that checks each level:

function deepMerge(target: any, source: any): any {
  if (Array.isArray(source)) return source; // Arrays replace, don't merge
  if (typeof source !== 'object' || source === null) return source;
  
  const output = { ...target };
  for (const key in source) {
    if (key in output && typeof output[key] === 'object' && !Array.isArray(output[key])) {
      output[key] = deepMerge(output[key], source[key]);
    } else {
      output[key] = source[key];
    }
  }
  return output;
}

Arrays replace instead of merging because MCP server args are ordered lists. Merging would create duplicates or reorder flags.

File Mode Preservation Bug

The first version of mcpr install wrote the new config with default permissions (644 on Unix). On macOS, Claude Desktop’s config is mode 600 (user read/write only). After install, the config became world-readable, leaking API keys stored in environment variables. The mcpr team hit this bug in production and fixed it by copying the original file’s mode bits:

const stats = await fs.stat(backupPath);
await fs.chmod(configPath, stats.mode);

This preserves user-only permissions and prevents accidental exposure of secrets in multi-user environments.

Security Trade-Offs: CLI vs. Postinstall Script

MCP config installers can run as:

  • Standalone CLI tools (like mcpr): User explicitly runs npx mcpr install, sees what happens, controls timing
  • npm postinstall scripts: Automatically run after npm install mcp-server-xyz, no user action needed
  • GUI installers: Electron or native apps with file pickers and visual config editors

Trade-off matrix:

ApproachUser ControlAutomationAttack SurfaceConfig Visibility
CLI toolHigh (explicit command)Low (manual step)Low (user reviews command)High (terminal output)
Postinstall scriptLow (implicit)High (automatic)Medium (runs on install)Low (hidden in npm logs)
GUI installerMedium (visual prompts)Medium (guided flow)High (native file access)Medium (dialogs show paths)

Postinstall scripts are dangerous because they run arbitrary code during npm install. A malicious MCP server package could overwrite configs, inject backdoors, or exfiltrate API keys. CLI tools require explicit user action, which creates a review checkpoint.

The mcpr approach uses a registry to validate slugs before installation. The registry acts as a trust boundary: only vetted servers get slugs, and the installer refuses to run without one. This prevents typosquatting (installing mcp-server-filesytem instead of mcp-server-filesystem) and reduces the risk of running untrusted code.

When Clients Use Different Schemas

Zed uses a context_servers array instead of an mcpServers object. The installer needs schema adapters per client:

function buildConfigForClient(serverMeta: ServerMeta, clientName: string) {
  switch (clientName) {
    case 'claude-desktop':
    case 'cline':
      return { mcpServers: { [serverMeta.slug]: serverMeta.config } };
    case 'zed':
      return { context_servers: [{ name: serverMeta.slug, ...serverMeta.config }] };
    default:
      throw new Error(`Unknown client: ${clientName}`);
  }
}

This abstraction layer isolates schema differences from the merge logic. Adding a new client means writing a new adapter, not rewriting the installer.

Observability: What Went Wrong

The installer needs to surface failures clearly:

  • Client not found: “Cline config not detected. Install Cline or specify a different client.”
  • Invalid slug: “Unknown slug ‘mcp-server-xyz’. Run mcpr search to find valid servers.”
  • Merge conflict: “Server ‘filesystem’ already exists in config. Use --force to overwrite.”
  • Permission denied: “Cannot write to config file. Check file permissions or run with sudo.”

The mcpr tool logs each step (registry lookup, path detection, merge, write) and exits with non-zero status codes on failure. This makes it scriptable in CI pipelines or setup automation.

Likely Failure Modes

Config file corruption: If the installer crashes mid-write, the config file may be incomplete JSON. The atomic write pattern (write to temp, rename) prevents this, but the backup file is critical for recovery.

Client updates changing paths: When Claude Desktop or Cline updates, config paths may move. The installer needs to probe multiple candidate paths and fall back gracefully.

Registry downtime: If the registry is unreachable, the installer can’t validate slugs. A local cache of previously installed servers would allow offline reinstalls.

Concurrent writes: Two installers running simultaneously can race on the config file. File locking (using fs.open with exclusive mode) would prevent corruption but adds complexity.

Schema drift: Clients may add new required fields to configs. The installer needs version detection to know which schema to generate.

Technical Verdict

Use a config installer when:

  • You’re shipping an MCP server on npm (like mcp-server-filesystem) and want Claude Desktop and Cline users to run one command instead of editing three JSON files manually
  • You maintain multiple MCP servers and want users to install without reading docs
  • You’re building a registry or marketplace that needs automated setup
  • You need to support multiple clients (Claude Desktop, Cline, Zed) with one tool
  • You want to reduce support burden from manual config editing errors

Avoid it when:

  • You have a single server with simple config (manual JSON is fine)
  • Your users are technical enough to prefer editing configs directly
  • You can’t maintain schema adapters as clients evolve
  • You need to support clients with non-JSON config formats. Helix uses TOML for configuration, which requires a different parser and schema adapter layer. Neovim plugins often use Lua or YAML, and merging those formats safely means rewriting the entire merge strategy. The JSON-focused installer pattern breaks down when you need to handle multiple markup languages with different nesting rules and type systems.

The gap MCP leaves is real: no standard install flow, no package manager, no config schema versioning. Config installers are a stopgap until the protocol standardizes distribution. If you build one, focus on atomic writes, permission preservation, and clear error messages. The merge logic is harder than it looks.

Tags

agentic-ai orchestration infrastructure

Primary Source

dev.to