Skip to content

Pitch MCP Server

Connect Claude Code (or any MCP client) to your Pitch account and work with the startup directory in natural language — search companies, update profiles, manage your investment pipeline, and more.

24 tools available. All calls run as the authenticated user — row-level security is enforced the same way it is in the web app.

What is MCP?

The Model Context Protocol is an open standard for connecting AI assistants to external systems. A Pitch MCP server runs locally on your machine, authenticates with your Pitch API key, and exposes a set of typed tools that Claude (or any MCP-compatible client) can call on your behalf.

Once installed, you can ask Claude Code things like “find fintech companies in Austin added in the past 6 months” and it will call the right Pitch tools, fetch the data from your account, and summarize the results — no copy-paste required.

Install

  1. Generate an API key at Settings → API Keys. Copy the pitch_… token immediately — it is shown exactly once.

  2. Register the server with Claude Code:

    claude mcp add pitch npx -y pitch-mcp-server \
      --env PITCH_API_KEY=pitch_YOUR_KEY_HERE

    No clone, no build, no Supabase secrets. The published pitch-mcp-server package trades your API key for a short-lived Supabase session via the hosted /api/mcp/bootstrap endpoint at startup.

  3. For Cursor, paste this into ~/.cursor/mcp.json:

    {
      "mcpServers": {
        "pitch": {
          "command": "npx",
          "args": ["-y", "pitch-mcp-server"],
          "env": {
            "PITCH_API_KEY": "pitch_YOUR_KEY_HERE"
          }
        }
      }
    }
  4. For the OpenAI Codex CLI, install with npm i -g @openai/codex then:

    codex mcp add pitch \
      --env PITCH_API_KEY=pitch_YOUR_KEY_HERE \
      -- npx -y pitch-mcp-server

    The trailing -- is required so Codex separates its own flags from the launch command. Verify with codex mcp list.

  5. Restart your client. Try a prompt from the examples below.

Try it

  • Find fintech companies in Austin added in the past 6 months that are raising a seed round.
  • Add Acme Robotics to my pipeline at the screening stage with a note about today's demo call.
  • Give me the investor persona review for acme-robotics from the latest batch.
  • Create a new company called "Lumen Logistics" as a draft, then tag it with Logistics and AI.
  • Show me the 5 most recently published companies in Houston.
  • Who owns the acme-robotics profile, and what other companies are they attached to?
  • Sign me up to Pitch as a founder. I'm jane@acme.com and my company is Acme Robotics.
  • Rotate my Pitch API key — the old one may have leaked.

Tool reference

Every tool below is generated from the authoritative manifest in mcp-server/src/manifest.ts — the same file the server uses when it registers itself, so this page cannot drift from the running server.

Companies

pitch_get_companyCompanies

Fetch one company by slug with tags, cities, team relationships, latest AI reviews, and staleness flags. Use this when you already know the slug. To find a company by name or keyword first, use pitch_search_companies.

Parameters

slugstringrequired

URL slug of the company, e.g. "acme-robotics". Lowercase, hyphenated. Call pitch_search_companies first if you only have the name.

Returns

Full company row plus `latest_reviews` (up to 5) and `staleness_records` (up to 10).

Example

Show me the Acme Robotics profile.

pitch_get_company({ slug: "acme-robotics" })

Common errors

  • Company not found: <slug> — verify the slug via pitch_search_companies.
pitch_list_companiesCompanies

List companies with structured filters (status, city, tag, funding stage, raising) and pagination. Use this when you have exact filter values. For natural-language queries like "fintech in austin added last month", use pitch_search_companies instead.

Parameters

statusenum (draft | published | unpublished | archived)optional

Publication status filter.

citystringoptional

Exact city name (case-insensitive). Use pitch_list_cities to see the curated list.

tagstringoptional

Exact tag name (case-insensitive). Use pitch_list_tags to see available tags.

fundingStageenum (pre-seed | seed | series-a | series-b | series-c | growth)optional

Funding stage key.

raisingFundsbooleanoptional

If true, only return companies actively raising.

sortenum (name | recent | trending | completion)optionaldefault: name

