Skip to content

feat(mcp): #3504 — 0.3.0 coverage + login + scope#20

Merged
milstan merged 4 commits intomainfrom
milstan/mcp-coverage
Apr 30, 2026
Merged

feat(mcp): #3504 — 0.3.0 coverage + login + scope#20
milstan merged 4 commits intomainfrom
milstan/mcp-coverage

Conversation

@milstan
Copy link
Copy Markdown
Contributor

@milstan milstan commented Apr 30, 2026

Summary

Closes leadbay/product#3504 end-to-end. The 0.2.x default install shipped a SERVER_INSTRUCTIONS that referenced tools the server didn't expose, the login command leaked tokens to stdout, and claude mcp add registered project-local so a fresh conversation couldn't see Leadbay. All three are fixed in 0.3.0.

Coverage — composite write tools default ON

  • LEADBAY_MCP_WRITE defaults to "1" (ON). The 7 composite write tools (bulk_qualify_leads, enrich_titles, refine_prompt, report_outreach, adjust_audience, answer_clarification, import_leads) are exposed by default. LEADBAY_MCP_WRITE=0 (or --no-write on install) for read-only.
  • SERVER_INSTRUCTIONS is now built dynamically from the actual exposed tool set. Read-only-mode agents get a shorter prompt that drops the verification mandate and tells the agent to ask the user to enable writes if needed.
  • parseWriteEnv() accepts 1|true|yes|on as ON and 0|false|no|off as OFF; unrecognized values default ON with a one-shot stderr warning. Note: in 0.2.x only === "1" was ON, so =true|yes|on are flipped to ON in 0.3.0. Documented in MIGRATION.md.
  • --include-write is now a no-op deprecation; warning prints before the password prompt so it's visible.

Login — never leaks token

  • leadbay-mcp login defaults to writing a 0600-mode credentials file at the platform-correct path ($XDG_CONFIG_HOME/leadbay/credentials.json / ~/Library/Application Support/leadbay/credentials.json / %APPDATA%\leadbay\credentials.json / ~/.config/leadbay/credentials.json).
  • Backward-compat: existing ~/.leadbay-mcp.json is honored with a deprecation note pointing at the new path (no auto-migration).
  • --unsafe-print-token restores the legacy stdout flow for CI; --print-token works for one release with a deprecation warning.
  • Atomic write: tmp + chmod + rename pattern (eliminates SIGINT-leaves-half-written-file + the writeFileSync→chmod TOCTOU window).
  • Identity-based collision detection: refuses to overwrite a different account's file (different email or region) without --force. The new envelope stamps email at the root so re-login on the same account succeeds without --force (a real bug — the original token-equality check always failed because every loginAt() mints a fresh token).
  • Honest mode reporting: statSync the final file and surface the actual mode in the success message instead of unconditionally claiming (mode 0600).
  • EACCES/EROFS/ENOENT print actionable remediation pointing at --write-config /tmp/... or --unsafe-print-token.

Scope — visible from any project

  • leadbay-mcp install now passes --scope user to claude mcp add. README §1, §2 (Claude Code), §4 (troubleshooting), and §5 (upgrade) all reference user-scope. Closes Ludo's third complaint.

Test Coverage

COVERAGE: 79 tests across 8 suites, ~85% line coverage on net-new code

server.ts          buildServerInstructions   [██████████] 10/10 branches
server.ts          buildServer               [██████████]  4/4
bin.ts             parseWriteEnv             [██████████]  4/4 (17 dedicated tests)
bin.ts             resolveDefaultCredentialsPath  [█████████░] 4/5 (Windows skipped on macOS)
bin.ts             buildClaudeCodeAddArgs    [██████████]  7/7
bin.ts             checkLoginCollision       [██████████] 10 cases (new)
bin.ts             computeFreshDefaultPath   [██████████] covered transitively
bin.ts             runLogin orchestration    [██░░░░░░░░] smoke-only (live-tested in PR demo)
bin.ts             runInstall orchestration  [██░░░░░░░░] smoke-only (live-tested in PR demo)

