Skip to content

fix(mcp,core): #3504 silent npx exit + dead URL hints + score contracts - #15

Merged
milstan merged 2 commits into
mainfrom
milstan/mcp-connect-fix-and-e2e
Apr 21, 2026
Merged

fix(mcp,core): #3504 silent npx exit + dead URL hints + score contracts#15
milstan merged 2 commits into
mainfrom
milstan/mcp-connect-fix-and-e2e

Conversation

@milstan

@milstan milstan commented Apr 21, 2026

Copy link
Copy Markdown
Contributor

Summary

Three failure modes a user hits before getting any work done, fixed in one pass:

  • product#3504npx -y @leadbay/mcp exited 0 silently on Node 25 because isEntrypoint compared the npx shim symlink to the resolved dist/bin.js. Both sides now go through realpathSync. New test/smoke/npx-entrypoint.test.ts is the regression guard (reproduce-then-fix verified).
  • Replaced dead app.leadbay.ai URLs in error strings + READMEs with the real recovery commands. Hints now include --region <us\|fr> because the CLI refuses without it (anti-cross-region credential-leak guard).
  • avg_score_0_to_10 was lying — per-question AI agent scores are discrete boosts (-10/0/10/20). Renamed to avg_qualification_boost. JSDoc on AiAgentResponse.score, LeadPayload.score, and LeadPayload.ai_agent_lead_score now documents the real contract (similarity + qualification boost, combined and normalized server-side to 0-100).

Live smoke harness extended with composite-level checks + optional thinking-model judge writing a redacted report to .context/. Token-first via LEADBAY_TEST_TOKEN; macOS Keychain fallback (refuses plaintext password env var). Versions: core 0.2.0→0.2.1 (private), leadclaw + mcp 0.2.1→0.2.2 (openclaw plugin manifest synced).

Test plan

  • pnpm -r typecheck green across core / leadclaw / mcp
  • pnpm test — unit suite passes
  • pnpm --filter @leadbay/mcp test:smokenpx-entrypoint.test.ts passes; live.test.ts passes against real account (LEADBAY_TEST_TOKEN + ANTHROPIC_API_KEY)
  • Reproduce-then-fix gate for #3504: stash realpath fix → smoke fails with empty stdout; restore → passes
  • After merge: comment on product#3504 with PR link + before/after npx --help transcript

🤖 Generated with Claude Code

milstan and others added 2 commits April 21, 2026 14:30
…e contracts

Three failure modes a user hits before they get any work done, fixed in one pass.

1. #3504 — `npx -y @leadbay/mcp` silently exits 0 on Node 25. The `isEntrypoint`
   check compared `process.argv[1]` (the npx shim symlink) to `import.meta.url`
   (the resolved `dist/bin.js`); they never match, so `main()` never runs and
   the MCP host sees a dead connection with no diagnostic. Both sides now go
   through `realpathSync` so the symlink collapses. Reproduce-then-fix verified
   with a new `test/smoke/npx-entrypoint.test.ts` regression guard.

2. Dead `app.leadbay.ai` URLs in error strings and READMEs replaced with the
   real recovery commands (`leadbay-mcp install`, `leadbay-mcp login`). Hints
   now include `--region <us|fr>` because the CLI refuses without it (anti-
   cross-region credential-leak guard) — earlier draft would have dead-ended
   the user a second time.

3. `avg_score_0_to_10` on `pull_leads` / `bulk_qualify_leads` was lying: per-
   question AI agent scores are discrete boosts (-10/0/10/20), not 0-10
   averages. Renamed to `avg_qualification_boost` with JSDoc on the underlying
   `AiAgentResponse.score` interface explaining the real contract. Documented
   `score` (ICP similarity) vs `ai_agent_lead_score` (deep-AI qualification
   boost) — they're combined and normalized server-side to 0-100.

Live smoke harness extended with composite-level checks + optional thinking-
model judge that writes a redacted report to `.context/`. Token-first via
`LEADBAY_TEST_TOKEN`; macOS Keychain fallback (refuses plaintext password env
var, on purpose). Versions: core 0.2.0→0.2.1 (private), leadclaw + mcp
0.2.1→0.2.2 (openclaw plugin manifest synced).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Both branches independently bumped mcp/leadclaw to 0.2.2. Combined into a
single 0.2.2 release: this branch's npx silent-exit fix + dead URL hints +
score-contract correction PLUS main's mental-model docs (SERVER_INSTRUCTIONS
inbox/two-scoring-layers/daily-rhythm + per-tool description rewrites + the
new openclaw.plugin.json description). Single coherent release rather than
two near-simultaneous tags.