Sort order. "recent" = newest published first; "trending" = most punches in the last 7 days; "completion" = highest profile completeness.

limitintegeroptionaldefault: 20

Max results per page, 1-100.

offsetintegeroptionaldefault: 0

Zero-based pagination offset.

Returns

`{ data: Company[], meta: { total, hasMore } }`

Example

List the 10 most recently published fintech companies in Austin.

pitch_list_companies({ city: "Austin", tag: "Fintech", status: "published", sort: "recent", limit: 10 })
pitch_search_companiesCompanies

Natural-language search over published companies. Parses cities, tags, funding stages, keywords, and date ranges like "past 6 months" or "added in 2025" from a free-form query. Prefer this over pitch_list_companies when the user's request is conversational.

Parameters

querystringrequired

Free-form search query, 1-500 chars. Understands cities, tags, stages, "raising now", and date phrases.

limitintegeroptionaldefault: 20

Max results, 1-100.

Returns

`{ data: Company[] with matchReason, meta: { total, hasMore }, parsedFilters, query }`

Example

Find fintech companies added in the past 6 months that are raising a seed round.

pitch_search_companies({ query: "fintech raising seed added in the past 6 months" })
pitch_create_companyCompanies

Create a new company with a single atomic insert. Automatically checks for duplicates by name, website, email, and LinkedIn — returns a `duplicatesFound` list (without inserting) if any matches exceed the confidence threshold. Re-call with `ignoreDuplicates` set to the slugs you want to skip to proceed. Tags, cities, and team members are added separately via pitch_manage_tags, pitch_manage_cities, and pitch_add_relationship.

Parameters

namestringrequired

Company name, 1-200 characters. The slug is generated from this.

taglinestringoptional

Short one-liner shown on directory cards, max 200 chars.

descriptionstringoptional

Full company description.

problemstringoptional

Problem statement.

solutionstringoptional

Solution statement.

tractionstringoptional

Traction / metrics blurb.

websitestringoptional

Website URL. Protocol optional — "https://" is auto-prepended if missing.

contactEmailstringoptional

Primary contact email.

twitterstringoptional

Twitter/X URL or handle.

linkedinstringoptional

LinkedIn company URL.

fundingStageenum (pre-seed | seed | series-a | series-b | series-c | growth)optional

Funding stage key. One of "pre-seed", "seed", "series-a", "series-b", "series-c", "growth".

raisingFundsbooleanoptional

True if actively raising.

fundraiseTargetnumberoptional

Target raise amount, USD.

foundedYearnumberoptional

Year founded, e.g. 2024.

headcountnumberoptional

Current team size.

statusenum (draft | published)optionaldefault: draft

Initial status. "draft" = hidden until published; "published" = visible in the directory immediately and triggers AI reviews.

ignoreDuplicatesstring[]optional

Slugs of potential duplicates to ignore. Pass the slugs returned by a prior call that flagged duplicates to proceed with creation.

Returns

`{ data: { id, slug } }` on success, or `{ data: null, duplicatesFound, message }` if duplicates exist and `ignoreDuplicates` was not set.

Example

Create Acme Robotics as a draft with a Seed stage and ignore the existing "acme" match.

pitch_create_company({ name: "Acme Robotics", status: "draft", fundingStage: "seed", ignoreDuplicates: ["acme"] })

Common errors

  • Potential duplicates found — re-call with ignoreDuplicates set to the slugs you want to skip.
pitch_update_companyCompanies

Update one or more fields on a company. Writes a row to `company_versions` with the before/after diff so every change is auditable. Only a fixed allowlist of fields is accepted — unknown keys in `fields` are silently dropped.

Parameters

slugstringrequired

Company slug to update.

fieldsobjectrequired

Object with one or more of: name, tagline, description, problem, solution, traction, website, contactEmail, fundingStage, raisingFunds, fundraiseTarget, foundedYear, headcount.

Returns

`{ data: updated company, fieldsUpdated: string[] }`

Example

Update the Acme tagline and mark them as raising.

pitch_update_company({ slug: "acme-robotics", fields: { tagline: "Warehouse robots.", raisingFunds: true } })