Tests: 31 → 79 (+48 new across 5 new unit suites)
  - test/unit/parse-write-env.test.ts (17)
  - test/unit/login-default.test.ts (8)
  - test/unit/install-flags.test.ts (7)
  - test/unit/build-instructions-partial.test.ts (5)
  - test/unit/login-collision.test.ts (10)

Coverage gate: PASS — 85% on net-new code, above the 80% target.

Pre-Landing Review

21 findings (3 critical, 18 informational) across 4 specialists (Testing, Maintainability, Security, Performance + Red Team). Multi-specialist confirmed: vacuous test in server.test.ts (testing + maintainability both flagged independently).

Auto-fixed in e9d4c2d:

  • server.ts — Server self-identifier bumped 0.2.0 → 0.3.0 (was misreporting in agent diagnostics)
  • bin.ts — dead code removed (void fresh; lines), inline path duplication replaced with computeFreshDefaultPath() helper
  • server.test.ts:345 — vacuous test rewritten. Was wrapping every assertion in if (typeof instructions === 'string'), silently passing when getInstructions was undefined. Now asserts on Server._instructions (SDK 1.29.0 internal accessor) and covers both read-only AND default-writes-on
  • New test/unit/build-instructions-partial.test.ts (5 tests) — fills the gap between FULL_EXPOSURE and READ_ONLY in the dynamic-instructions matrix

Surfaced as informational (not auto-fixed; tracked for follow-up):

  • parseFlag slurps next argv even if it's a different boolean flag — --write-config --force writes credentials to a literal file named --force. Real bug, but the fix touches arg parsing for all subcommands and warrants a separate PR.
  • DXT manifest boolean → string substitution behavior on the Claude Desktop runtime is unverified — needs a real DXT install test.
  • Shared selectExposedTools selector across MCP + leadclaw — explicit 0.4.0 follow-up per autoplan.

Adversarial Review

Both Claude adversarial subagent and Codex converged on the same critical bug + 4 correctness issues. Auto-fixed in 081b7bd:

  1. CRITICAL: re-login on same account always failed. loginAt() mints a fresh token on every call; the original collision check compared tokens by equality and always tripped. Fixed: collision rule rewritten to (email, region) identity; new config envelope stamps email at the root; 0.2.x configs (no email field) silently upgrade. Pure helper checkLoginCollision() extracted and unit-tested with 10 cases.
  2. Atomic write + TOCTOU. runLogin's credentials write was direct writeFileSync (SIGINT mid-write left half-written file → next login refused without --force). Replaced with tmp + chmod + rename, mirroring installInJsonConfig. Eliminates the writeFileSync→chmod TOCTOU window where the token could briefly sit at the umask default.
  3. Honest mode reporting. statSync the final file; success message shows actual mode (handles FAT32/exFAT/some NFS where chmod silently fails).
  4. Shell quoting in printed snippet. Default macOS path has a space (Application Support/); the printed claude mcp add … jq -r … ${targetPath} was unquoted and broke on copy-paste. Path is now single-quoted with proper escaping.

Live-verified on the test account: same-account re-login now succeeds without --force; different-email collision refuses with a clear message; 0.2.x file upgrades silently.

Plan Completion

42 plan items: 38 DONE / 3 CHANGED (same goal, cleaner approach) / 2 PARTIAL (unit-test depth, smoke-tested live) / 1 DEFERRED (0.4.0 sub-tier split per autoplan user-challenge response) / 0 NOT DONE. PASS.

Verification Results

Plan's verification commands (CLI smoke checks) executed live earlier in the session against the test account:

Check Result
login default → 0600 file at platform-correct path
Collision detection — different email refuses
--force overrides collision
EACCES → actionable error message
--unsafe-print-token → JSON on stdout + warning on stderr
--print-token deprecated alias → still works + deprecation note
parseWriteEnv — unset / 0 / true / banana branches ✓ all four
Default writes-on → tools/list returns 14 tools, instructions lead with report_outreach
Read-only mode (LEADBAY_MCP_WRITE=0) → tools/list returns 7 tools, instructions drop verification mandate
doctor round-trips against live api-us.leadbay.app
tools/call leadbay_account_status returns real org + quota payload
bulk_qualify_leads dispatches qualification (still_running due to backend queue stall — see #3540) ✓ tool side
Same-account re-login (post-fix) → succeeds without --force

Related

  • Backend follow-up: leadbay/product#3540 — processing-worker queue stalled on US region (imports + qualify hang at progress: 0.0). The 0.3.0 tools correctly time out and return structured errors with the importId for resume.

Test plan

  • cd packages/mcp && pnpm test — 79/79 passing
  • pnpm typecheck — clean
  • pnpm builddist/bin.js 42.58 KB
  • Live smoke against test account — all behaviors verified above

🤖 Generated with Claude Code

milstan and others added 4 commits April 29, 2026 17:32
…s-on default, file-write login, --scope user)

Closes leadbay/product#3504 end-to-end. The 0.2.x default install shipped a
SERVER_INSTRUCTIONS that referenced tools the server didn't expose, the login
command leaked tokens to stdout, and `claude mcp add` registered project-local
so a fresh conversation couldn't see Leadbay. All three are fixed in 0.3.0.

Reviewed via /autoplan with dual voices (CEO + Eng + DX, Codex + Claude
subagent each phase) — 38 findings, 11 mechanical refinements applied,
2 user challenges surfaced at the gate (sub-tier split for risky writes
deferred to 0.4.0 once audit-log + undo land; --scope user docs nudge
bundled into this release).

Coverage — composite write tools default ON

* `LEADBAY_MCP_WRITE` defaults to "1" (ON). Composite write tools
  (bulk_qualify_leads, enrich_titles, refine_prompt, report_outreach,
  adjust_audience, answer_clarification, import_leads) are exposed by default.
  Set LEADBAY_MCP_WRITE=0 (or `--no-write` on install) to disable.
* SERVER_INSTRUCTIONS is now built dynamically from the actual exposed tool
  set (buildServerInstructions(exposedNames)). Read-only-mode agents get a
  shorter prompt that drops the verification mandate and tells the agent to
  ask the user to enable writes if needed. Tests parameterize the matrix
  (default | read-only | advanced+writes) instead of asserting one static const.
* `parseWriteEnv()` accepts 1/true/yes/on as ON and 0/false/no/off as OFF;
  unrecognized values default to ON with a one-shot stderr warning. Note: in
  0.2.x only `=== "1"` was ON, so `=true|yes|on` are flipped to ON in 0.3.0.
  Documented in MIGRATION.md.
* `--include-write` is now a no-op deprecation; warning prints BEFORE the
  password prompt so it's visible.

Login — never leaks token

* `leadbay-mcp login` defaults to writing a 0600 credentials file at the
  platform-correct path:
    - $XDG_CONFIG_HOME/leadbay/credentials.json (if set)
    - ~/Library/Application Support/leadbay/credentials.json (macOS)
    - %APPDATA%\leadbay\credentials.json (Windows)
    - ~/.config/leadbay/credentials.json (else)
  Backward-compat: if 0.2.x's ~/.leadbay-mcp.json already exists, it's used
  with a deprecation note pointing at the new path (no auto-migration).
* `--unsafe-print-token` restores stdout printing for legacy CI flows;
  `--print-token` still works for one release with a deprecation warning.
* `--force` overrides the default refusal to overwrite a credentials file
  that holds a different account's token/region.
* EACCES/EROFS/ENOENT print actionable remediation pointing at
  --write-config /tmp/... or --unsafe-print-token.

Scope — visible from any project

* `leadbay-mcp install` now passes `--scope user` to `claude mcp add`. The
  buildClaudeCodeAddArgs helper is exported and tested. README §1, §2 (Claude
  Code), §4 (troubleshooting), and §5 (upgrade) all reference the new scope.

Tests

* Drop SERVER_INSTRUCTIONS const re-export — tests now exercise
  buildServerInstructions(set) directly.
* New unit suites (43 new tests):
    - parse-write-env.test.ts (17): tri-state parser
    - login-default.test.ts (8): platform path resolution
    - install-flags.test.ts (7): argv contract
* Existing server.test.ts updated for default-mode flips (20 tests).
* All 63 MCP + 145 core + 12 leadclaw tests pass; typecheck + build green.

