Module: Esp::McpInstaller

Defined in:
lib/esp/mcp_installer.rb

Overview

Registers ‘esp mcp serve` with a local MCP client by merging a stdio server entry into that client’s config JSON, preserving any other servers and top-level keys already there.

Deliberately kept out of Esp::Operations: rewriting a user’s AI-client config is a CLI/operator action, never something an agent should be able to drive over MCP itself.

The two clients share the ‘mcpServers` / stdio shape and an identical server entry; they differ only in where the config lives. The command is an absolute path to `bin/esp` — note `$CLAUDE_PROJECT_DIR` looks tempting for a portable Claude Code entry but is unset when `.mcp.json` is parsed (it’s a hooks-only variable), so the server silently fails to launch. The cost is a machine-specific path in the config.

Step-22.5 migration: the install action quietly drops any existing entry named ‘mw` (the historical pre-rename name) when it writes the new `esp` entry, so a re-install after the rename leaves a single, current entry.

Defined Under Namespace

Classes: Result

Constant Summary collapse

SERVER_NAME =
'esp'.freeze
LEGACY_NAME =
'mw'.freeze
CLIENTS =
{
  'claude-code' => {
    label: 'Claude Code',
    path: -> { File.join(Esp::ROOT, '.mcp.json') }
  },
  'claude-desktop' => {
    label: 'Claude Desktop',
    path: -> { File.expand_path('~/Library/Application Support/Claude/claude_desktop_config.json') }
  }
}.freeze

Class Method Summary collapse

Class Method Details

.clientsObject



42
43
44
# File 'lib/esp/mcp_installer.rb', line 42

def clients
  CLIENTS.keys
end

.entry_for(client) ⇒ Object



46
47
48
49
# File 'lib/esp/mcp_installer.rb', line 46

def entry_for(client)
  client_meta(client) # validate
  { 'type' => 'stdio', 'command' => File.join(Esp::ROOT, 'bin', 'esp'), 'args' => %w[mcp serve] }
end

.install(client, name: SERVER_NAME, path: nil) ⇒ Object

Merge the entry into the client’s config. path overrides the default location (tests point it at a tmpfile). Returns a Result whose :action is :created / :updated / :unchanged. The :migrated flag is true when this install removed a legacy ‘mw` entry pointing at the same install.



55
56
57
58
59
60
61
62
63
64
65
66
67
# File 'lib/esp/mcp_installer.rb', line 55

def install(client, name: SERVER_NAME, path: nil)
  meta = client_meta(client)
  config_path = path || meta[:path].call
  entry = entry_for(client)
  config = read_config(config_path)
  servers = (config['mcpServers'] ||= {})
  migrated = migrate_legacy_entry(servers, name)
  action = action_for(servers[name], entry)
  servers[name] = entry
  write_config(config_path, config) unless action == :unchanged && !migrated
  Result.new(client: client, label: meta[:label], config_path: config_path,
             name: name, action: action, entry: entry, migrated: migrated)
end