Common errors

  • Company not found: <slug>
  • No valid fields to update — check that `fields` keys match the allowlist.
pitch_set_company_statusCompanies

Change a company's publication status (draft / published / unpublished / archived). Archiving is destructive and requires `confirm: true`; calling without it returns a preview showing how many followers and pipeline entries would be affected.

Parameters

slugstringrequired

Company slug.

statusenum (draft | published | unpublished | archived)required

Target status.

confirmbooleanoptional

Required when status is "archived". Without it, the tool returns a preview of impacted followers and pipeline entries and does not write.

Returns

On success: `{ data: { slug, status, publishedAt } }`. Archive without confirm: `{ confirmRequired: true, impact: { followerCount, pipelineEntryCount }, message }`.

Example

Publish Acme Robotics.

pitch_set_company_status({ slug: "acme-robotics", status: "published" })
pitch_check_duplicatesCompanies

Check whether a company already exists by name, website, email domain, or LinkedIn URL before creating it. Returns up to 5 matches sorted by confidence (50-95). Useful as a preview step in agent workflows that handle user input.

Parameters

namestringrequired

Company name to check.

websitestringoptional

Website URL (optional).

contactEmailstringoptional

Contact email (optional).

linkedinstringoptional

LinkedIn URL (optional).

Returns

`{ data: Match[], count }` where each Match has `{ id, slug, name, confidence, reasons[] }`.

Example

Is there already a company called "Acme" on Pitch?

pitch_check_duplicates({ name: "Acme", website: "https://acme.example" })

People

pitch_get_personPeople

Fetch one person by slug with all their company relationships. Email is omitted by default for privacy — pass `includeEmail: true` if the caller has a legitimate reason (and has accepted the consequences).

Parameters

slugstringrequired

Person slug, e.g. "jane-doe".

includeEmailbooleanoptional

If true, include the `email` field. Defaults to false.

Returns

Person row with embedded `relationships` and company info.

Example

Who is Jane Doe and what companies is she attached to?

pitch_get_person({ slug: "jane-doe" })

Common errors

  • Person not found: <slug> — verify the slug before retrying.
pitch_create_personPeople

Create a new Person record. A Person is a profile with a name and optional bio; it may or may not be linked to a user account. Slug is auto-generated from the name with a numeric suffix for collisions.

Parameters

firstNamestringrequired

First name (min 1 char).

lastNamestringoptional

Last name.

emailstringoptional

Email address (not validated here).

biostringoptional

Short bio.

linkedinstringoptional

LinkedIn URL.

twitterstringoptional

Twitter/X URL.

Returns

`{ data: { id, slug } }`

Example

Add Jane Doe to the directory.

pitch_create_person({ firstName: "Jane", lastName: "Doe", bio: "Robotics lead." })
pitch_add_relationshipPeople

Link a person to a company with a role (founder, employee, investor, mentor, advisor, board, other). Use this after pitch_create_person and pitch_create_company to wire up the team.

Parameters

personSlugstringrequired

Person slug.

companySlugstringrequired

Company slug.

roleenum (founder | employee | investor | mentor | advisor | board | other)required

Relationship role.

titlestringoptional

Optional title, e.g. "CEO" or "Head of Engineering".

isOwnerbooleanoptional

If true, this person owns the company profile (can edit).

isMaintainerbooleanoptional

If true, this person can edit the profile without being the owner.

Returns

`{ data: { id } }`

Example

Make Jane Doe the founder and owner of Acme Robotics.

pitch_add_relationship({ personSlug: "jane-doe", companySlug: "acme-robotics", role: "founder", title: "CEO", isOwner: true })

Common errors

  • Person not found: <slug>
  • Company not found: <slug>
  • Invalid role. Must be one of: founder, employee, investor, mentor, advisor, board, other.

Tags

pitch_list_tagsTags

List all tags in the system. Use this to discover valid tag names before calling pitch_manage_tags or pitch_list_companies with a tag filter.

Parameters

sortenum (name | count)optionaldefault: count

Sort by alphabetical name or by company count (default).