Conflicts resolved:
- packages/leadclaw/CHANGELOG.md: merged 0.2.2 entries.
- packages/leadclaw/openclaw.plugin.json: kept main's longer description.
- packages/mcp/CHANGELOG.md: merged 0.2.2 entries.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@milstan
milstan merged commit b4feaf5 into main Apr 21, 2026
1 check passed
ArtyETH06 added a commit that referenced this pull request Jun 8, 2026
… import pipeline

Live testing through Claude Desktop's actual server binary revealed the
import-pipeline add path (leadbay_import_and_qualify) returns 401 on the
test account — POST /imports is rejected even with a valid token, and the
agent surfaced it as a misleading "LEADBAY_TOKEN is no longer valid" error.

The direct endpoint the web UI uses — POST /leads/{id}/contacts — works with
the SAME token (verified 200, contact created with its id). So:

- New leadbay_add_contact composite write tool wrapping POST
  /leads/{id}/contacts. One call, no import/qualify quota, accepts
  first/last name + optional job_title/linkedin_page/email/phone. Pairs with
  leadbay_remove_contact (POST /contacts/{id}/archive).
- Revert the import_and_qualify routing/recipe changes — the add-contact
  intent now routes to leadbay_add_contact instead. (Kept one incidental fix:
  a stale leadbay_research_lead -> leadbay_research_lead_by_id reference.)
- research_lead_by_id anti-trigger now points at leadbay_add_contact.
- index / _composite-file-names / TOOLS_WITH_ROUTING registration,
  WORKFLOWS row #15 + contract updated, new unit test.

Verified live through Desktop's bin.js + token: add -> contact created (no
401); remove -> archived; company back to 0 contacts. Suite green
(core 324, promptforge 16, mcp 366).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
milstan pushed a commit that referenced this pull request Jun 10, 2026
…e / remove / pin / unpin) (#93)

* feat(contacts): add + remove a contact on a company from the conversation

Closes the workflow gap from leadbay/product#3703: reps prospecting through
Claude hit companies with no suggested contacts and left for Attio + Full
Enrich because there was no way to add a contact in-conversation.

Diagnosis: the backend already supported creating a contact (the import
pipeline's CONTACT_* fields, incl. CONTACT_LINKEDIN, create an org contact
keyed to a parent company). The gap was routing/discoverability, not a
missing endpoint.

Changes:
- Route the add-a-contact intent to leadbay_import_and_qualify: routing
  frontmatter + a single-contact-add recipe in its description, a reciprocal
  anti-trigger from research_lead_by_id, a WORKFLOWS row + eval contract.
- Fix the recipe: LEAD_NAME is required even when LEADBAY_ID is passed
  (the import rejects the row otherwise — caught by a live probe).
- New leadbay_remove_contact composite write tool — the undo for the add
  path. Wraps the backend's POST /contacts/{id}/archive (the same call the
  web UI's contact-delete fires; keyed by the contact's own id, soft-delete).
  Description + routing, index/_composite-file-names/TOOLS_WITH_ROUTING
  registration, WORKFLOWS row #16 + contract, unit test.

Verified live against a test account: add -> contact appears on the company;
remove -> contact gone. Full suite green (core 320, promptforge 16, mcp 366).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refactor(contacts): add via direct POST /leads/{id}/contacts, not the import pipeline

Live testing through Claude Desktop's actual server binary revealed the
import-pipeline add path (leadbay_import_and_qualify) returns 401 on the
test account — POST /imports is rejected even with a valid token, and the
agent surfaced it as a misleading "LEADBAY_TOKEN is no longer valid" error.

The direct endpoint the web UI uses — POST /leads/{id}/contacts — works with
the SAME token (verified 200, contact created with its id). So:

- New leadbay_add_contact composite write tool wrapping POST
  /leads/{id}/contacts. One call, no import/qualify quota, accepts
  first/last name + optional job_title/linkedin_page/email/phone. Pairs with
  leadbay_remove_contact (POST /contacts/{id}/archive).
- Revert the import_and_qualify routing/recipe changes — the add-contact
  intent now routes to leadbay_add_contact instead. (Kept one incidental fix:
  a stale leadbay_research_lead -> leadbay_research_lead_by_id reference.)
