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:
- Find the server on GitHub
- Read the README for its launch command
- Copy a JSON fragment
- Locate the correct config file for your client
- 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:
| Client | macOS Path | Windows Path | Linux 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.json | Similar under %APPDATA%\Code | Similar 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 configurecommand 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
--updateflag
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
| Concern | Current Approach | Risk |
|---|---|---|
| Config schema drift | Hardcoded per-client schemas | Breaks when clients update their JSON structure |
| Credential management | Print warning, require manual setup | Poor UX for servers with required secrets |
| Concurrent installs | No locking | Two install commands can corrupt the config file |
| Registry availability | Fetch from GitHub on every install | Network dependency, no offline mode |
| Server version pinning | Uses latest by default | No 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.