Model Context Protocol

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.

Your API key, in a header $0.20 a lookup, nothing for a bad number Open source, MIT Listed in the official MCP registry

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.

verify_phone_number

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.

Unparseable or outside the US: $0
get_credit_balance

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.

Free to call
buy_credits

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.

Free to call
scrub_list

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.

Never from credit
get_list_status

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.

Read only
What a turn looks like
youIs (202) 555-0143 safe to put on tomorrow's dial list?
agentcalls verify_phone_number { "phone": "(202) 555-0143" }
tool+12025550143: mobile on Verizon Wireless. Charged $0.2. { "isLitigator": false, "activityScore": 82, "outcome": "clean", "keep": true, ... }
agentYes. It is an active Verizon mobile, activity score 82, and it is not on the litigator list. Note that this did not check the National Do Not Call Registry.

Three steps, one of them in your editor

1

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.

2

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.

3

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.

  1. Run this in a terminal, with your key in place of the placeholder. Add --scope user to make it available in every project rather than the current one.

    terminal
    claude mcp add --transport http numberbroom https://numberbroom.com/mcp \
      --header "Authorization: Bearer nb_live_YOUR_KEY"
  2. Or, for a repo the whole team shares, commit a .mcp.json that reads the key from an environment variable so it never lands in git:

    .mcp.json
    {
      "mcpServers": {
        "numberbroom": {
          "type": "http",
          "url": "https://numberbroom.com/mcp",
          "headers": { "Authorization": "Bearer ${NUMBERBROOM_API_KEY}" }
        }
      }
    }
  3. Type /mcp inside Claude Code to confirm numberbroom shows as connected, then ask: "Verify (202) 555-0143 and tell me whether it is safe to dial."

If the header is missing or the key is not the whole 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.

  1. Create or open ~/.cursor/mcp.json for every project, or .cursor/mcp.json inside one project, and add:

    mcp.json
    {
      "mcpServers": {
        "numberbroom": {
          "url": "https://numberbroom.com/mcp",
          "headers": { "Authorization": "Bearer nb_live_YOUR_KEY" }
        }
      }
    }
  2. 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} inside headers.

  3. 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.

  1. 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\.

  2. 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.

    claude_desktop_config.json
    {
      "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" }
        }
      }
    }
  3. Quit and reopen Claude Desktop. The tools icon under the message box lists numberbroom once the bridge is up.

This is the desktop app only. The claude.ai website, mobile apps and Cowork connect from Anthropic's cloud and cannot use this file; see the Claude.ai tab.

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.

  1. Add this to .vscode/mcp.json in the workspace, or to your user-level file via the MCP: Open User Configuration command:

    .vscode/mcp.json
    {
      "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}" }
        }
      }
    }
  2. 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.

  1. Open ~/.codeium/windsurf/mcp_config.json (Cascade's MCP settings has a button for it) and add:

    mcp_config.json
    {
      "mcpServers": {
        "numberbroom": {
          "serverUrl": "https://numberbroom.com/mcp",
          "headers": { "Authorization": "Bearer nb_live_YOUR_KEY" }
        }
      }
    }
  2. 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.

Claude's custom connectors connect to a remote server from Anthropic's cloud and authenticate with OAuth or with nothing at all; the dialog has no place for an API key. Since October 5, 2026, the server also accepts an OAuth sign-in to your NumberBroom account for connectors like this one, but we have not yet completed a connection from claude.ai end to end. Setup steps will be published here once that test passes.
  1. 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.

  2. 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.

  1. Open the GPT editor, create a GPT, go to Configure, Actions, Create new action.

  2. 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.

  3. Paste this schema. It describes the five REST endpoints the MCP tools wrap:

    openapi.yaml
    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 }
  4. 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.

A GPT can use either apps or actions, not both, so this action lives in a GPT of its own. The full REST reference is on the developer page.

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.

  1. connection
    {
      "url": "https://numberbroom.com/mcp",
      "transport": "streamable-http",
      "headers": { "Authorization": "Bearer nb_live_YOUR_KEY" }
    }
  2. 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.

  3. 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.

