Developers

LeadSmarts MCP server

Let your AI agent qualify leads. LeadSmarts speaks the Model Context Protocol over HTTP: an agent submits leads, follows the research, reads Hot, Warm, and Cold verdicts with sourced evidence, and drafts outreach, all through 8 self-describing tools.

Introduction

LeadSmarts researches each lead on the live web against the workspace's business context and returns a verdict with scored, sourced evidence. The MCP server exposes that to agents: Claude Code, agents built on the Claude API, or any MCP client that supports the streamable HTTP transport with a bearer header.

The server is stateless and self-describing. Once connected, an agent reads the tool list, and every tool result includes a plain-language hint saying what to do next, so an agent needs no extra prompting to run the full loop.

Server URLhttps://bjmsxprjidjdyquiroxy.supabase.co/functions/v1/mcp-server
TransportMCP streamable HTTP, stateless (POST only)
AuthAuthorization: Bearer lsk_… (per-workspace API key)
Toolsget_account_status, list_batches, get_batch_status, get_results (read); import_leads, decide_crm_match, retry_lead, generate_outreach (import access)

LeadSmarts never sends outreach and never writes to your CRM without the note mode you chose in the app. An agent gets drafts and findings; sending stays with a human or the tool they choose.

Quickstart

1. Create an API key

In the LeadSmarts app, open API & agents and create a key. Choose Read only for an agent that only reads findings, or Read and import for one that submits leads, which spends credits. Keys with import access also carry a default policy for leads that match your CRM (see CRM matches). The key is shown once; copy it into your agent's configuration.

2. Connect an agent

bash
claude mcp add --transport http leadsmarts https://bjmsxprjidjdyquiroxy.supabase.co/functions/v1/mcp-server \
  --header "Authorization: Bearer lsk_your_key_here"

3. Make the first call

An agent's first useful call is get_account_status: it returns the credits available and the batch limit, so the agent knows how many leads it can submit. From there the loop is import_leads, get_batch_status until the batch is complete, then get_results. In Claude Code, connect the server and ask:

text
Qualify the leads in saascon-scans.csv with LeadSmarts, wait for research to finish,
and give me the hot ones with the evidence behind each verdict.

Authentication and scopes

Every request carries a workspace API key as a bearer token. Keys start with lsk_, are created and revoked under API & agents, and are stored only as a hash, so a lost key cannot be recovered, only replaced. A revoked key stops working immediately. Requests without a valid key get HTTP 401 with a JSON-RPC error body and a WWW-Authenticate: Bearer header.

ScopeToolsNotes
readget_account_status, list_batches, get_batch_status, get_resultsEvery key. Reads credits, batches, progress, and findings.
writeimport_leads, decide_crm_match, retry_lead, generate_outreach"Read and import" keys. Importing spends credits; the other three are free.

A read-only key does not see the write tools in tools/list; calling one anyway returns an isError result explaining that the key is read-only. Keys act on behalf of the whole workspace: one key per agent, and revoke keys you stop using.

The research loop

  1. Check credits with get_account_status.
  2. Submit up to 1,000 leads with import_leads. The call returns at once with a batchId; research runs in the background and typically takes 2 to 10 minutes per batch.
  3. Follow progress with get_batch_status. While status is in_progress the result carries retryAfterSeconds (60); wait that long between calls rather than polling tightly. Leads finish individually, and completed ones can be read before the batch is done.
  4. Read findings with get_results, by batchId (paged) or by specific jobIds. Each result has the verdict, four scores from 0 to 100, a summary, source URLs, and per-criterion evidence, so an agent can write the reasoning back into its own system.
  5. Act: draft outreach with generate_outreach, resolve CRM matches with decide_crm_match, retry failures with retry_lead.

Batch statuses: in_progress (research running), needs_decision (every remaining lead waits for a CRM decision), complete (nothing left running), empty. Lead stages move through queued, crm_lookup, research, scoring and end in completed, failed, cancelled, or declined_after_crm_match; crm_match_review is a pause, not an end.

Credits

