Agents

Connect as an MCP server

The remote Streamable HTTP MCP server exposes the same contract as the REST API — search, profiles, emails, price estimates, account status, and durable jobs — so any MCP-capable agent can use Stormy as a tool.

Claude Code

Export the key in the same shell first. $STORMY_API_KEY is expanded by your shell at the moment you connect. Paste the command without it and the header is stored as the literal eleven characters, so every call answers 401 Invalid or revoked Stormy API key with a valid key in your clipboard.

terminal
export STORMY_API_KEY=stm_live_...

claude mcp add --transport http stormy https://stormy.ai/mcp \
  --header "Authorization: Bearer $STORMY_API_KEY"

Claude Desktop, Cursor, Windsurf, VS Code — any mcpServers client

Not everyone is in a terminal. Any client that reads an mcpServers block takes the config below. In Claude Desktop it goes in Settings → Developer → Edit Config; in Cursor it is ~/.cursor/mcp.json; in a repo it is .mcp.json. Restart the app afterwards.

claude_desktop_config.json
{
  "mcpServers": {
    "stormy": {
      "url": "https://stormy.ai/mcp",
      "headers": { "Authorization": "Bearer stm_live_YOUR_KEY_HERE" }
    }
  }
}

Put the key in literally, exactly as shown. A JSON file is not a shell and will not expand $STORMY_API_KEY. Stormy does not yet support the one-click OAuth connector flow, so a header is the way in.

Codex

~/.codex/config.toml
[mcp_servers.stormy]
url = "https://stormy.ai/mcp"
bearer_token_env_var = "STORMY_API_KEY"

Claude Agent SDK

Python
from claude_agent_sdk import ClaudeAgentOptions, ClaudeSDKClient
import os

options = ClaudeAgentOptions(
    mcp_servers={
        "stormy": {
            "type": "http",
            "url": "https://stormy.ai/mcp",
            "headers": {"Authorization": f"Bearer {os.environ['STORMY_API_KEY']}"},
        }
    },
)

async with ClaudeSDKClient(options=options) as agent:
    await agent.query("Find 25 cached YouTube roofing creators.")
    async for message in agent.receive_response():
        print(message)

How long a call takes

search_people takes 45–90 seconds. It fetches and reads real profiles and judges each one against your request; there is no cached shortcut that skips the reading. Past 100 seconds it stops holding the connection and returns a job_id with poll_after_seconds instead.

lookup_profile 1–5s cached · find_emails up to 90s for a batch of 25 · a discover_creators job 30–60s. A search is not hung at 60 seconds — retrying it pays twice.

Tools

search_peopleplatform · query · limit · fresh · min_followers · max_followers

Find people from a plain-language query. Every result is judged against what you asked for and carries its score and reason. Use min/max_followers to ask for accounts your own size rather than the biggest in the niche.

lookup_profiletarget · platform · fresh · include_posts

Resolve a URL, handle, or channel ID into one normalized profile.

get_video_transcripturl · language

Everything said in a YouTube video, as text, captions or transcribed audio.

find_emailsplatform · targets[1..25]

Verified email enrichment. Bills only successful finds.

estimate_pricequantity · include_email

Quote the exact rate card before a large call.

account_status

Entitlements, remaining prepaid usage, and top-up URL.

describe_social_data

The machine-readable platform, field, pricing and workflow contract.

start_social_joboperation · arguments · idempotency_key

Queue durable work. Operations: search_people, lookup_profile, find_emails, discover_creators.

get_social_jobjob_id

Status, progress, results, and the exact time to poll again.

list_social_jobsstatus · limit

Recover prior work instead of submitting a duplicate.

cancel_social_jobjob_id

Stop a job before a worker starts it.

Durable jobs and discover_creators

start_social_job also accepts discover_creators, the deeper creator search. It writes and expands its own queries, judges every profile, and is the operation that takes an audience band, an engagement floor and an account-type filter — the shape of "find me people my size who would post about this". It counts with target_count, not limit, and an argument name it does not recognise is rejected rather than silently dropped.

start_social_job arguments
{
  "operation": "discover_creators",
  "arguments": {
    "platform": "instagram",
    "query": "creators who post about handmade jewellery",
    "target_count": 10,
    "max_followers": 50000,
    "exclude_business": true,
    "max_spend_usd": 2.00
  }
}

Agent execution policy

Any people search → a durable job, or a direct search_people call if you can wait 45–90 seconds; both run the same engine. A single known profile → lookup_profile. Estimate when the user asks about cost or wants 25+ paid results. Honor poll_after_seconds, split email targets into batches of 25, never infer contacts from search results, and never resubmit a nonterminal job. Read warnings on a short result before retrying — it says whether the niche was thin or whether part of the search did not answer.

Machine-readable contract: capabilities JSON · OpenAPI · llms.txt