# `firma mcp install` — register Claude Code alongside Desktop

## Background / problem

`firma mcp install` currently registers the firma MCP server in a single
location: the Claude **Desktop** config (`claude_desktop_config.json`).
`mcpInstallCommand()` in `mcp.ts` writes only the Desktop path returned by
`getClaudeConfigPath()` and never touches the Claude **Code** config
(`~/.claude.json`).

→ Claude Code users must register firma manually even after running
`firma mcp install`. This design makes a single `mcp install` run register
firma in both Desktop and Code.

(Installing a skill into `~/.claude/skills/` is split out as a follow-up
task. It is not part of this scope.)

## Decision: direct edit + atomic write

`~/.claude.json` lives at `homedir()/.claude.json` on macOS, Windows, and
Linux alike. With no platform branching it is simpler than the Desktop
path (`getClaudeConfigPath()` branches heavily for win32).

Delegating to the `claude` CLI (`claude mcp add`) depends on whether the
CLI is installed and on `which`/`where` branching, which makes
"identical behavior on Mac and Windows" harder to guarantee.
→ Direct edit is chosen.

`fs.renameSync` replaces an existing file on Windows in Node as well, so a
temp+rename atomic write behaves identically across operating systems.

## Structure

### Shared helper

Extract the current Desktop write logic (read → parse → mutate → write).

```
upsertMcpServer(configPath, name, entry)
  - if the file exists: readFileSync + JSON.parse, otherwise {}
  - config.mcpServers ??= {}
  - config.mcpServers[name] = entry
  - atomic write: write to `${configPath}.tmp`, then renameSync
  - JSON.stringify(config, null, 2) + a single trailing newline (existing format)
```

Both Desktop and Code call this helper. It removes the duplicated
read-modify-write and makes the Desktop write atomic as well.

### Path functions

- `getClaudeConfigPath()` — Desktop (unchanged)
- `getClaudeCodeConfigPath()` (new) → `join(homedir(), '.claude.json')` — no branching

### Entry shape (intentionally different)

- Desktop: `{ command, args }` — keep existing
- Code: `{ type: 'stdio', command, args }` — matches the existing
  `~/.claude.json` convention

Both targets use the same `command` / `mcpPath` returned by `getMcpBinPath()`.

## Edge cases

- **Code config file missing** → create the minimal shape
  `{ mcpServers: { firma: ... } }`. `homedir()` always exists, so no
  mkdir is needed.
- **Code config JSON parse failure** → `~/.claude.json` is a ~68KB user
  state file, so never overwrite it. Print a warning and skip only the
  Code target.
- **Concurrent write race** — a running Claude Code may write
  `~/.claude.json` at the same time. An atomic rename prevents file
  *corruption* but a concurrent change could still be *lost*. `install`
  is a one-off action, so this is accepted as a known limitation.

## Failure isolation

The Desktop write and the Code write are independent. If one throws, the
other still proceeds, and success/failure is reported per target. The
current `process.exit(1)` immediate abort is replaced with per-target
try/catch. (A shared prerequisite failure, e.g. the binary not being
found, still aborts immediately.)

## Idempotency

Re-running overwrites only the `mcpServers.firma` entry. Other servers and
keys are preserved. → Idempotent.

## Output

```
✓ Claude Desktop  ~/Library/.../claude_desktop_config.json
✓ Claude Code     ~/.claude.json
  Binary: .../dist/mcp.js
↻ Restart Claude Desktop to activate. Claude Code picks it up on the next session.
★ Like firma? Star us at github.com/evan-moon/firma
```

## Tests

`mcp.ts` currently has no tests. After extracting `upsertMcpServer`, add
tests (using a temp directory):

- creates a new file — a minimal config with only `mcpServers.firma`
- merges into an existing file — adds firma to an existing `mcpServers`
- **preserves other keys** — other servers and other top-level keys remain
- idempotent re-run — only the firma entry is updated
- invalid-JSON input — throws, or is otherwise skippable by the caller

## Follow-up: Windows Desktop config path fallback (separate commit)

The existing win32 branch of `getClaudeConfigPath()` detected a Microsoft
Store install by checking whether the
`Packages\Claude_*\LocalCache\Roaming\Claude\` **directory** exists. That
directory is created only after the app's first launch, so an
installed-but-unlaunched Store app failed detection, fell back to
`%APPDATA%\Claude\`, and created the config in the wrong location.

Change the detection signal from a runtime artifact to the
**package folder present at install time** (`Packages\Claude_<hash>`).
Priority:

1. Store config file already exists → Store path
2. standard config file already exists → standard path (`%APPDATA%\Claude\...`)
3. neither → Store path if the `Packages\Claude_*` folder is present,
   else standard path

An already-existing config file always wins over a guess, which prevents
creating a duplicate config in the wrong location.

Extract the path resolution into a pure `resolveWindowsDesktopConfigPath(deps)`
with injectable `exists` / `findClaudePackage` so the branches can be unit
tested. (Verification on a real Windows host is not possible — dependency
injection covers only the branching.)

## Out of scope

- Installing a skill into `~/.claude/skills/` (follow-up task)
- The CLI+MCP parity rule — `mcp install` installs the MCP server itself,
  so no corresponding MCP tool can exist. Exception.