A workspace has a monthly plan allowance plus any purchased credits. One credit is spent per lead that is actually researched, and only when the research completes; a lead that fails or is cancelled costs nothing.

  • Served results. A lead researched less than 7 days ago under the current business context is not researched again: import_leads lists it in served with its jobId and age in hours, no credit is spent, and get_results with that jobId returns the findings. The window is measured from the moment the earlier research completed, to the hour.
  • Re-research after 7 days. Submitting a lead whose result is 7 or more days old researches it again and spends a credit on completion. This is how weekly or monthly re-checks work: just submit the list again.
  • Fresh rerun inside the window. Pass freshRerun: true to research a recent lead again anyway. It spends a credit.
  • In progress. A lead whose research is still running is listed in inProgress and is never researched twice at once, even with freshRerun.
  • Changed business context. If the workspace's business context changed since a result was produced, a re-submission researches again, since the earlier evaluation no longer applies.
  • Free tools. generate_outreach, retry_lead, and decide_crm_match never spend credits themselves. A retried lead, or one continued with research_anyway, spends one credit if it then completes, like any research.
  • Not enough credits. If a batch needs more credits than the workspace has, nothing is imported and the error says how many leads would fit. Only leads that will actually be researched count; served and in-progress leads do not.

CRM matches

Before researching, LeadSmarts checks each lead against the CRMs connected to the workspace (HubSpot, Salesforce, Pipedrive, Attio, Freshsales, or a custom MCP server). A match can mean the lead is already in a conversation, so what happens next is a policy, chosen per import with onCrmMatch or per key as its default:

PolicyEffectCredit
pauseThe lead stops at crm_match_review with the matched record attached, until a human in the app or the agent calls decide_crm_match.None until continued
skipThe lead ends as declined_after_crm_match.None
research_anywayResearch continues; the result still shows the CRM record.One on completion

pause is the default for new keys and the safest choice for an agent that has a human in the loop: surface the match, then decide. An unattended agent should pre-commit to skip or research_anyway so a batch never stalls.

If a CRM connection has stopped working, the affected leads fail with a plain message naming the connection and what to do (for example, that the Pipedrive connection is no longer authorized and should be reconnected under Integrations). The connection is also marked in the app. retry_lead succeeds once a human has reconnected it.

Rate limits

Credits bound how many leads a key can research. Two per-key hourly limits bound how much work a runaway agent can create:

ActionLimit
import_leads30 imports per key per hour. Prefer fewer, larger batches (up to 1,000 leads each).
generate_outreach60 outreach drafts per key per hour.

A rate-limited call returns an isError result that asks the agent to wait about 15 minutes. Polling get_batch_status is not rate-limited, but the tool asks for 60 seconds between calls; research takes minutes, and tighter polling learns nothing sooner.

Errors

The server follows the MCP convention of two error kinds:

  • Protocol errors are JSON-RPC errors: an unknown method or tool, or arguments that fail the tool's schema (code -32602, with the failing paths in data.issues). These mean the call itself was malformed.
  • Tool errors are successful responses with isError: true and a text block written for the agent to read and act on: a batch id from another workspace, not enough credits (with the billing URL), nothing to retry, a rate limit. The agent should read the message, adjust, and continue.
json
{
  "jsonrpc": "2.0",
  "id": 4,
  "result": {
    "content": [
      {
        "type": "text",
        "text": "This import needs 12 credits but the workspace has 5 left. Nothing was imported. Either import at most 5 leads, or ask the workspace owner to buy credits or upgrade at https://app.leadsmarts.app/billing."
      }
    ],
    "isError": true
  }
}

HTTP-level responses: 401 for a missing, invalid, or revoked key (JSON-RPC error body, code -32001); 405 for GET or DELETE, since the server holds no sessions and opens no server-initiated streams; 400 for a body that is not JSON.

Tool reference

All tools are called with the standard tools/call method. Results carry the JSON both as a text block and as structuredContent. Ids in the examples are illustrative.

get_account_statusread

Credits and limits for this LeadSmarts workspace: plan, credits remaining this period, purchased credits, and the maximum leads per batch. Call this before import_leads so you know how many leads can be researched.

Parameters

No parameters. Pass an empty object.

Example

json
{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "tools/call",
  "params": {
    "name": "get_account_status",
    "arguments": {}
  }
}

Returns (the structuredContent of the result; the same JSON is also in the text block):

json
{
  "plan": "starter",
  "creditsRemaining": 38,
  "monthlyAllowance": 50,
  "usedThisPeriod": 12,
  "purchasedCreditsRemaining": 0,
  "periodStart": "2026-09-01",
  "periodEnd": "2026-10-01",
  "maxLeadsPerBatch": 1000
}

list_batchesread

Recent research batches in this workspace, newest first, with per-batch progress counts. Use it to find a batchId for get_batch_status or get_results.

Parameters

NameTypeDescription
limitintegeroptionalHow many batches to return (default 20). min 1, max 50

Example

json
{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "tools/call",
  "params": {
    "name": "list_batches",
    "arguments": {
      "limit": 5
    }
  }
}

Returns (the structuredContent of the result; the same JSON is also in the text block):

json
{
  "batches": [
    {
      "id": "3f9a2c4e-6b7d-4e1f-9a2b-8c7d6e5f4a3b",
      "name": "SaaSCon badge scans",
      "source": "mcp",
      "totalLeads": 24,
      "createdAt": "2026-09-03T14:02:11Z",
      "status": "in_progress",
      "counts": {
        "total": 24,
        "completed": 19,
        "inProgress": 4,
        "awaitingCrmDecision": 1,
        "failed": 0,
        "cancelled": 0,
        "declined": 0
      }
    }
  ]
}

get_batch_statusread

Progress of one research batch: overall status, stage counts, and the current stage of every lead. While status is in_progress, wait retryAfterSeconds and call again. Once a lead is completed its findings are available from get_results even if the batch is not finished. Status needs_decision means leads matched CRM records and wait for decide_crm_match.

Parameters

NameTypeDescription
batchIduuidrequiredBatch id from import_leads or list_batches.

Example

json
{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "tools/call",
  "params": {
    "name": "get_batch_status",
    "arguments": {
      "batchId": "3f9a2c4e-6b7d-4e1f-9a2b-8c7d6e5f4a3b"
    }
  }
}

Returns (the structuredContent of the result; the same JSON is also in the text block):

json
{
  "batch": {
    "id": "3f9a2c4e-6b7d-4e1f-9a2b-8c7d6e5f4a3b",
    "name": "SaaSCon badge scans",
    "source": "mcp",
    "totalLeads": 24,
    "createdAt": "2026-09-03T14:02:11Z"
  },
  "status": "in_progress",
  "counts": {
    "total": 24,
    "completed": 19,
    "inProgress": 4,
    "awaitingCrmDecision": 1,
    "failed": 0,
    "cancelled": 0,
    "declined": 0
  },
  "hint": "Research is still running. Call get_batch_status again in about 60 seconds. Completed leads can already be read with get_results.",
  "leads": [
    {
      "jobId": "7c1d9e2f-3a4b-4c5d-8e6f-1a2b3c4d5e6f",
      "leadId": "b1c2d3e4-…",
      "stage": "completed",
      "lead": {
        "fullName": "Jane Rivera",
        "email": "jane@meridiansoftware.com",
        "company": "Meridian Software",
        "companyWebsite": "https://meridiansoftware.com",
        "jobTitle": "VP Marketing",
        "linkedinUrl": ""
      }
    },
    {
      "jobId": "9e8d7c6b-5a4f-4e3d-9c2b-1a0f9e8d7c6b",
      "leadId": "c2d3e4f5-…",
      "stage": "crm_match_review",
      "lead": {
        "fullName": "Omar Haddad",
        "email": "omar@northwind.io",
        "company": "Northwind",
        "companyWebsite": "https://northwind.io",
        "jobTitle": "Head of Sales",
        "linkedinUrl": ""
      },
      "crmMatch": {
        "provider": "hubspot",
        "recordType": "contact",
        "recordName": "Omar Haddad",
        "recordUrl": "https://app.hubspot.com/contacts/…"
      }
    }
  ],
  "retryAfterSeconds": 60
}

get_resultsread

Qualification findings for completed leads: Hot/Warm/Cold verdict, recommended action, scores (0-100), a research summary, and per-criterion evidence with source URLs. Pass a batchId (paged with limit and offset) or up to 100 jobIds. Leads that are still researching are omitted; check get_batch_status for them.

Parameters

NameTypeDescription
batchIduuidoptionalReturn results for every completed lead in this batch.
jobIdsarray of stringoptionalReturn results for these specific jobs (jobId values from get_batch_status). min 1, max 100
limitintegeroptionalPage size when using batchId (default 50). min 1, max 100
offsetintegeroptionalPage offset when using batchId (default 0). min 0

Example

json
{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "tools/call",
  "params": {
    "name": "get_results",
    "arguments": {
      "batchId": "3f9a2c4e-6b7d-4e1f-9a2b-8c7d6e5f4a3b",
      "limit": 50
    }
  }
}

Returns (the structuredContent of the result; the same JSON is also in the text block):

json
{
  "batchId": "3f9a2c4e-6b7d-4e1f-9a2b-8c7d6e5f4a3b",
  "results": [
    {
      "jobId": "7c1d9e2f-3a4b-4c5d-8e6f-1a2b3c4d5e6f",
      "leadId": "b1c2d3e4-…",
      "batchId": "3f9a2c4e-6b7d-4e1f-9a2b-8c7d6e5f4a3b",
      "lead": {
        "fullName": "Jane Rivera",
        "email": "jane@meridiansoftware.com",
        "company": "Meridian Software",
        "companyWebsite": "https://meridiansoftware.com",
        "jobTitle": "VP Marketing",
        "linkedinUrl": ""
      },
      "qualification": "hot",
      "recommendedAction": "prioritize",
      "scores": {
        "overallReadiness": 81,
        "productFit": 84,
        "buyingIntent": 76,
        "confidence": 88
      },
      "recommendedProduct": "Team plan",
      "alternativeProduct": "",
      "summary": "Meridian is hiring three SDRs and its VP Marketing has written publicly about outbound being guesswork; strong fit for the Team plan.",
      "sources": [
        "https://meridiansoftware.com/careers",
        "https://www.linkedin.com/posts/…"
      ],
      "evidence": {
        "productFit": [
          {
            "criterion": "Industry",
            "outcome": "confirmed_match",
            "awardedPoints": 20,
            "maxPoints": 20,
            "evidence": "B2B SaaS, per the company site",
            "sourceUrl": "https://meridiansoftware.com/about",
            "inferred": false,
            "missing": false,
            "conflictingEvidence": ""
          }
        ],
        "buyingIntent": [
          {
            "criterion": "Hiring for the buyer role",
            "outcome": "confirmed_match",
            "awardedPoints": 15,
            "maxPoints": 15,
            "evidence": "3 SDR openings posted 12 days ago",
            "sourceUrl": "https://meridiansoftware.com/careers",
            "inferred": false,
            "missing": false,
            "conflictingEvidence": ""
          }
        ],
        "disqualifiers": [],
        "missingCriteria": [
          "Tech stack"
        ]
      },
      "scoredAt": "2026-09-03T14:09:40Z"
    }
  ],
  "hasMore": false
}

import_leadsimport access

Submit leads for qualification research. Spends one credit per lead that is actually researched. A lead researched less than 7 days ago under the current business context is served from its existing result at no cost (returned in served; read it with get_results jobIds); a lead researched 7 or more days ago is researched again and costs a credit; a lead still being researched is returned in inProgress rather than researched twice. Returns a batchId at once; research then takes 2 to 10 minutes per batch, so follow up with get_batch_status and get_results. If the batch needs more credits than the workspace has, nothing is imported. onCrmMatch decides what happens when a lead is found in the connected CRM: "pause" waits for a human or decide_crm_match, "skip" declines it for free, "research_anyway" researches it; omit it to use the policy set on this API key. Max 1000 leads per call.

Parameters

NameTypeDescription
leadsarray of lead objectsrequiredmin 1, max 1,000
batchNamestringoptionalShown in the LeadSmarts app's history. Name it after the source, e.g. the event or list. max 120 chars
onCrmMatchpause | skip | research_anywayoptionalPolicy for leads that match a CRM record. Defaults to the policy set on this API key (pause unless changed).
freshRerunbooleanoptionalResearch leads again even if a result less than 7 days old exists (spends credits). Default false.

Lead object

A lead needs at least one of email, company, fullName, or linkedinUrl. More fields give better research.

FieldTypeNotes
fullNamestring
emailstring
companystring
companyWebsitestring
jobTitlestring
linkedinUrlstring
geostringLocation, if known.
industrystring

Example

json
{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "tools/call",
  "params": {
    "name": "import_leads",
    "arguments": {
      "leads": [
        {
          "fullName": "Jane Rivera",
          "email": "jane@meridiansoftware.com",
          "company": "Meridian Software",
          "jobTitle": "VP Marketing"
        },
        {
          "fullName": "Omar Haddad",
          "company": "Northwind",
          "companyWebsite": "northwind.io"
        },
        {
          "email": "priya@acme.example"
        }
      ],
      "batchName": "SaaSCon badge scans",
      "onCrmMatch": "pause"
    }
  }
}

Returns (the structuredContent of the result; the same JSON is also in the text block):

json
{
  "batchId": "3f9a2c4e-6b7d-4e1f-9a2b-8c7d6e5f4a3b",
  "leadsQueued": 2,
  "invalid": 0,
  "duplicates": 0,
  "served": [
    {
      "jobId": "7c1d9e2f-3a4b-4c5d-8e6f-1a2b3c4d5e6f",
      "completedAt": "2026-09-01T09:15:00Z",
      "ageHours": 52
    }
  ],
  "inProgress": [],
  "creditsRemaining": 36,
  "onCrmMatch": "pause",
  "retryAfterSeconds": 60,
  "hint": "Batch accepted with 2 leads queued (skipped: 1 served from a result less than 7 days old (no credit spent; researched 2 days 4 hours ago)). Research typically completes in 2 to 10 minutes. Call get_batch_status with this batchId in about 60 seconds, then get_results once leads are completed."
}

decide_crm_matchimport accessfree

Resolve leads paused because they matched a record in the connected CRM (stage crm_match_review in get_batch_status). Pass one jobId, or a batchId to decide every paused lead in that batch. research_anyway continues research and spends a credit per lead; declined ends the lead for free. Surface the CRM match to your user before deciding when you can.

Parameters

NameTypeDescription
jobIduuidoptionalOne paused lead (jobId from get_batch_status).
batchIduuidoptionalEvery paused lead in this batch.
decisionresearch_anyway | declinedrequired

Example

json
{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "tools/call",
  "params": {
    "name": "decide_crm_match",
    "arguments": {
      "jobId": "9e8d7c6b-5a4f-4e3d-9c2b-1a0f9e8d7c6b",
      "decision": "research_anyway"
    }
  }
}

Returns (the structuredContent of the result; the same JSON is also in the text block):

json
{
  "decided": 1,
  "decision": "research_anyway",
  "hint": "1 lead re-entered research. Call get_batch_status in about 60 seconds."
}

retry_leadimport accessfree

Retry leads whose research failed or was cancelled (stage failed or cancelled in get_batch_status). Pass one jobId, or a batchId to retry every failed or cancelled lead in that batch. The retry itself is free; as with any research, one credit is spent only if the lead then completes. Read the lead's error first: a CRM connection error needs a human to reconnect the CRM before a retry can succeed.

Parameters

NameTypeDescription
jobIduuidoptionalOne failed or cancelled lead (jobId from get_batch_status).
batchIduuidoptionalEvery failed or cancelled lead in this batch.

Example

json
{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "tools/call",
  "params": {
    "name": "retry_lead",
    "arguments": {
      "batchId": "3f9a2c4e-6b7d-4e1f-9a2b-8c7d6e5f4a3b"
    }
  }
}

Returns (the structuredContent of the result; the same JSON is also in the text block):

json
{
  "retried": 1,
  "retryAfterSeconds": 60,
  "hint": "1 lead re-entered research. Call get_batch_status in about 60 seconds."
}

generate_outreachimport accessfree

Draft personalized outreach for one completed lead, grounded only in the confirmed, sourced evidence from its research and the workspace's outreach tone and restricted claims. channel email returns a subject and body; channel linkedin returns a short connection note plus a message within LinkedIn's limits. style optionally overrides the workspace tone for this draft. Free: no credit is spent. The draft is saved in the LeadSmarts app so a human sees the same text. LeadSmarts never sends anything; hand the draft to your user or their sending tool.

Parameters

NameTypeDescription
jobIduuidrequiredA completed lead (jobId from get_batch_status or get_results).
channelemail | linkedinoptionalOutreach channel (default "email").
styleprofessional | friendly | directoptionalTone override for this draft. Omit to use the workspace's outreach tone.

Example

json
{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "tools/call",
  "params": {
    "name": "generate_outreach",
    "arguments": {
      "jobId": "7c1d9e2f-3a4b-4c5d-8e6f-1a2b3c4d5e6f",
      "channel": "email",
      "style": "friendly"
    }
  }
}

Returns (the structuredContent of the result; the same JSON is also in the text block):

json
{
  "jobId": "7c1d9e2f-3a4b-4c5d-8e6f-1a2b3c4d5e6f",
  "draftId": "5d4c3b2a-…",
  "channel": "email",
  "subject": "Your SDR hiring push at Meridian",
  "body": "Hi Jane,\n\nSaw Meridian is bringing on three SDRs this quarter and your note that outbound still feels like guesswork. …",
  "hint": "Give the subject and body to your user or their email tool. Nothing has been sent."
}

Protocol details

  • Transport: MCP streamable HTTP, stateless. Every request is one JSON-RPC 2.0 message posted to the server URL; the response is one JSON message. Notifications (no id) get HTTP 202 with no body. There is no session id to keep.
  • Methods: initialize, ping, tools/list, tools/call. Resources and prompts are not offered.
  • Protocol versions: 2025-11-25, 2025-06-18, 2025-03-26, 2024-11-05. A client asking for another version is answered with the latest.
  • Instructions: initialize returns server instructions that teach the agent the loop. They currently read:

LeadSmarts qualifies B2B leads: it researches each lead against the workspace's business context and returns a Hot/Warm/Cold verdict with scored, sourced evidence. Typical loop: get_account_status to check credits, import_leads to submit a batch (one credit per researched lead), get_batch_status to follow progress, then get_results to read findings for completed leads and carry them into your own system. Research takes minutes per lead. When get_batch_status reports in_progress, wait retryAfterSeconds before calling again rather than polling in a tight loop. A lead researched less than 7 days ago is served from its existing result for free; after 7 days a re-submission researches it again and costs a credit. Leads that match a record in the workspace's CRM pause for a decision unless import_leads was given an onCrmMatch policy (or the API key carries one); decide_crm_match resolves paused leads. generate_outreach drafts email or LinkedIn outreach for a completed lead at no cost; retry_lead re-runs failed or cancelled leads for free. LeadSmarts never sends outreach itself. Read-only keys see only the read tools.

json
{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "initialize",
  "params": {
    "protocolVersion": "2025-06-18",
    "capabilities": {},
    "clientInfo": {
      "name": "my-agent",
      "version": "1.0.0"
    }
  }
}

Registry and connectors

The server's manifest for the official MCP Registry uses the name app.leadsmarts/leadsmarts. Clients that browse the registry can connect from that listing by supplying a LeadSmarts API key for the Authorization header.

Connector directories that require OAuth sign-in (such as custom connectors in Claude.ai) are not supported yet; use a client that accepts a bearer header, as in the quickstart.

Changelog

  • 2026-09-04, server 3.0.0. New tools generate_outreach and retry_lead. The 7-day re-research rule: import_leads reports served and inProgress leads with job ids and result age; older results are researched again on re-submission. Per-key default for onCrmMatch. CRM connection failures now fail leads with a plain, actionable message. Registry manifest published in the repository.
  • 2026-09-03, server 2.0.0. import_leads with onCrmMatch policies, decide_crm_match, the write scope, and per-key import rate limit.
  • 2026-09-03, server 1.0.0. Read-only server: get_account_status, list_batches, get_batch_status, get_results. API keys under API & agents.

Questions or a client that does not connect? Contact us.