- research_lead_by_id anti-trigger now points at leadbay_add_contact.
- index / _composite-file-names / TOOLS_WITH_ROUTING registration,
  WORKFLOWS row #15 + contract updated, new unit test.

Verified live through Desktop's bin.js + token: add -> contact created (no
401); remove -> archived; company back to 0 contacts. Suite green
(core 324, promptforge 16, mcp 366).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(contacts): add pin + unpin contact tools

Round out the contact-management surface alongside add/remove. Both endpoints
verified live (204) and the web UI uses them.

- leadbay_pin_contact  → POST /contacts/{id}/pin   (mark a person as priority)
- leadbay_unpin_contact → POST /contacts/{id}/unpin (clear the pin)

One tool per file (pin-contact.ts / unpin-contact.ts) to satisfy the
COMPOSITE_FILE_TOOL_NAMES ↔ directory round-trip audit. Descriptions +
routing, index / _composite-file-names / TOOLS_WITH_ROUTING registration,
WORKFLOWS rows #17/#18 + contracts, unit tests.

Verified live through Desktop's bin.js + token: add → pin → unpin → remove
all return success. Suite green (core 329, promptforge 16, mcp 366).

Note: contact MODIFY has no working backend endpoint (PUT/PATCH
/contacts/{id} both 404), so edit-in-place is intentionally NOT included —
needs a backend route first.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(contacts): add leadbay_update_contact — edit a contact in place

Completes the contact-management surface (create/remove/pin/unpin/update).

Backend route cracked from the error messages: POST /contacts/{id}/update
with a SNAKE_CASE body that MUST include first_name + last_name (the endpoint
validates the full identity and 400s "invalid contact" on a partial body;
camelCase 400s "JSON deserialization error"). Edits in place — same id.

- leadbay_update_contact composite write tool (contact_id + first/last name
  required; job_title/linkedin_page/email/phone optional).
- Description + routing, index / _composite-file-names / TOOLS_WITH_ROUTING
  registration, WORKFLOWS row #19 + contract, unit test.

Verified live through Desktop's bin.js + token: add (OLD TITLE) → update →
title becomes SVP Engineering on the same contact id → remove. Suite green
(core 332, promptforge 16, mcp 366).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(contacts): correct remove-contact routing + allow null clears on update (review)

Two pre-landing review findings on PR #93:

- [P2] remove-contact anti-trigger + prose still pointed "add a contact" at
  leadbay_import_and_qualify (the old import path that 401s / burns quota).
  Repointed the anti-trigger to leadbay_add_contact and fixed the stale "undo
  for import_and_qualify" prose to reference leadbay_add_contact.

- [P2] update-contact input schema declared optional fields as type:"string"
  only, so MCP schema validation would reject a valid "clear this
  email/title/phone/linkedin" (null) update before execute ran — even though
  the params type + execute already forward null. Changed those four fields to
  type:["string","null"]. Verified live: POST /contacts/{id}/update with
  email:null / job_title:null returns 200 and clears the fields.

New test (update-contact-null-clear.test.ts): asserts null is sent on the wire
for a clear, untouched fields are omitted, and the schema declares the nullable
types.

Suite green (core 334, promptforge 16, mcp 366).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* docs(contacts): fix stale remove-contact docstring → leadbay_add_contact (review)

The remove-contact docstring still called the add path
leadbay_import_and_qualify; the add path was refactored to the direct
leadbay_add_contact endpoint. User-facing description was already fixed in
review; this code comment was the last stale reference.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refactor(contacts): move single-call contact tools composite/ → tools/ (review)

Milan: the 5 contact tools are each a single API call, so by the repo
convention (CLAUDE.md: "single relay call → granular") they're granular, not
composite. Moved add/update/remove/pin/unpin-contact from composite/ to tools/
(source, tests, and promptforge templates).

They stay registered in compositeWriteTools — same pattern as
likeLead/dislikeLead/addNote: granular-shaped tools kept on the DEFAULT write
surface so reps manage contacts in-conversation without LEADBAY_MCP_ADVANCED.
Dropped from COMPOSITE_FILE_TOOL_NAMES (no longer in composite/), so they also
shed the _triggered_by mandate — consistent with the other granular writes.

No behavior change: generated tool descriptions are byte-identical (promptforge
keys on `name:`, not directory). Suite green (core 353, promptforge 16,
mcp 370; composite-file-names round-trip audit passes).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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