Skip to main content
Glama
enzoemir1

leadpipe-mcp

Enrich Lead

lead_enrich
Idempotent

Enrich a lead with company name, industry, size, country, website, headcount, and tech stack using the email domain. Updates the lead in place for accurate scoring.

Instructions

Derive and attach company data to an existing lead using the email domain: company name, industry, size, country, website, estimated headcount, and common tech stack. Does not call external APIs — enrichment is driven by the built-in domain knowledge base. Updates the lead in place and returns the enriched record, ready for lead_score. Run this before lead_score for the best qualification accuracy.

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault
lead_idYesUUID of the lead to enrich (returned by lead_ingest or lead_search)

Implementation Reference

  • Main enrichment logic: extracts domain from email, checks freemail, fetches company info via Hunter.io API or domain heuristics (curated INDUSTRY_MAP, TLD hints, name keyword matching), estimates company size, and merges with existing lead data (user data wins). Updates lead status to 'enriched' and stores enriched_at timestamp.
    /**
     * Enrich an existing lead with company data.
     *
     * Merge rules (user data is sacred):
     *   - Any field the user already set on the lead (name, industry, etc.) is kept.
     *   - Enrichment only fills fields that are undefined on the existing lead.
     *   - Every field from the CompanyInfo schema is explicitly set to `null`
     *     when unknown, so the output shape is stable and discoverable.
     */
    export async function enrichLead(leadId: string, store?: Storage): Promise<Lead> {
      if (!RE_UUID.test(leadId)) throw new ValidationError(`Invalid lead ID format: ${leadId}`);
    
      const s = store ?? defaultStorage;
      const lead = await s.getLeadById(leadId);
      if (!lead) throw new NotFoundError('Lead', leadId);
    
      const domain = extractDomain(lead.email);
      const isFreemail = isFreemailDomain(domain);
      const existing = lead.company;
    
      let fetched: CompanyInfo;
      if (isFreemail) {
        fetched = { domain: existing?.domain };
      } else {
        fetched = await fetchCompanyFromDomain(domain, existing);
        const estimatedSize = estimateCompanySize(domain);
        if (estimatedSize && !fetched.size && !existing?.size) {
          fetched.size = estimatedSize;
        }
      }
    
      // Merge: existing user-provided data wins over heuristic fallbacks
      const pick = <T>(userVal: T | undefined, heuristic: T | undefined): T | undefined =>
        userVal !== undefined ? userVal : heuristic;
    
      const merged: CompanyInfo = {
        name: pick(existing?.name, fetched.name),
        domain: pick(existing?.domain, fetched.domain ?? (domain && !isFreemail ? domain : undefined)),
        industry: pick(existing?.industry, fetched.industry),
        size: pick(existing?.size, fetched.size),
        country: pick(existing?.country, fetched.country),
        description: pick(existing?.description, fetched.description),
        linkedin_url: pick(existing?.linkedin_url, fetched.linkedin_url),
        tech_stack: pick(existing?.tech_stack, fetched.tech_stack),
      };
    
      const updated = await s.updateLead(leadId, {
        company: merged,
        status: lead.status === 'new' ? 'enriched' : lead.status,
        enriched_at: new Date().toISOString(),
      });
    
      return updated!;
    }
  • Fetches company info from Hunter.io API (if HUNTER_API_KEY env var is set) or falls back to domain heuristics including curated INDUSTRY_MAP, TLD hints, and name keyword matching.
    /** Fetch company info from external API (Hunter.io) or fall back to domain heuristics. */
    async function fetchCompanyFromDomain(
      domain: string,
      existing: CompanyInfo | undefined,
    ): Promise<CompanyInfo> {
      const info: CompanyInfo = { domain };
    
      // Try Hunter.io if API key is set
      const hunterKey = process.env.HUNTER_API_KEY;
      if (hunterKey) {
        try {
          const url = `https://api.hunter.io/v2/domain-search?domain=${encodeURIComponent(domain)}&api_key=${hunterKey}`;
          const controller = new AbortController();
          const timer = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
          const res = await fetch(url, { signal: controller.signal }).finally(() => clearTimeout(timer));
          if (res.ok) {
            const data = await res.json() as any;
            const d = data?.data;
            if (d) {
              info.name = d.organization ?? undefined;
              info.industry = d.industry ?? undefined;
              info.country = d.country ?? undefined;
              info.description = d.description ?? undefined;
              info.linkedin_url = d.linkedin ?? undefined;
              if (d.technologies && Array.isArray(d.technologies)) {
                info.tech_stack = d.technologies;
              }
            }
          }
        } catch {
          // Hunter.io failed — continue with fallback
        }
      }
    
      // Only set a fallback name if the user did NOT already provide one.
      // This preserves user-provided casing and spelling (e.g. "VelocityAI" vs "Velocityai").
      if (!existing?.name && !info.name) {
        const parts = domain.split('.');
        info.name = parts[0].charAt(0).toUpperCase() + parts[0].slice(1);
      }
    
      // Industry resolution: curated map → TLD heuristic → name keyword heuristic
      if (!info.industry) {
        info.industry =
          INDUSTRY_MAP[domain] ??
          guessIndustry(domain, existing?.name ?? info.name) ??
          undefined;
      }
    
      return info;
    }
  • src/index.ts:179-209 (registration)
    Registers the 'lead_enrich' tool with the MCP server. Input schema requires a single lead_id (UUID). The handler calls ensureProOrReject for license enforcement, then delegates to enrichLead from the enrichment service.
    // ━━━ TOOL: lead_enrich ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
    server.registerTool(
      'lead_enrich',
      {
        title: 'Enrich Lead',
        description:
          'Derive and attach company data to an existing lead using the email domain: company name, industry, size, country, website, estimated headcount, and common tech stack. Does not call external APIs — enrichment is driven by the built-in domain knowledge base. Updates the lead in place and returns the enriched record, ready for lead_score. Run this before lead_score for the best qualification accuracy.',
        inputSchema: z.object({
          lead_id: z.string().uuid().describe('UUID of the lead to enrich (returned by lead_ingest or lead_search)'),
        }),
        annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false },
      },
      async ({ lead_id }) => {
        try {
          const reject = await ensureProOrReject(LICENSE_CONFIG, 'lead_enrich');
          if (reject) return reject;
          const lead = await enrichLead(lead_id);
          return {
            content: [
              {
                type: 'text' as const,
                text: `Lead enriched: ${lead.email} — Company: ${lead.company?.name ?? 'unknown'}, Industry: ${lead.company?.industry ?? 'unknown'}, Size: ${lead.company?.size ?? 'unknown'}`,
              },
            ],
            structuredContent: lead,
          };
        } catch (error) {
          return handleToolError(error);
        }
      }
    );
  • CompanyInfo schema defines the shape of enrichment data: name, domain, industry, size, country, description, linkedin_url, tech_stack. This is the output structure populated by lead_enrich.
    /** Information about a company. */
    export const CompanyInfoSchema = z.object({
      name: z.string().optional(),
      domain: z.string().optional(),
      industry: z.string().optional(),
      size: CompanySizeSchema.optional(),
      country: z.string().optional(),
      description: z.string().optional(),
      linkedin_url: z.string().optional(),
      tech_stack: z.array(z.string()).optional(),
    });
    export type CompanyInfo = z.infer<typeof CompanyInfoSchema>;
  • Estimates company size based on domain name, returning size brackets like '5000+', '1001-5000', '201-500', or default '51-200' for unknown domains.
    /** Estimate company size from domain. Returns a default midmarket bracket for unknown domains. */
    function estimateCompanySize(domain: string): CompanyInfo['size'] {
      // Well-known large companies
      const large = ['google.com', 'microsoft.com', 'apple.com', 'amazon.com', 'meta.com', 'salesforce.com'];
      if (large.includes(domain)) return '5000+';
    
      const midLarge = ['stripe.com', 'shopify.com', 'hubspot.com', 'atlassian.com', 'twilio.com', 'databricks.com', 'snowflake.com'];
      if (midLarge.includes(domain)) return '1001-5000';
    
      const mid = ['vercel.com', 'notion.so', 'linear.app', 'figma.com', 'anthropic.com'];
      if (mid.includes(domain)) return '201-500';
    
      // Unknown domain — assume small-to-mid market (51-200). This is the default
      // bracket for "unknown small company" and lets scoring treat it as neutral
      // rather than penalizing it to zero.
      return '51-200';
    }
Behavior4/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Annotations indicate idempotent, non-destructive, mutable. Description adds that it updates the lead in place and returns the enriched record, aligning with annotations. Could specify behavior if domain not found, but overall sufficient.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

Three sentences efficiently cover purpose, behavior, and sequencing advice. No redundant information, every sentence adds value.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness4/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

For a single-param tool with no output schema, the description covers input, behavior, and recommendation. Minor gap: no mention of error cases (e.g., missing domain). Still comprehensive.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema describes lead_id as UUID from lead_ingest/lead_search. Description adds context that enrichment uses email domain, implying lead must have one. This adds value beyond schema, so above baseline of 3.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description clearly states the tool enriches a lead with company data using the email domain, listing specific fields. It distinguishes from siblings by advising to run before lead_score for accuracy.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines4/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

Provides context that enrichment uses built-in knowledge base (no external API) and recommends ordering before lead_score. Could explicitly mention when not to use or alternative tools, but current guidance is clear.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

Install Server

Other Tools

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/enzoemir1/leadpipe-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server