Ingest Lead
lead_ingestAdd a lead to the pipeline with email and optional details like name, company, source, tags, and custom fields. Returns a UUID and status. Duplicate emails return an error.
Instructions
Add a single lead to the pipeline. Required: email. Optional: first_name, last_name, job_title, company_name, phone, source ("website"|"linkedin"|"referral"|"event"|"cold_outreach"|"partner"|"other"), tags (string array), custom_fields. Returns the stored lead object with a generated UUID, initial status="new", created_at, and a null score (run lead_score to populate). Throws a duplicate error if the email is already in the pipeline — use lead_search first if you need upsert behaviour.
Input Schema
| Name | Required | Description | Default |
|---|---|---|---|
| Yes | Business email address. Required and used as the unique key — duplicate emails are rejected, not upserted. Example: "alex@acme.com". | ||
| first_name | No | Optional first name. Stored verbatim and used for personalization in CRM exports. | |
| last_name | No | Optional last name. Combined with first_name to populate full_name on the stored lead. | |
| phone | No | Optional phone number in any format. Stored verbatim and forwarded to CRM exports. | |
| job_title | No | Job title used by lead_score for the job_title dimension. High-value titles (ceo, cto, vp, director, head, founder) earn the highest points. Configurable via config_scoring.high_value_titles. | |
| company_name | No | Company display name. If omitted, lead_enrich will derive it from the email domain. | |
| company_domain | No | Company root domain (e.g. "acme.com"). If omitted, derived from the email. Used by lead_enrich for the domain knowledge-base lookup. | |
| source | No | Where the lead originated. One of: website_form, landing_page, api, csv_import, manual, webhook. Defaults to "api". | api |
| source_detail | No | Free-text refinement of source — e.g. "homepage hero form", "Q1 webinar", "Reddit r/SaaS post". | |
| tags | No | Free-form tags for downstream filtering in lead_search and lead_export. Example: ["enterprise", "follow_up", "demo_requested"]. | |
| custom_fields | No | Arbitrary string→string metadata. Use for UTM parameters, A/B test variants, or anything you want to preserve through scoring and export. |
Implementation Reference
- src/index.ts:127-153 (registration)Registration of the 'lead_ingest' tool on the MCP server with title, description, input schema, and handler calling ingestLead().
// ━━━ TOOL: lead_ingest ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ server.registerTool( 'lead_ingest', { title: 'Ingest Lead', description: 'Add a single lead to the pipeline. Required: email. Optional: first_name, last_name, job_title, company_name, phone, source ("website"|"linkedin"|"referral"|"event"|"cold_outreach"|"partner"|"other"), tags (string array), custom_fields. Returns the stored lead object with a generated UUID, initial status="new", created_at, and a null score (run lead_score to populate). Throws a duplicate error if the email is already in the pipeline — use lead_search first if you need upsert behaviour.', inputSchema: LeadIngestInputSchema, annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false }, }, async (input) => { try { const lead = await ingestLead(input); return { content: [ { type: 'text' as const, text: `Lead ingested: ${lead.email} (id: ${lead.id}, status: ${lead.status})`, }, ], structuredContent: lead, }; } catch (error) { return handleToolError(error); } } ); - src/tools/ingest.ts:6-45 (handler)The core handler function `ingestLead()` that creates a Lead object from input, checks for duplicate email via storage, generates a UUID, and stores it.
export async function ingestLead(input: LeadIngestInput): Promise<Lead> { const existing = await storage.getLeadByEmail(input.email); if (existing) { throw new DuplicateError('email', input.email); } const now = new Date().toISOString(); const lead: Lead = { id: uuidv4(), email: input.email, first_name: input.first_name, last_name: input.last_name, full_name: input.first_name && input.last_name ? `${input.first_name} ${input.last_name}` : input.first_name ?? input.last_name, phone: input.phone, job_title: input.job_title, company: input.company_name || input.company_domain ? { name: input.company_name, domain: input.company_domain, } : undefined, source: input.source ?? 'api', source_detail: input.source_detail, tags: input.tags ?? [], custom_fields: input.custom_fields ?? {}, score: null, score_breakdown: null, status: 'new', created_at: now, updated_at: now, enriched_at: null, scored_at: null, exported_at: null, }; return storage.addLead(lead); } - src/models/lead.ts:140-152 (schema)Zod schema `LeadIngestInputSchema` defining the input validation for lead_ingest: email (required), and optional fields like first_name, last_name, phone, job_title, company_name, company_domain, source, source_detail, tags, custom_fields.
export const LeadIngestInputSchema = z.object({ email: z.string().email().describe('Business email address. Required and used as the unique key — duplicate emails are rejected, not upserted. Example: "alex@acme.com".'), first_name: z.string().optional().describe('Optional first name. Stored verbatim and used for personalization in CRM exports.'), last_name: z.string().optional().describe('Optional last name. Combined with first_name to populate full_name on the stored lead.'), phone: z.string().optional().describe('Optional phone number in any format. Stored verbatim and forwarded to CRM exports.'), job_title: z.string().optional().describe('Job title used by lead_score for the job_title dimension. High-value titles (ceo, cto, vp, director, head, founder) earn the highest points. Configurable via config_scoring.high_value_titles.'), company_name: z.string().optional().describe('Company display name. If omitted, lead_enrich will derive it from the email domain.'), company_domain: z.string().optional().describe('Company root domain (e.g. "acme.com"). If omitted, derived from the email. Used by lead_enrich for the domain knowledge-base lookup.'), source: LeadSourceSchema.default('api').describe('Where the lead originated. One of: website_form, landing_page, api, csv_import, manual, webhook. Defaults to "api".'), source_detail: z.string().optional().describe('Free-text refinement of source — e.g. "homepage hero form", "Q1 webinar", "Reddit r/SaaS post".'), tags: z.array(z.string()).optional().describe('Free-form tags for downstream filtering in lead_search and lead_export. Example: ["enterprise", "follow_up", "demo_requested"].'), custom_fields: z.record(z.string(), z.string()).optional().describe('Arbitrary string→string metadata. Use for UTM parameters, A/B test variants, or anything you want to preserve through scoring and export.'), });