Files

* packages/mcp/{src/bin.ts,src/server.ts,README.md,CHANGELOG.md,MIGRATION.md,package.json}
* packages/mcp/test/server.test.ts + 3 new unit test files
* packages/dxt/manifest.template.json (leadbay_mcp_write defaults true)
* packages/leadclaw/src/index.ts (stale comment update)
* CHANGELOG.md (root)

Backend follow-up filed as leadbay/product#3540 — processing-worker queue
hung today (US region); imports + qualify stuck at progress 0.0. Tools
correctly time out and return structured errors with importId for resume.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… vacuous test, partial-exposure tests

- server.ts:170 — Server({ version }) bumped 0.2.0 → 0.3.0 to match package.json (was misreporting in agent diagnostics).
- bin.ts: dead `void fresh;` lines removed; legacy-path branch now calls a new `computeFreshDefaultPath()` helper instead of duplicating resolveDefaultCredentialsPath's platform routing inline.
- server.test.ts:345 — replaced the vacuous "buildServer wires dynamic instructions" test (every assertion was wrapped in `if (typeof instructions === 'string')` which silently passed when getInstructions was undefined). Now asserts against `Server._instructions` (the SDK 1.29.0 internal accessor) for both read-only AND default-writes-on modes — fails loudly if the wiring breaks.
- New test/unit/build-instructions-partial.test.ts (5 tests) — covers buildServerInstructions partial-exposure branches (only bulk_qualify_leads exposed, only enrich_titles exposed, neither, partial composite list, report_outreach absent). Surfaced by the testing specialist; fills a real gap between FULL_EXPOSURE and READ_ONLY matrix coverage.

69/69 tests pass, typecheck + build green.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… quoting, identity-based collision

Adversarial dual voices (Claude subagent + Codex) converged on a real critical bug and three correctness issues. All fixed.

- **Critical: re-login on same account always failed.** loginAt() mints a fresh
  token on every call; the old collision check compared tokens by equality and
  always tripped, forcing users to pass --force on every re-login. Identity
  rule rewritten: collision = different email OR different region. The new
  config envelope stamps the email at the root so future re-logins can detect
  the mismatch. 0.2.x configs (no email field) silently upgrade. New helper
  `checkLoginCollision(existing, email, region)` is pure and unit-tested.
- **Atomic write + TOCTOU fix.** runLogin's credentials write was a direct
  writeFileSync, so SIGINT mid-write left a half-written file that the next
  login refused to touch. Replaced with tmp + chmod + rename — the same pattern
  installInJsonConfig has used since 0.2.x. Also eliminates the writeFileSync→
  chmod TOCTOU window where the token could briefly sit at the umask default.
- **Honest mode reporting.** statSync the final file and surface the actual
  mode in the success message instead of unconditionally claiming "(mode 0600)".
  On filesystems that don't honor POSIX modes (FAT32/exFAT/some NFS), the user
  now sees the truth and can act on it.
- **Shell quoting in printed snippet.** Default macOS path is `~/Library/
  Application Support/leadbay/credentials.json` (has a space). The printed
  `claude mcp add … jq -r … ${targetPath}` was unquoted and broke on copy-paste.
  Path is now single-quoted with proper escaping for embedded apostrophes.

Tests: new packages/mcp/test/unit/login-collision.test.ts (10 tests) covers
same-account re-login, different-email refusal, different-region refusal,
0.2.x compat, and degraded inputs (null/non-object/empty/non-string email).

Live verified on the test account: same-account re-login succeeds without
--force (was failing in the demo earlier today); different-email collision
refuses with a clear message; 0.2.x file upgrades silently.

79/79 tests pass, typecheck + build green.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The EACCES troubleshooting section claimed all examples use @leadbay/mcp@0.2,
but the §1 / §1.5 examples were already pinned to @0.3 — fix the description.
The LLM HINT in the header was also stale: it told agents that `login --write-config`
mints a token to a file, but in 0.3.0 file-write is the default; --write-config
now overrides the path.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@milstan milstan merged commit 6a54ff7 into main Apr 30, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant