A phone verification MCP server for your AI agent
Connect it once and your agent can ask whether a number is a live mobile, who carries it, and whether it belongs to a known litigator, then act on the answer. It can also send a whole list for a scrub you pay for by card. Works in Claude Code, Cursor, Claude Desktop (through the mcp-remote bridge), VS Code and Windsurf with nothing but your API key.
What your agent gets
The endpoint is https://numberbroom.com/mcp, Streamable HTTP. Every tool call becomes a call to the same REST API the developer docs describe, with your key, so limits and billing are identical.
Takes one US number in any common format. Returns carrier-level line type (mobile, landline, VoIP, toll-free or other), carrier name, an activity score with a likely-disconnected flag, and whether the number is a known litigator, plus a keep or drop outcome the agent can act on. A number that does not parse, or one outside the US (Canada, Puerto Rico, the Caribbean, overseas), is answered without a lookup and not charged. Does not check Do Not Call registries.
Remaining pre-paid balance on the connected account, with the number of lookups it covers. Lets an agent check before a run and stop cleanly instead of failing mid-list.
Creates a Stripe Checkout link that adds $10 to $5,000 of credit to the connected account. Nothing is charged until a person opens the link and pays by card, so an agent that runs short can hand you the link instead of stopping.
Sends a whole list, up to 10,000 numbers, to the paid list scrub: the same scrub as a file upload, at list rates, so a big list costs less per number than one lookup at a time. Returns the price and a Stripe Checkout link a person pays by card; nothing runs until they do. Mode full removes litigators, disconnected lines and anything that is not a live mobile; litigator_only removes known litigators and numbers that are not valid, and keeps landlines, VoIP and disconnected numbers. Numbers outside the US are left out of the price and the scrub.
Status of a list started with scrub_list: paid, running or done, the counts once it is done, and a download link for the cleaned file that lasts one hour.
Three steps, one of them in your editor
Create an account and add credit
Sign up with email or Google, then top up between $10 and $5,000 from Settings. Credit is pre-paid and never expires. It pays for every lookup; a whole list sent with scrub_list is paid separately, by card.
Generate a key for this client
In Settings, API access. Label it after the client, like "Cursor on laptop", and make a separate key for each one, so a leak is one revoke. It starts with nb_live_ and is shown once; only a hash is stored.
Add the server to your client
Pick your client below and paste the snippet with your key in place of nb_live_YOUR_KEY. Then ask your agent to verify a number.
Claude Code
One command. Claude Code speaks Streamable HTTP natively and passes the header on every request.
-
Run this in a terminal, with your key in place of the placeholder. Add
--scope userto make it available in every project rather than the current one.claude mcp add --transport http numberbroom https://numberbroom.com/mcp \ --header "Authorization: Bearer nb_live_YOUR_KEY" -
Or, for a repo the whole team shares, commit a
.mcp.jsonthat reads the key from an environment variable so it never lands in git:{ "mcpServers": { "numberbroom": { "type": "http", "url": "https://numberbroom.com/mcp", "headers": { "Authorization": "Bearer ${NUMBERBROOM_API_KEY}" } } } } Type
/mcpinside Claude Code to confirm numberbroom shows as connected, then ask: "Verify (202) 555-0143 and tell me whether it is safe to dial."
nb_live_ string, the server answers as it would a client with no key, by asking it to sign in instead. Fix the header rather than following a sign-in prompt.Cursor
Cursor reads MCP servers from a JSON file and supports a headers block on remote servers.
-
Create or open
~/.cursor/mcp.jsonfor every project, or.cursor/mcp.jsoninside one project, and add:{ "mcpServers": { "numberbroom": { "url": "https://numberbroom.com/mcp", "headers": { "Authorization": "Bearer nb_live_YOUR_KEY" } } } } In a project file that other people can see, write
"Bearer ${env:NUMBERBROOM_API_KEY}"instead and export the variable in your shell. Cursor resolves${env:NAME}insideheaders.Open Cursor's MCP settings, check that numberbroom is enabled with five tools listed, and ask the agent to verify a number.
Claude Desktop
The "Add custom connector" dialog only takes a URL and optional OAuth credentials; it has nowhere to put an API key. Claude Desktop does still run local stdio servers from its config file, so the open source mcp-remote bridge carries the header for you. Needs Node.js installed.
Open Settings, Developer, Edit Config. That opens
claude_desktop_config.json: on macOS it lives in~/Library/Application Support/Claude/, on Windows in%APPDATA%\Claude\.-
Add the server. The header value goes through an environment variable on purpose: Claude Desktop on Windows mangles a space inside an argument, and
Authorization:${AUTH_HEADER}with no space avoids it.{ "mcpServers": { "numberbroom": { "command": "npx", "args": [ "mcp-remote", "https://numberbroom.com/mcp", "--transport", "http-only", "--header", "Authorization:${AUTH_HEADER}" ], "env": { "AUTH_HEADER": "Bearer nb_live_YOUR_KEY" } } } } Quit and reopen Claude Desktop. The tools icon under the message box lists numberbroom once the bridge is up.
VS Code with GitHub Copilot
VS Code's MCP config supports HTTP servers with headers, and its inputs mechanism prompts for the key once and stores it as a secret instead of writing it into the file.
-
Add this to
.vscode/mcp.jsonin the workspace, or to your user-level file via the MCP: Open User Configuration command:{ "inputs": [ { "type": "promptString", "id": "numberbroom-key", "description": "NumberBroom API key (nb_live_...)", "password": true } ], "servers": { "numberbroom": { "type": "http", "url": "https://numberbroom.com/mcp", "headers": { "Authorization": "Bearer ${input:numberbroom-key}" } } } } Click Start above the server entry, paste the key when prompted, then use the tools from Copilot's agent mode.
Windsurf
Windsurf's Cascade reads remote servers from a JSON file with a serverUrl and a headers block.
-
Open
~/.codeium/windsurf/mcp_config.json(Cascade's MCP settings has a button for it) and add:{ "mcpServers": { "numberbroom": { "serverUrl": "https://numberbroom.com/mcp", "headers": { "Authorization": "Bearer nb_live_YOUR_KEY" } } } } Refresh the server list in Cascade and confirm all five tools appear.
Claude.ai, mobile apps and Cowork
No setup steps here yet, and we would rather say so than hand you instructions we have not seen work.
Use Claude Code or Claude Desktop today. Both hold the key on your machine and send it as a header, which works now. Their tabs are above.
If your team needs this inside claude.ai specifically, tell us. Knowing someone is waiting on it is what moves the test up the list.
ChatGPT
ChatGPT's MCP connectors, in Developer mode, accept OAuth or no authentication, and OpenAI's own docs say ChatGPT cannot present custom API keys. Since October 5, 2026, the server also accepts an OAuth sign-in to your NumberBroom account, but we have not yet completed a connection from ChatGPT end to end; setup steps for the connector will be published here once that test passes. What works today is the REST API, through a custom GPT Action with API key auth, and it returns the same fields.
Open the GPT editor, create a GPT, go to Configure, Actions, Create new action.
Set Authentication to API Key, Auth Type to Bearer, and paste your
nb_live_key. OpenAI encrypts it at rest and it never appears in the chat.-
Paste this schema. It describes the five REST endpoints the MCP tools wrap:
openapi: 3.1.0 info: title: NumberBroom phone verification version: "1.0" servers: - url: https://numberbroom.com/api paths: /v1/verify: post: operationId: verifyPhoneNumber summary: Verify one US phone number. Returns line type, carrier, activity score and known litigator status. Costs $0.20; an unparseable number or one outside the US is free. requestBody: required: true content: application/json: schema: type: object required: [phone] properties: phone: type: string description: The number in any common US format. responses: "200": description: Verification result. valid is false when the input is not a US phone number, or when its carrier data is withheld (reason suppressed). content: application/json: schema: type: object properties: phone: { type: string, description: The input as sent } e164: { type: [string, "null"], description: null when the input does not parse } valid: { type: boolean } reason: { type: string, enum: [unparseable, not_nanp, outside_us, suppressed], description: Present only when valid is false -- why the number was not checked. suppressed is permanent -- asking again returns the same } message: { type: string, description: Present only when valid is false } lineType: { type: string } carrier: { type: [string, "null"] } isLitigator: { type: [boolean, "null"], description: true when court filings tie the number to a TCPA/FDCPA/FCRA/CFPB suit (which law is not reported and no database catches every litigator) -- null when no litigator check ran -- the carrier lookup reported the number as not valid (outcome invalid) } litigator: { type: string, enum: [yes, no, not_checked], description: isLitigator as a word, for no-code tools that store and compare text (GoHighLevel, Google Sheets) -- not_checked when no litigator check ran } activityScore: { type: [number, "null"], description: 0 to 100; null when the carrier data has no signal } isLikelyDisconnected: { type: [boolean, "null"], description: true at an activity score of 30 or below; null when there is no score } outcome: { type: string } keep: { type: boolean } dncEvaluated: { type: boolean } charged: { type: number } creditsRemaining: { type: number } /v1/credits: get: operationId: getCreditBalance summary: Remaining pre-paid balance and how many lookups it covers. Free to call. responses: "200": description: Balance content: application/json: schema: type: object properties: credits: { type: number } ratePerLookup: { type: number } lookupsRemaining: { type: integer } dailyLimit: { type: integer, description: Lookups a day this key may make when called the way this request was } accountDailyLimit: { type: integer, description: Lookups a day across every key on the account } /v1/credits/checkout: post: operationId: createCreditCheckout summary: A Stripe Checkout link that adds credit to this key's account when a person pays it. Free to call. requestBody: required: true content: application/json: schema: type: object required: [amount] properties: amount: { type: number, description: Dollars of credit, 10 to 5000. } responses: "200": description: Open url to pay. Credit lands when the payment clears. content: application/json: schema: type: object properties: url: { type: string } sessionId: { type: string } /v1/lists: post: operationId: createListCheckout summary: Send a whole list for the paid list scrub. Returns the price at list rates and a Stripe Checkout link for a person to pay; nothing is taken from API credit. requestBody: required: true content: application/json: schema: type: object required: [numbers] properties: numbers: type: array description: Up to 10,000 phone numbers, digits and phone punctuation only. items: type: string scrubMode: { type: string, enum: [full, litigator_only] } responses: "200": description: The price and the link to pay it. The scrub runs once payment clears. content: application/json: schema: type: object properties: jobId: { type: string } url: { type: string } rowCount: { type: integer } chargedNumbers: { type: integer } outsideUsRows: { type: integer, description: "Numbers outside the US (Canada, Puerto Rico, the Caribbean, overseas): not priced or scrubbed, and returned in their own file once the scrub completes" } amount: { type: number } scrubMode: { type: string } message: { type: string } /v1/lists/{jobId}: get: operationId: getListStatus summary: Status and counts for one of this account's list scrubs, with a download link that lasts an hour once it is done. parameters: - name: jobId in: path required: true schema: type: string responses: "200": description: Counts appear once status is complete. content: application/json: schema: type: object properties: jobId: { type: string } status: { type: string } paid: { type: boolean } scrubMode: { type: string } totalNumbers: { type: [integer, "null"] } amount: { type: [number, "null"] } dashboardUrl: { type: string } cleanCount: { type: integer } litigatorCount: { type: integer } voipLandlineCount: { type: integer } disconnectedCount: { type: integer } invalidCount: { type: integer, description: Numbers that are not valid phone numbers (removed in either scrub mode); absent on lists scrubbed before 2026-10-05 } errorCount: { type: integer } ownDncCount: { type: integer } outsideUsRows: { type: integer, description: "Numbers outside the US, left out of the scrub and not charged; absent on lists scrubbed before 2026-10-05" } downloadUrl: { type: string } outsideUsDownloadUrl: { type: string, description: "Those numbers' rows, as sent, in a file of their own; present only when the list had any. Lasts as long as downloadUrl" } removedDownloadUrl: { type: string, description: "The removed rows, as sent, with line_type, carrier, activity_score and removed_because, known litigators included; never a Do Not Call Registry match. Present only when rows were removed. Lasts as long as downloadUrl" } litigatorDownloadUrl: { type: string, description: "The known litigator rows alone, as sent, with line_type, carrier and activity_score; present only when the list had any, and absent on lists scrubbed before 2026-10-14. Lasts as long as downloadUrl" } downloadExpiresAt: { type: string } Save, then test in the preview: "Verify (202) 555-0143." Every lookup spends the key owner's balance, so keep the GPT private to your team rather than publishing it to the store.
Any other MCP client
If your client can add a remote server over Streamable HTTP and attach a static header, it works. Most clients take a JSON block shaped like this; the field names vary slightly, the values do not.
-
{ "url": "https://numberbroom.com/mcp", "transport": "streamable-http", "headers": { "Authorization": "Bearer nb_live_YOUR_KEY" } } If the client only speaks stdio, run it through
npx mcp-remote https://numberbroom.com/mcp --header "Authorization: Bearer nb_live_YOUR_KEY", the same bridge the Claude Desktop tab uses.Searching for numberbroom in a client that browses the official MCP registry finds the same server: it is published there as
io.github.cameron-creations/numberbroom-mcp.
initialize request to it with your key in the Authorization header; the server answers with its name and version. Without a key it answers 401. Source and a test suite are at github.com/cameron-creations/numberbroom-mcp.Same price as the API, because it is the API
No subscription and no per-seat fee. Your agent spends the balance you loaded, one lookup at a time. A list sent with scrub_list is paid by card at list rates instead, never from that balance.
Line type, carrier, activity score and litigator flag, all in one call.
Pre-paid through Stripe. Credit does not expire.
Lookups a day per key through this server, unless you set the key's own limit, up to 5,000 lookups. The account stops at 5,000 a day across all of its keys. Resets at midnight UTC.
Unparseable numbers, numbers outside the US, failed carrier lookups, and every balance check.
Before you connect
Do I need OAuth or a separate developer account?
Does the MCP server store my key or the numbers I check?
What does a lookup cost, and when am I not charged?
What if an agent loops, or my key leaks?
Can my agent scrub a whole list through MCP?
Does a lookup check the Do Not Call registry?
Can I add it in claude.ai or ChatGPT?
Give your agent a phone it can trust
Add credit, generate a key, paste one snippet. The first lookup is a minute away and costs twenty cents.