feat: leadbay_import_leads MCP/OpenClaw tool (#3537)#19
Merged
Conversation
Add LeadbayClient.requestRawBinary() for non-JSON uploads (CSV, future binary). Mirrors request() exactly (auth, semaphore, error mapping, _lastMeta, mock-mode) with a caller-supplied Content-Type and Buffer/string body. Backs the upcoming leadbay_import_leads composite which posts CSV to /imports. Add file-import wire types matching the live API (snake_case, probed 2026-04-28 against api-us): FileImportPayloadV15, MappingsPayload, ImportRecordPayload, PreProcessing/Processing payloads, PaginatedResponse. Extend ToolContext with optional signal?: AbortSignal so long-running composites can honor caller cancellation. 4 new client tests cover requestRawBinary parity (auth, 401/403/429 mapping, Content-Type, _lastMeta).
Wraps Leadbay's existing CSV-import wizard so external automations (CRM,
analytics, email correspondents) can hand a list of company domains to
Leadbay and get back stable leadIds for downstream chaining into
leadbay_bulk_qualify_leads / leadbay_research_lead.
Surface (locked):
Input { domains: [{domain, name?}], dry_run?, per_phase_budget_ms?, total_budget_ms? }
Output { leads, not_imported: [{domain, reason}], importIds, region, _meta, cancelled? }
Internal flow (the wedge): preflight admin check → normalize+dedupe →
chunk at 100 → per-chunk: synthesize CSV (RFC 4180 + formula-injection
guard, MCP_ROW_ID column) → POST /imports → poll preprocess → commit
mappings (skip in dry_run) → poll process → poll records to terminal
(stabilization across 2 polls; treats match_type=NO_MATCH as terminal
since the wizard leaves those records IMPORTING forever) → reconcile by
MCP_ROW_ID with normalized-domain fallback. AbortSignal honored between
awaits; importId is captured immediately after POST so callers always
have it on cancel/timeout.
8 typed error codes (IMPORT_PREPROCESS_FAILED, IMPORT_PROCESSING_FAILED,
IMPORT_BUDGET_EXHAUSTED, IMPORT_NOT_TERMINAL, IMPORT_ADMIN_REQUIRED,
IMPORT_BILLING_REQUIRED, IMPORT_PAGINATION_RUNAWAY, IMPORT_EMPTY_INPUT)
plus per-domain not_imported.reason (malformed | no_match | uncrawled |
ambiguous | internal_error | dry_run).
Limitation: the wizard only matches against the existing crawler-built
lead universe. Uncrawled domains land in not_imported with
reason="uncrawled"; the tool does NOT create new leads. Backend
follow-up tracked at leadbay/product#3538 (programmatic /1.5/leads/import
with crawler dispatch).
35 unit tests + 1 smoke suite cover normalize/dedupe, CSV escaping
(injection guard with whitespace-prefix trim, RFC 4180 quoting), happy
path, dry_run, preprocess errors, >100-input chunking, and admin
preflight.
Closes leadbay/product#3537.
@leadbay/mcp registers leadbay_import_leads via the existing compositeWriteTools catalogue, gated by LEADBAY_MCP_WRITE=1. @leadbay/leadclaw exposes it under exposeWrite=true (manifest updated). Both follow the same pattern as bulk_qualify_leads / report_outreach. Server test asserts the tool is hidden by default and exposed when includeWrite=true. Contract test asserts manifest ↔ code parity and that the tool is gated behind exposeWrite.
Ships leadbay_import_leads. CHANGELOGs explicitly call out that this is a write tool that mutates user state (creates a row in the user's CRM-imports list per call, touches onboarding state). Suitable for occasional automation, not high-cadence (>5 calls/day) — backend follow-up tracked at leadbay/product#3538 will lift this restriction. mcp/README adds an "Importing domains from external systems → leadIds" section under Advanced with the LEADBAY_MCP_WRITE=1 quickstart and the side-effect / wedge-limitation disclosures. 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
Adds
leadbay_import_leads— a new MCP/OpenClaw composite write tool that accepts a list of company domains and returns LeadbayleadIds for ones the crawler already knows. Output chains naturally intoleadbay_bulk_qualify_leads({ leadIds })andleadbay_research_lead. Closes leadbay/product#3537.Implementation strategy (post
/autoplandual-voice review): wraps Leadbay's existing CRM-import wizard atPOST /1.5/imports. Live API was probed end-to-end (17 candidate paths) before any code; backend has no clean domain-import endpoint, so this PR ships the wedge while leadbay/product#3538 tracks the proper async-import-with-crawl backend follow-up.Foundation (commit 1):
LeadbayClient.requestRawBinary()— mirrorsrequest()exactly (auth, semaphore, error mapping,_lastMeta,LEADBAY_MOCK=1parity) for non-JSON uploadsToolContext.signal?: AbortSignalfor caller cancellationComposite tool (commit 2):
not_imported.reasonenumMCP/OpenClaw exposure (commit 3):
LEADBAY_MCP_WRITE=1(MCP) andexposeWrite=true(OpenClaw)Docs + version bump (commit 4):
packages/mcp/README.mdAdvancedTool surface
Test Coverage
35 new unit tests for the composite + 4 new parity tests for
requestRawBinary+ 1 new live smoke suite. Total: 186 tests passing across all packages.Coverage covers: normalize/dedupe, CSV escaping (RFC 4180 + injection guard with whitespace-trim), happy path, preprocess errors, dry_run path, >100-input chunking, admin preflight, requestRawBinary auth/error/meta parity, contract gating, MCP server registration. Concurrency / retry-storm tests deferred to backend follow-up.
Pre-Landing Review
A pre-landing reviewer subagent and Codex adversarial pass independently identified concerns. 6 fixes auto-applied:
lastRecords+ unreachable code removedcheckAbortedstyle consistencyChunkRunOutputshrunk to{importId, records}(dropped 3 dead fields)lookup.duplicatescollection removed (dedupe is silent by design)\" =HYPERLINK(...)\")IMPORT_PAGINATION_RUNAWAYoff-by-one fixed (was firing on legittotalPages == maxPagesPerPollboundary; now uses an explicitexhaustedPaginationflag)importIdis captured the momentPOST /importssucceeds (was lost on abort/timeout mid-poll, leaving orphan wizard rows untraceable)Live e2e Verification (milstan@leadbay.ai, US backend)
IMPORT_EMPTY_INPUT✓not_imported.reason=\"malformed\"✓dry_run(2 domains) → preprocess only, allreason=\"dry_run\", importId returned, noupdate_mappingscall ✓reason=\"uncrawled\"✓ (latest run: 11.9s)Known Limitations (v1)
startFileless,updateOnboardingStep(PROCESSING)). Suitable for occasional automation, not high-cadence (>5 calls/day). Backend follow-up #3538 covers the clean async endpoint.not_importedwithreason=\"uncrawled\". The wedge cannot create new leads for unknown websites; the caller decides what to do.Test plan
pnpm -r typecheck(all 4 packages clean)pnpm -r build(core/mcp/leadclaw + 1428KB DXT bundle)pnpm -r test(145 core + 12 leadclaw + 29 mcp = 186 tests passing)🤖 Generated with Claude Code