To check the endpoint is up without a client, POST an 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.

Per lookup
$0.20

Line type, carrier, activity score and litigator flag, all in one call.

Top-up
$10 to $5,000

Pre-paid through Stripe. Credit does not expire.

Daily limit over MCP
500

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.

Never charged
$0

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?
Not for a client that can send a header. Claude Code, Cursor, VS Code, Windsurf and Claude Desktop (through the stdio bridge) need only your NumberBroom API key, sent as a Bearer token in the connection's Authorization header; generate it in Settings after adding credit. Connectors that cannot send a header, such as claude.ai and ChatGPT, would use an OAuth sign-in to your NumberBroom account instead. The server has supported that since October 5, 2026, and we are still testing it with those two. Either way there is no app registration and no per-seat plan.
Does the MCP server store my key or the numbers I check?
Not when you connect with an API key in a header. Each tool call forwards your Authorization header to the same REST API the developer docs describe and returns the answer, and a fresh server instance is built per request so nothing crosses between callers. An OAuth sign-in works differently by design: NumberBroom creates a key for that connection, and the server keeps it encrypted with the sign-in grant, so the app holds a token and never the key. Every connection made that way is listed in Settings under Connected apps, where Disconnect revokes its key. The server itself stores no numbers, and its source is public under the MIT licence.
What does a lookup cost, and when am I not charged?
verify_phone_number costs $0.20 against your pre-paid balance, the same as one REST call. A number that does not parse as a US phone number, or one outside the US, is answered without a lookup and costs nothing, a lookup the carrier data provider cannot complete is refunded, and get_credit_balance and get_list_status never spend credit. buy_credits charges nothing until a person pays its link, and a list sent with scrub_list is paid by card at list rates, never from credit. Through this server a key stops at 500 lookups a day unless you give it its own limit in Settings, up to 5,000 lookups, and an account stops at 5,000 lookups a day across all of its keys. Credit is topped up between $10 and $5,000 and does not expire.
What if an agent loops, or my key leaks?
Three limits hold. A key used through this server with no limit of its own stops at 500 lookups a day, which is $100 at most, and you can set any key lower in Settings, API access. The account stops at 5,000 lookups a day across all of its keys, however many it holds. Keys are one per client, up to 10, so revoking a leaked one takes effect on its next call and leaves your other clients working. A stolen key cannot raise its own limit or mint another: key management needs your signed-in account.
Can my agent scrub a whole list through MCP?
Yes, with scrub_list. It sends up to 10,000 numbers to the same paid list scrub as a file upload, priced per row with volume bands and a $5 minimum, and returns the price and a Stripe Checkout link. A person pays it by card; nothing is taken from API credit and nothing runs until they pay. get_list_status then reports whether it is paid, running or done, the counts, and a download link for the cleaned file that lasts one hour. verify_phone_number stays one number per call, for checking a few as you go. If the list is already a file, the file upload runs the same scrub.
Does a lookup check the Do Not Call registry?
No. verify_phone_number does not check Do Not Call registries, and every response carries dncEvaluated: false so an agent never has to infer coverage it has not got. The litigator flag comes from a court-sourced database of known TCPA plaintiffs. Screening reduces risk; it does not eliminate it, and none of this is legal advice.
Can I add it in claude.ai or ChatGPT?
Not with steps from us yet. Both connect to remote MCP servers from their own cloud and authenticate with OAuth or nothing at all; neither has a field for an API key. Since October 5, 2026, the server also accepts an OAuth sign-in to your NumberBroom account for connectors like these, but we have not yet completed a connection from either one end to end, and setup steps for them will be published on this page once we have. Today Claude Code, Claude Desktop, Cursor, VS Code and Windsurf hold the key on your machine and send it as a header, so they work, and ChatGPT users can reach the same lookups through a custom GPT Action against the REST API.

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.