feat(mcp): #3504 — 0.3.0 coverage + login + scope#20
Merged
Conversation
…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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Closes leadbay/product#3504 end-to-end. The 0.2.x default install shipped a
SERVER_INSTRUCTIONSthat referenced tools the server didn't expose, thelogincommand leaked tokens to stdout, andclaude mcp addregistered 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_WRITEdefaults 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-writeoninstall) for read-only.SERVER_INSTRUCTIONSis 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()accepts1|true|yes|onas ON and0|false|no|offas OFF; unrecognized values default ON with a one-shot stderr warning. Note: in 0.2.x only=== "1"was ON, so=true|yes|onare flipped to ON in 0.3.0. Documented in MIGRATION.md.--include-writeis now a no-op deprecation; warning prints before the password prompt so it's visible.Login — never leaks token
leadbay-mcp logindefaults 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).~/.leadbay-mcp.jsonis honored with a deprecation note pointing at the new path (no auto-migration).--unsafe-print-tokenrestores the legacy stdout flow for CI;--print-tokenworks for one release with a deprecation warning.--force. The new envelope stampsemailat the root so re-login on the same account succeeds without--force(a real bug — the original token-equality check always failed because everyloginAt()mints a fresh token).statSyncthe final file and surface the actual mode in the success message instead of unconditionally claiming(mode 0600).--write-config /tmp/...or--unsafe-print-token.Scope — visible from any project
leadbay-mcp installnow passes--scope usertoclaude mcp add. README §1, §2 (Claude Code), §4 (troubleshooting), and §5 (upgrade) all reference user-scope. Closes Ludo's third complaint.Test Coverage
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 withcomputeFreshDefaultPath()helperserver.test.ts:345— vacuous test rewritten. Was wrapping every assertion inif (typeof instructions === 'string'), silently passing whengetInstructionswas undefined. Now asserts onServer._instructions(SDK 1.29.0 internal accessor) and covers both read-only AND default-writes-ontest/unit/build-instructions-partial.test.ts(5 tests) — fills the gap between FULL_EXPOSURE and READ_ONLY in the dynamic-instructions matrixSurfaced as informational (not auto-fixed; tracked for follow-up):
parseFlagslurps next argv even if it's a different boolean flag —--write-config --forcewrites credentials to a literal file named--force. Real bug, but the fix touches arg parsing for all subcommands and warrants a separate PR.selectExposedToolsselector 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: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 stampsemailat the root; 0.2.x configs (no email field) silently upgrade. Pure helpercheckLoginCollision()extracted and unit-tested with 10 cases.runLogin's credentials write was directwriteFileSync(SIGINT mid-write left half-written file → next login refused without--force). Replaced with tmp + chmod + rename, mirroringinstallInJsonConfig. Eliminates the writeFileSync→chmod TOCTOU window where the token could briefly sit at the umask default.statSyncthe final file; success message shows actual mode (handles FAT32/exFAT/some NFS where chmod silently fails).Application Support/); the printedclaude 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:
logindefault → 0600 file at platform-correct path--forceoverrides collision--unsafe-print-token→ JSON on stdout + warning on stderr--print-tokendeprecated alias → still works + deprecation noteparseWriteEnv— unset /0/true/bananabranchestools/listreturns 14 tools, instructions lead withreport_outreachLEADBAY_MCP_WRITE=0) →tools/listreturns 7 tools, instructions drop verification mandatedoctorround-trips against liveapi-us.leadbay.apptools/call leadbay_account_statusreturns real org + quota payloadbulk_qualify_leadsdispatches qualification (still_running due to backend queue stall — see #3540)--forceRelated
progress: 0.0). The 0.3.0 tools correctly time out and return structured errors with theimportIdfor resume.Test plan
cd packages/mcp && pnpm test— 79/79 passingpnpm typecheck— cleanpnpm build—dist/bin.js42.58 KB🤖 Generated with Claude Code