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
Generate an API key at Settings → API Keys. Copy the
pitch_… token immediately — it is shown exactly once.Register the server with Claude Code:
claude mcp add pitch npx -y pitch-mcp-server \ --env PITCH_API_KEY=pitch_YOUR_KEY_HERENo clone, no build, no Supabase secrets. The published
pitch-mcp-serverpackage trades your API key for a short-lived Supabase session via the hosted/api/mcp/bootstrapendpoint at startup.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" } } } }For the OpenAI Codex CLI, install with
npm i -g @openai/codexthen:codex mcp add pitch \ --env PITCH_API_KEY=pitch_YOUR_KEY_HERE \ -- npx -y pitch-mcp-serverThe trailing
--is required so Codex separates its own flags from the launch command. Verify withcodex mcp list.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_companyCompaniesFetch 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
slugstringrequiredURL 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_companiesCompaniesList 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)optionalPublication status filter.
citystringoptionalExact city name (case-insensitive). Use pitch_list_cities to see the curated list.
tagstringoptionalExact tag name (case-insensitive). Use pitch_list_tags to see available tags.
fundingStageenum (pre-seed | seed | series-a | series-b | series-c | growth)optionalFunding stage key.
raisingFundsbooleanoptionalIf true, only return companies actively raising.
sortenum (name | recent | trending | completion)optionaldefault: nameSort order. "recent" = newest published first; "trending" = most punches in the last 7 days; "completion" = highest profile completeness.
limitintegeroptionaldefault: 20Max results per page, 1-100.
offsetintegeroptionaldefault: 0Zero-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_companiesCompaniesNatural-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
querystringrequiredFree-form search query, 1-500 chars. Understands cities, tags, stages, "raising now", and date phrases.
limitintegeroptionaldefault: 20Max 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_companyCompaniesCreate 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
namestringrequiredCompany name, 1-200 characters. The slug is generated from this.
taglinestringoptionalShort one-liner shown on directory cards, max 200 chars.
descriptionstringoptionalFull company description.
problemstringoptionalProblem statement.
solutionstringoptionalSolution statement.
tractionstringoptionalTraction / metrics blurb.
websitestringoptionalWebsite URL. Protocol optional — "https://" is auto-prepended if missing.
contactEmailstringoptionalPrimary contact email.
twitterstringoptionalTwitter/X URL or handle.
linkedinstringoptionalLinkedIn company URL.
fundingStageenum (pre-seed | seed | series-a | series-b | series-c | growth)optionalFunding stage key. One of "pre-seed", "seed", "series-a", "series-b", "series-c", "growth".
raisingFundsbooleanoptionalTrue if actively raising.
fundraiseTargetnumberoptionalTarget raise amount, USD.
foundedYearnumberoptionalYear founded, e.g. 2024.
headcountnumberoptionalCurrent team size.
statusenum (draft | published)optionaldefault: draftInitial status. "draft" = hidden until published; "published" = visible in the directory immediately and triggers AI reviews.
ignoreDuplicatesstring[]optionalSlugs 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_companyCompaniesUpdate 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
slugstringrequiredCompany slug to update.
fieldsobjectrequiredObject 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_statusCompaniesChange 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
slugstringrequiredCompany slug.
statusenum (draft | published | unpublished | archived)requiredTarget status.
confirmbooleanoptionalRequired 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_duplicatesCompaniesCheck 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
namestringrequiredCompany name to check.
websitestringoptionalWebsite URL (optional).
contactEmailstringoptionalContact email (optional).
linkedinstringoptionalLinkedIn 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_personPeopleFetch 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
slugstringrequiredPerson slug, e.g. "jane-doe".
includeEmailbooleanoptionalIf 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_personPeopleCreate 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
firstNamestringrequiredFirst name (min 1 char).
lastNamestringoptionalLast name.
emailstringoptionalEmail address (not validated here).
biostringoptionalShort bio.
linkedinstringoptionalLinkedIn URL.
twitterstringoptionalTwitter/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_relationshipPeopleLink 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
personSlugstringrequiredPerson slug.
companySlugstringrequiredCompany slug.
roleenum (founder | employee | investor | mentor | advisor | board | other)requiredRelationship role.
titlestringoptionalOptional title, e.g. "CEO" or "Head of Engineering".
isOwnerbooleanoptionalIf true, this person owns the company profile (can edit).
isMaintainerbooleanoptionalIf 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.
Cities
pitch_list_citiesCitiesList 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
statestringoptionalFilter by state code or name, e.g. "TX".
limitintegeroptionaldefault: 100Max 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_citiesCitiesAdd 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
companySlugstringrequiredCompany slug.
addstring[]optionalCity slugs or names to add. Must already exist — see pitch_list_cities.
removestring[]optionalCity 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_pipelinePipelineList 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)optionalFilter by pipeline stage.
sortenum (updated | created | name)optionaldefault: updatedSort 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_pipelinePipelineAdd 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
companySlugstringrequiredCompany slug to add.
statusenum (watching | met_founder | screening | passed | invested | portfolio)optionaldefault: watchingInitial pipeline stage.
notesstringoptionalFree-form notes.
sourcestringoptionalWhere 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_pipelinePipelineUpdate 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
companySlugstringrequiredCompany slug.
statusenum (watching | met_founder | screening | passed | invested | portfolio)optionalNew pipeline stage.
notesstringoptionalReplacement 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_followFollowsFollow 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)requiredEntity type.
slugstringrequiredEntity slug.
actionenum (follow | unfollow)requiredFollow 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_reviewsReviewsGet 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
companySlugstringrequiredCompany slug.
personaenum (investor | marketer | technical | customer | coach)optionalFilter to a specific persona.
latestbooleanoptionalIf 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_statsAdminGet 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_startSignupStart 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)requiredUser persona. "founder" = building a company; "investor" = writing checks; "both" = combined founder+investor path. Determines the onboarding path after verification.
emailstringrequiredEmail address to send the 6-digit OTP code to.
first_namestringoptionalFirst name.
last_namestringoptionalLast name.
company_namestringoptionalCompany name (founder persona). Used for duplicate detection and draft creation after verify.
company_websitestringoptionalCompany website (founder persona). Used for duplicate detection.
investor_firm_namestringoptionalFirm name (investor persona). Stored on the investor application.
investor_titlestringoptionalTitle at the firm (investor persona), e.g. "Partner".
device_labelstringoptionalLabel 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_verifySignupVerify 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_idstringrequiredThe request_id returned by pitch_signup_start.
codestringrequiredThe 6-digit OTP code from the verification email. Dashes and spaces are stripped automatically.
idempotency_keystringoptionalUUID 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_companySignupTrigger 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_slugstringrequiredSlug 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_keySignupMint 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-runclaude 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: "..."— usepitch_search_companieswith a keyword to discover the slug, then retry.Potential duplicates found— apitch_create_companycall hit the dedupe check. ReviewduplicatesFound[]in the response, then re-call withignoreDuplicatesset to the slugs you want to skip (or an empty array to ignore all).Company already in pipeline— usepitch_update_pipelineto change the stage of an existing entry instead ofpitch_add_to_pipeline.No matching cities found— cities are a curated list. Callpitch_list_citiesto see available cities, then retry with a valid slug or name.