limitintegeroptionaldefault: 100

Max tags to return, 1-500.

Returns

`{ data: Tag[] }` with `{ id, name, slug, company_count }`.

Example

What are the most common tags on Pitch?

pitch_list_tags({ sort: "count", limit: 20 })
pitch_manage_tagsTags

Add or remove tags on a company. Tags in `add` that don't exist yet are created automatically (unlike cities). Pass an empty `remove` and a non-empty `add`, or vice versa, or both.

Parameters

companySlugstringrequired

Company slug to tag.

addstring[]optional

Tag names to add. Unknown tags are auto-created.

removestring[]optional

Tag names to remove. Unknown tags are silently skipped.

Returns

`{ data: { companySlug, added, removed } }`

Example

Tag Acme with Fintech and AI, and remove the Hardware tag.

pitch_manage_tags({ companySlug: "acme-robotics", add: ["Fintech", "AI"], remove: ["Hardware"] })

Common errors

  • Company not found: <slug>
  • At least one of add or remove is required.

Cities

pitch_list_citiesCities

List curated cities available for tagging companies. Cities are a controlled vocabulary — unlike tags, pitch_manage_cities will NOT auto-create new ones. Call this to discover valid city slugs and names before calling pitch_manage_cities.

Parameters

statestringoptional

Filter by state code or name, e.g. "TX".

limitintegeroptionaldefault: 100

Max cities to return, 1-500.

Returns

`{ data: City[] }` with `{ id, name, slug, state, country }`.

Example

What Texas cities does Pitch recognize?

pitch_list_cities({ state: "TX" })
pitch_manage_citiesCities

Add or remove cities on a company. Cities must already exist — this tool does NOT auto-create them (by design; the city list is curated). Accepts either the slug or the name as the identifier. Unknown cities are returned in `notFound` rather than silently dropped.

Parameters

companySlugstringrequired

Company slug.

addstring[]optional

City slugs or names to add. Must already exist — see pitch_list_cities.

removestring[]optional

City slugs or names to remove.

Returns

`{ data: { companySlug, added, removed, notFound } }`

Example

Mark Acme as based in Austin and Houston.

pitch_manage_cities({ companySlug: "acme-robotics", add: ["austin", "houston"] })

Common errors

  • Company not found: <slug>
  • No matching cities found: <list> — use pitch_list_cities to see available cities.

Pipeline

pitch_list_pipelinePipeline

List entries in your personal investment pipeline (CRM). Results are scoped to the authenticated user — you only ever see your own pipeline.

Parameters

statusenum (watching | met_founder | screening | passed | invested | portfolio)optional

Filter by pipeline stage.

sortenum (updated | created | name)optionaldefault: updated

Sort order.

Returns

`{ data: PipelineEntry[] }` with embedded company info.

Example

Show me everything in my "screening" pipeline stage.

pitch_list_pipeline({ status: "screening" })
pitch_add_to_pipelinePipeline

Add a company to your personal investment pipeline. Defaults to the "watching" stage. Returns an error if the company is already in your pipeline — use pitch_update_pipeline to change the stage of an existing entry.

Parameters

companySlugstringrequired

Company slug to add.

statusenum (watching | met_founder | screening | passed | invested | portfolio)optionaldefault: watching

Initial pipeline stage.

notesstringoptional

Free-form notes.

sourcestringoptional

Where you found this company, e.g. "Warm intro from X".

Returns

`{ data: { id, companySlug, status } }`

Example

Add Acme Robotics to my pipeline at the "screening" stage.

pitch_add_to_pipeline({ companySlug: "acme-robotics", status: "screening", source: "Warm intro from Jane" })

Common errors

  • Company not found: <slug>
  • Company already in pipeline — use pitch_update_pipeline to change the stage.
pitch_update_pipelinePipeline

Update an existing pipeline entry's status and/or notes. At least one of `status` or `notes` must be provided. The entry must already exist — use pitch_add_to_pipeline to create new entries.

Parameters

companySlugstringrequired

Company slug.

statusenum (watching | met_founder | screening | passed | invested | portfolio)optional

New pipeline stage.

notesstringoptional

Replacement notes (not appended).

Returns

`{ data: PipelineEntry }`

Example

Move Acme from screening to met_founder and add a note about today's call.

pitch_update_pipeline({ companySlug: "acme-robotics", status: "met_founder", notes: "Met with Jane — great demo" })

Common errors

  • No pipeline entry found for <slug> — use pitch_add_to_pipeline first.
  • At least one of status or notes is required.

Follows

pitch_toggle_followFollows

Follow or unfollow a company or person. Following drives notifications (funding updates, team changes, government awards, etc.). Already-following / not-following states are treated as success — no error.

Parameters

typeenum (company | person)required

Entity type.

slugstringrequired

Entity slug.

actionenum (follow | unfollow)required

Follow or unfollow.

Returns

`{ data: { action, type, slug } }`

Example

Follow Acme Robotics.

pitch_toggle_follow({ type: "company", slug: "acme-robotics", action: "follow" })

Common errors

  • <type> not found: <slug>

Reviews

pitch_get_reviewsReviews

Get AI review results for a company. Pitch runs 5 parallel Claude persona reviews (investor, marketer, technical, customer, coach) when a profile is published or updated. Pass `latest: true` to get only the most recent batch; pass a specific `persona` to narrow down.

Parameters

companySlugstringrequired

Company slug.

personaenum (investor | marketer | technical | customer | coach)optional

Filter to a specific persona.

latestbooleanoptional

If true, return only the most recent review batch.

Returns

`{ data: Review[] }` with `{ persona, score, rationale, strengths, suggestions, red_flags, created_at }`.

Example

What did the investor persona say about Acme's latest review?

pitch_get_reviews({ companySlug: "acme-robotics", persona: "investor", latest: true })

Common errors

  • Company not found: <slug>
  • Invalid persona — must be one of: investor, marketer, technical, customer, coach.

Admin

pitch_get_statsAdmin

Get platform-wide dashboard stats: company/people counts, recent signups, notification volume, breakdowns by city/stage/pipeline status, and forwarded-email queue depth. Takes no parameters.

No parameters.

Returns

`{ data: { stats: { totalCompanies, publishedCompanies, ... }, companiesByCity, companiesByStage, pipelineByStatus } }`

Example

Give me a snapshot of the Pitch platform right now.

pitch_get_stats({})

Signup

pitch_signup_startSignup

Start a new signup or signin flow by sending a 6-digit OTP code to the user's email. Does NOT require an existing account or API key — this is the entry point for brand-new users signing up from Claude Code. Returns a request_id to pass to pitch_signup_verify. The response shape is intentionally identical for new and existing accounts (no account enumeration).

Parameters

personaenum (founder | investor | both)required

User persona. "founder" = building a company; "investor" = writing checks; "both" = combined founder+investor path. Determines the onboarding path after verification.

emailstringrequired

Email address to send the 6-digit OTP code to.

first_namestringoptional

First name.

last_namestringoptional

Last name.

company_namestringoptional

Company name (founder persona). Used for duplicate detection and draft creation after verify.

company_websitestringoptional

Company website (founder persona). Used for duplicate detection.

investor_firm_namestringoptional

Firm name (investor persona). Stored on the investor application.

investor_titlestringoptional

Title at the firm (investor persona), e.g. "Partner".

device_labelstringoptional

Label for the device/machine, e.g. "claude-code/joshdesk". Used for multi-device key management.

Returns

`{ request_id, next: "verify_email", mode: "signup" | "signin", expires_in_minutes, hint }`

Example

Sign me up to Pitch as a founder. I'm jane@acme.com.

pitch_signup_start({ persona: "founder", email: "jane@acme.com", first_name: "Jane", last_name: "Doe", company_name: "Acme Robotics", device_label: "claude-code/janebook" })

Common errors

  • rate_limited — too many signup attempts. Retry after the indicated period.
  • signup_disabled — MCP signup is temporarily disabled.
pitch_signup_verifySignup

Verify a 6-digit OTP code from a prior pitch_signup_start call. On success, creates the user account (if new), person record, and API key atomically. The API key is returned ONCE in plaintext — save it to ~/.claude.json immediately. For existing accounts, mints a new API key (signin path).

Parameters

request_idstringrequired

The request_id returned by pitch_signup_start.

codestringrequired

The 6-digit OTP code from the verification email. Dashes and spaces are stripped automatically.

idempotency_keystringoptional

UUID idempotency key to prevent duplicate account creation on retries. Auto-generated if not provided.

Returns

`{ status: "verified", mode, api_key: { value, sensitive: true, rotation_due_at }, person: { id, slug }, user_id, next, company? }`

Example

The code is 481-302.

pitch_signup_verify({ request_id: "ps_7f3a...", code: "481302" })

Common errors

  • invalid_code — wrong OTP. Up to 5 attempts before lockout.
  • expired — the request_id has expired. Re-call pitch_signup_start.
  • locked — too many failed attempts. Re-call pitch_signup_start for a fresh code.
pitch_claim_companySignup

Trigger the email-verification claim flow for an existing unowned company. Sends a verification email to the company's contact email with a 48-hour cryptographic link. The user clicks the link from any device to complete the claim. Requires authentication (existing API key).

Parameters

company_slugstringrequired

Slug of the company to claim. Must already exist and must not be owned by another user.

Returns

`{ status: "verification_sent", company_slug, expires_in_minutes, contact_email_hint }` or `{ status: "already_owner" }`

Example

Claim the acme-robotics company profile.

pitch_claim_company({ company_slug: "acme-robotics" })

Common errors

  • Company not found: <slug>
  • already_owned — this company is owned by someone else.
  • no_contact_email — the company has no contact email on file for verification.
  • rate_limited — too many claim attempts.
pitch_rotate_keySignup

Mint a fresh API key and revoke the current one in a single call. The new key is returned ONCE in plaintext — save it to ~/.claude.json immediately. The old key is revoked instantly. Use this when a key is suspected compromised or when rotation_due_at is approaching. Requires authentication.

No parameters.

Returns

`{ new_key: { value, sensitive: true, rotation_due_at }, old_key_revoked_at }`

Example

Rotate my Pitch API key.

pitch_rotate_key({})

Common errors

  • No active API key found — should not happen if authenticated.

Troubleshooting

Tool not appearing after install

Restart Claude Code after running claude mcp add. Then verify the server is registered with claude mcp list. If the server is listed but the tools don't show up in the chat picker, check the Claude Code debug console for startup errors — the Pitch launcher logs auth failures to stderr.

Authentication failures

The server only needs PITCH_API_KEY in its environment. On startup, it POSTs the key to /api/mcp/bootstrap, which validates it server-side and returns a short-lived Supabase session — no Supabase URL, anon key, or service-role key ever touches your machine.

  • FATAL: Missing PITCH_API_KEY — your MCP client launched the server without the env var. Re-run claude mcp add pitch npx -y pitch-mcp-server --env PITCH_API_KEY=pitch_….
  • Invalid or revoked PITCH_API_KEY — regenerate the key at Settings → API Keys and update your client’s launch config.
  • Rate limited while validating PITCH_API_KEY— the bootstrap endpoint allows 10 requests/minute per IP. You only hit this if you’re restarting the server in a tight loop; wait 60 seconds and retry.

Rate limits

Tool calls run under your authenticated Supabase session and inherit the same rate limits as the web app. The hosted bootstrap endpoint itself is throttled at 10 requests per minute per IP — that only kicks in at server startup, not during normal tool use.

Common error messages

  • Company not found: "..." — use pitch_search_companies with a keyword to discover the slug, then retry.
  • Potential duplicates found — a pitch_create_company call hit the dedupe check. Review duplicatesFound[] in the response, then re-call with ignoreDuplicates set to the slugs you want to skip (or an empty array to ignore all).
  • Company already in pipeline — use pitch_update_pipeline to change the stage of an existing entry instead of pitch_add_to_pipeline.
  • No matching cities found — cities are a curated list. Call pitch_list_cities to see available cities, then retry with a valid slug or name.
Questions or issues? Open an issue on the repo.