PrivCo MCP Search
Setup / access (remote connector or local npm server): see INSTALL.md in this skill folder.
Full per-tool field reference: read the privco://docs/usage-guide resource from the MCP server (and privco://docs/overview) — the server ships the canonical, always-current reference in its own contract.
Guided workflows (present a dashboard, research a company, discover by criteria, top-N-by-valuation, recent funding): invoke the MCP server's built-in prompts — company_dashboard, research_company, discover_companies_by_criteria, top_companies_by_valuation, recent_funding_query. The distilled workflow sketches below mirror them for always-on steering.
The bullets below are the high-value-per-line distillation — load them once,
do not re-derive by trial and error.
Tool inventory (17 tools)
All tools are exposed under the mcp__<server-name>__* namespace once the
MCP server is registered — mcp__privco-data-mcp__* in the standard setup;
a remote connector carries whatever name it was registered under (see
"Access & auth" below and INSTALL.md).
Permission-gated tools: funding_search and deal_search are live,
but require the API key to carry the matching funding_search /
deal_search permission (or everything) — a key without it returns 403.
If you lack the permission, approximate via company_search +
per-candidate vc_deals / ma_deals.
Stage 1 — entity resolution (name/details → profile_id)
These two tools look similar but differ in strictness, inputs accepted,
and whether they tell you how confident the match is. Pick deliberately.
match — strict, exact-ish matching on company name and/or website
only. Returns up to 20 ranked candidates as a flat list with no confidence
score. Designed for: "the user gave me a recognizable company name, give me
the canonical row(s)." Because the criteria are intentionally strict, it
can silently miss results when the input is incomplete or differs even
slightly from the source data (e.g., "Stripe" vs "Stripe, Inc.",
"stripe.io" vs "stripe.com"). First row is usually correct for unique
brands; common-word names need disambiguation by profile_type,
profile_classification, industry. Pass includeCityState: true to
surface HQ city/state in the result rows for co-located collisions.
Inputs: name, website, includeCityState. GET endpoint, response shape:
{data: [{name, profile_id, profile_type, profile_classification, permalink, location, industry, website[]}], meta}.
identification — flexible, fuzzy / tolerant matching that accepts
many partial inputs (DBA, URL, address, country/state/city/zip, phone,
industry, Facebook/LinkedIn/Twitter URLs, alternate names, reference IDs)
and returns ranked candidates with a confidence score. Designed for:
"I have messy, partial, or fuzzy fields from an external source — give
me the best mapping with a probability so I can decide whether to trust
it." POST endpoint to /identification/company. Each result row carries:
mapping_strength — integer 1–5 (higher = stronger). mapping_strength_need_review: "Yes" flags rows that should be human-reviewed before being trusted.
matching_probability — float 0–1 (raw confidence).
match_summary / mismatch_summary — human-readable strings explaining
why the engine scored it that way (e.g. "Exact Name Match" /
"Reference ID Mismatch|Phone Mismatch").
- Optional
threshold query param (default 0.75) filters out
low-confidence candidates server-side.
Inputs (all optional except name): name, dba, url, country,
state, city, address, zip, phone, industry, social_facebook,
social_linkedin, social_twitter, linkedin_url, classification,
clean_name, fn (formal/legal name), aka, reference_id,
company_only.
Decision rule — when in doubt, prefer identification. It is the full
entity-resolution engine: fuzzy and tolerant, accepts many combinable
signals beyond name/website, and returns a confidence score you can act
on. Use it whenever the input is fuzzy, partial, or from an external
source (CRM dump, prospect list, partner feed), when you hold extra
signals (address, phone, LinkedIn URL, reference ID), or when downstream
automation must gate on confidence. Reserve match for the quick first
try on a clean, recognizable name/URL — it matches on name/website ONLY,
strictly, and reports no confidence. Always fall through to
identification when match returns 0 or visibly-wrong rows — a
match miss does not mean the entity is absent from PrivCo.
Stage 2 — discovery (criteria → candidate list)
company_search — multi-filter (location, industry, keyword, revenue,
valuation, funding, employees, growth, year founded, PE/VC inclusion). Up to
50 summary rows per page.
people_search — equivalent for contacts.
funding_search — Round-centric search (fundingTypes, total, timestamp,
industry, location, investor types, keyword). Use when the question is
about rounds rather than about companies. Same gotchas as
company_search: keyword.condition is required ("should" / "must").
Requires the funding_search (or everything) API permission.
deal_search — M&A-deal-centric search (target/buyer/seller industries,
deal value, query, isPeDeal, target PE/VC-backed flags). Filter
precedence quirk: setting isPeDeal (any boolean) SUPPRESSES
inclusionExclusion.targetsIsPeBacked/targetsIsVcBacked/buyersAllOfType
— don't mix. Default sort: relevance if query set, else timestamp desc.
Requires the deal_search (or everything) API permission.
industry_keyword — autocomplete for keyword IDs (use before keyword
filter when unsure).
industry_pics — list of PICS top-level industry codes.
Stage 3 — enrichment (profile_id → full record)
profile — full company profile (revenue, employees, funding, founders,
contacts, investors, competitors, subsidiaries).
general_profile — full profile for non-company entities (Investor,
Person, etc.). Use when profile_type != "Company" from match.
people — execs & board on a company.
financials / revenue_financials — multi-year statements / revenue-only
time series.
vc_deals — round-by-round funding (per-round valuation, amount, date,
investors). Use this for latestValuation since profile doesn't carry it.
ma_deals — M&A history.
Stage 4 — contact reveal (hash → details)
company_contact — resolve a contact hash to full businessEmail / phone /
LinkedIn.
investor_contact — same for investor-associated contacts.
- Both take the opaque hash from
people_search row's hash field.
NOT from profile.contacts[] — that block has no hash, just
{title, name, email} summary.
associatedType on the people_search row tells you which reveal endpoint
to use: Company → company_contact, Investor → investor_contact.
- To find Investor-associated rows, you must pass
{"filters": {"investorOnly": true}} to people_search — default is
Company-only and title filters like ["Partner"] are not effective.
- A successful call consumes a contact-reveal credit on the account —
don't call speculatively.
Filter semantics — the gotchas
1. location.state requires the full state name
// WRONG — silently returns 0 hits
{"location": {"selection": [{"type": "state", "value": "NY"}]}}
// RIGHT
{"location": {"selection": [{"type": "state", "value": "New York"}]}}
The response payload returns "hq_state": "NY", so the response shape does not
telegraph the required input shape. Verified empirically: "NY" → 0 hits,
"New York" → 6,444 hits on the same query.
2. industry vs keyword are distinct filters
| Filter |
Vocabulary |
Example values |
When |
industry.rawIndustries (or .selection) |
PICS top-level |
"Software & Internet Services", "Biotechnology" |
Broad industry buckets |
keyword.rawKeywords (or .selection) |
Tagged terms from industry_keyword autocomplete |
"Fintech", "Payments", "Generative AI" |
Specific business-model / tech tags |
Passing a broad word like "Software" to keyword.rawKeywords returns 0
hits — it isn't a registered keyword. Call industry_keyword(query: "...")
first when unsure.
3. inclusionExclusion is a requirement, not an addition
{"inclusionExclusion": {"pe": "include"}} // REQUIRE PE-backed
{"inclusionExclusion": {"pe": "exclude"}} // EXCLUDE PE-backed
"include" means must be PE-backed, not also include. Combining with other
restrictive filters quickly empties the result set — drop one filter at a time
when debugging zero-result queries.
4. sorting.field enum is small
Valid: name | state | employee | industry | yearFounded | totalFunding | revenueGrowthRate1 | revenueGrowthRate3 | hasContact.
Not sortable: latestValuation, latestRevenue, latestEbitda. For "top
N by valuation" workflows, see Workflow C below.
5. revenue.includeMissing quietly changes result composition
includeMissing: true includes companies with null/empty revenue — common for
early-stage, pre-revenue, and stealth companies, which often have
latestRevenue: null. When testing "revenue > X" assertions, leave it
false (default).
6. keyword.condition is required whenever keyword filter is used
// WRONG — 400 Bad Request
{"keyword": {"rawKeywords": ["Fintech"]}}
// RIGHT
{"keyword": {"rawKeywords": ["Fintech"], "condition": "should"}}
condition is "should" (match any keyword) or "must" (match all). It's
match-engine terminology — don't confuse with inclusionExclusion's
include / exclude (different filter, different vocabulary).
7. profileType path param is lowercase
// WRONG — 400 with cryptic "profileType must be one of the following values: "
GET /profile/Company/44612
// RIGHT
GET /profile/company/44612
'company' | 'investor' | 'advisor'. Affects profile, general_profile,
people, vc_deals, ma_deals. The MCP handlers don't normalize casing.
8. deal_search.isPeDeal SUPPRESSES sibling filters
Setting filters.isPeDeal to any boolean silently disables
inclusionExclusion.targetsIsPeBacked, inclusionExclusion.targetsIsVcBacked,
and buyersAllOfType in the same request. The precedence is intentional —
pick one approach per query:
- Want only PE deals? →
{"isPeDeal": true} (alone).
- Want deals where the target is PE-backed? → omit
isPeDeal, use
{"inclusionExclusion": {"targetsIsPeBacked": "include"}}.
9. deal_search sort default depends on whether query is set
When sorting is omitted: response is ordered by relevance scoring if
filters.query is set, otherwise by timestamp desc. Explicit sorting
always wins. Pass {"sorting": {"field": "timestamp", "order": "desc"}}
when you want deterministic ordering regardless of query presence.
Summary rows: included vs omitted
company_search summary rows contain:
- ✅
id, name, state, industry, url (permalink), keywords[],
classification (Private/Public), revenueGrowthRate1,
revenueGrowthRate3, latestEbitda
- ❌
latestRevenue, latestValuation, totalFunding, yearFounded,
ownersFounders, headquarters.city, summary, funding-round detail, M&A
detail
If you need any omitted field, follow up with profile per company.
Round-level data → also vc_deals. M&A detail → also ma_deals. Budget the
round-trips when planning multi-candidate work — a 22-company dataset is
~22+ profile calls plus targeted vc_deals/ma_deals.
Practical workflows
A. Named-entity lookup
match(name="Airbnb") → profile_id 78
profile(78) → full record
vc_deals(profileType="company", profileId=78) → all rounds + valuations
ma_deals(profileType="company", profileId=78) → M&A history
3–4 calls per entity for full enrichment.
B. Filter-criteria discovery
1. industry_keyword(query="fintech") → keyword id (if using id-based filter)
2. company_search({
location: {selection: [{type: "state", value: "New York"}]},
industry: {rawIndustries: ["Software & Internet Services"]},
revenue: {min: 1000000},
inclusionExclusion: {pe: "include"}
}, sorting: {field: "totalFunding", order: "desc"})
3. For each candidate of interest → profile(id) for dollar fields
C. "Top N by valuation" (two-stage pattern)
1. company_search(filters: {latestValuation: {min: 1000000000}},
sorting: {field: "totalFunding", order: "desc"})
2. Take top ~50 (totalFunding correlates roughly with valuation tier)
3. profile() each → real latestValuation
4. Sort client-side by latestValuation desc, take top N
The API can filter by valuation but not sort by it.
D. Recent funding ("raised >X in year Y")
1. company_search(filters: {latestFundingYear: {min: 2023},
funding: {min: 10000000}})
2. For round-level confirmation, vc_deals() each → inspect round dates
latestFundingYear filters on the most recent round; funding.min on the
cumulative total. If the user means "raised >$10M in 2023 specifically",
confirm with vc_deals round dates.
Field-shape gotchas
- Dollar fields are VARCHAR strings on
profile ("19400000000") — parse
client-side.
profile.investors[] is aggregated across rounds with duplicates — dedupe
before display.
vc_deals dates use split-VARCHAR pattern: {"year": "2017", "month": "3", "day": "9"}. day may be "". Compare as strings.
vc_deals rows where target is a different company are returned when the
queried company acted as an investor — filter by target_company_id == queried_id for self-funding only.
profile.summary is often empty for non-flagship entities — don't promise
it in assertions.
- There is no
latestValuation field on profile. Get it from
vc_deals (last round's valuation).
- Public companies have null/no private-valuation field; their market cap
isn't in PrivCo.
When building a fixture / assertion dataset
- Every "filter must exclude X" assertion needs a representative X inside
the dataset, otherwise the test passes vacuously. Pair each positive slot
with at least one negative control on the same axis (CA-software vs
NY-software, low-rev vs rev>1M, pre-2015 vs post-2015).
- Reuse rich anchor entities across many slots to keep the dataset small.
- Track coverage in an explicit matrix doc so the fixture set can be
re-validated when the assertion list changes.
Presenting company data — the dashboard prompt
When the user wants a company's data presented (a dashboard, one-pager,
or visual intelligence view) rather than answered in prose, invoke the MCP
server's built-in company_dashboard prompt. It chains the standard
Workflow A data pull (match → profile → financials → vc_deals / ma_deals → people) into a single HTML dashboard widget: header + 6 metric cards +
revenue/headcount charts + valuation range + financial/profile/funding
tables, with the PrivCo house style rules (CSS variables, Chart.js
conventions, sentence case) and the same field-shape gotchas listed above.
Access & auth — two transports, same 17 tools
- Remote connector (recommended): PrivCo-hosted endpoint at
https://mcp.privco.com/mcp (MCP Streamable HTTP). Auth is OAuth 2.1 —
the user signs in with their PrivCo account in a browser; no API key in
any config. Works in claude.ai, Claude Desktop, Claude Code
(claude mcp add --transport http), ChatGPT (developer mode), and any
spec-compliant client. Requires API access enabled on the PrivCo account.
- Local server (npm
privco-data-mcp, stdio): reads PRIVCO_API_KEY
from its environment (set in the MCP client config) and sends it to the
PrivCo v3 API as the x-api-key header. For automation/CI or key-based
setups.
- Tool names and schemas are identical on both; only the client-visible
prefix differs (it carries whatever server/connector name was registered).
- Full setup steps + troubleshooting for both paths:
INSTALL.md.
- For the full per-tool field reference, common pitfalls in one-line form, and
expanded workflow detail, read the
privco://docs/usage-guide resource from
the MCP server — the canonical, always-current reference in the contract.
When a tool returns an error
Errors come back as isError: true with a JSON body. Since privco-data-mcp
1.5.0 that body is classified, so branch on the fields rather than the prose:
{ "success": false, "error": "<message>", "status": 401,
"code": "unauthorized", "retryable": false }
unauthorized (401/403) — stop and tell the user. Bad key, out-of-term
subscription, or a missing per-endpoint grant (funding_search and
deal_search need one). Never retry; it cannot succeed.
rate_limited (429) — back off, then retry. If the allowance is exhausted
rather than throttled it stays 429 until the period resets.
bad_request (4xx) — the message relays the API's own validation text. Fix
the arguments; do not retry unchanged.
not_found (404) — re-run entity resolution instead of retrying.
upstream_error (5xx) / timeout / network_error — transient, one retry
is reasonable.
Two traps:
- Before 1.5.0 every failure returned one fixed string (
"Match request failed" and its per-tool siblings), so an invalid key looked exactly like a
throttle or a 500. On an older server, do not infer the cause from the text.
- A successful
tools/list does not mean the credentials are valid. The
catalogue is served to any request carrying a credential header, real or
not, on both the local and remote transports. The first real tool call is
the authentication check.
When this skill applies — and when not
Apply when the user is doing entity lookup, criteria-driven discovery,
fixture/dataset building, or any multi-tool MCP workflow against PrivCo data.
Skip when the user is editing the privco-data-mcp server source itself,
or when they're talking to the PrivCo REST API directly without the MCP
wrapper.
1---2name: privco-mcp-search-skill3description: Use the PrivCo MCP tools (`mcp__privco-data-mcp__*`) correctly for company / people / funding queries. Covers the non-obvious filter semantics (full state name, industry vs keyword, sorting enum, summary-row gaps, includeMissing), the standard match → profile → vc_deals workflow, and the two-stage pattern for "top N by valuation". Activate when the user wants to search/filter PrivCo data, look up a specific company, or build a multi-entity dataset. Trigger phrases: "find companies that…", "search PrivCo for…", "who are the X companies in Y", "look up <company> in PrivCo", "valuations for…", "build a fixture / dataset of companies matching…".4---56# PrivCo MCP Search78Setup / access (remote connector or local npm server): see `INSTALL.md` in this skill folder.9Full per-tool field reference: read the `privco://docs/usage-guide` resource from the MCP server (and `privco://docs/overview`) — the server ships the canonical, always-current reference in its own contract.10Guided workflows (present a dashboard, research a company, discover by criteria, top-N-by-valuation, recent funding): invoke the MCP server's built-in **prompts** — `company_dashboard`, `research_company`, `discover_companies_by_criteria`, `top_companies_by_valuation`, `recent_funding_query`. The distilled workflow sketches below mirror them for always-on steering.1112The bullets below are the high-value-per-line distillation — load them once,13do not re-derive by trial and error.1415## Tool inventory (17 tools)1617All tools are exposed under the `mcp__<server-name>__*` namespace once the18MCP server is registered — `mcp__privco-data-mcp__*` in the standard setup;19a remote connector carries whatever name it was registered under (see20"Access & auth" below and `INSTALL.md`).2122> **Permission-gated tools:** `funding_search` and `deal_search` are live,23> but require the API key to carry the matching `funding_search` /24> `deal_search` permission (or `everything`) — a key without it returns 403.25> If you lack the permission, approximate via `company_search` +26> per-candidate `vc_deals` / `ma_deals`.2728**Stage 1 — entity resolution (name/details → profile_id)**2930These two tools look similar but differ in **strictness**, **inputs accepted**,31and **whether they tell you how confident the match is**. Pick deliberately.3233- `match` — **strict, exact-ish matching** on company name and/or website34 only. Returns up to 20 ranked candidates as a flat list with no confidence35 score. Designed for: *"the user gave me a recognizable company name, give me36 the canonical row(s)."* Because the criteria are intentionally strict, it37 can **silently miss results when the input is incomplete or differs even38 slightly from the source data** (e.g., "Stripe" vs "Stripe, Inc.",39 "stripe.io" vs "stripe.com"). First row is usually correct for unique40 brands; common-word names need disambiguation by `profile_type`,41 `profile_classification`, `industry`. Pass `includeCityState: true` to42 surface HQ city/state in the result rows for co-located collisions.43 Inputs: `name`, `website`, `includeCityState`. GET endpoint, response shape:44 `{data: [{name, profile_id, profile_type, profile_classification, permalink, location, industry, website[]}], meta}`.4546- `identification` — **flexible, fuzzy / tolerant** matching that accepts47 many partial inputs (DBA, URL, address, country/state/city/zip, phone,48 industry, Facebook/LinkedIn/Twitter URLs, alternate names, reference IDs)49 and returns ranked candidates **with a confidence score**. Designed for:50 *"I have messy, partial, or fuzzy fields from an external source — give51 me the best mapping with a probability so I can decide whether to trust52 it."* POST endpoint to `/identification/company`. Each result row carries:53 - `mapping_strength` — integer 1–5 (higher = stronger). `mapping_strength_need_review: "Yes"` flags rows that should be human-reviewed before being trusted.54 - `matching_probability` — float 0–1 (raw confidence).55 - `match_summary` / `mismatch_summary` — human-readable strings explaining56 *why* the engine scored it that way (e.g. `"Exact Name Match"` /57 `"Reference ID Mismatch|Phone Mismatch"`).58 - Optional `threshold` query param (default 0.75) filters out59 low-confidence candidates server-side.60 Inputs (all optional except `name`): `name`, `dba`, `url`, `country`,61 `state`, `city`, `address`, `zip`, `phone`, `industry`, `social_facebook`,62 `social_linkedin`, `social_twitter`, `linkedin_url`, `classification`,63 `clean_name`, `fn` (formal/legal name), `aka`, `reference_id`,64 `company_only`.6566**Decision rule — when in doubt, prefer `identification`.** It is the full67entity-resolution engine: fuzzy and tolerant, accepts many combinable68signals beyond name/website, and returns a confidence score you can act69on. Use it whenever the input is fuzzy, partial, or from an external70source (CRM dump, prospect list, partner feed), when you hold extra71signals (address, phone, LinkedIn URL, reference ID), or when downstream72automation must gate on confidence. Reserve `match` for the quick first73try on a clean, recognizable name/URL — it matches on name/website ONLY,74strictly, and reports no confidence. **Always fall through to75`identification` when `match` returns 0 or visibly-wrong rows** — a76`match` miss does not mean the entity is absent from PrivCo.7778**Stage 2 — discovery (criteria → candidate list)**79- `company_search` — multi-filter (location, industry, keyword, revenue,80 valuation, funding, employees, growth, year founded, PE/VC inclusion). Up to81 50 summary rows per page.82- `people_search` — equivalent for contacts.83- `funding_search` — Round-centric search (fundingTypes, total, timestamp,84 industry, location, investor types, keyword). Use when the question is85 *about rounds* rather than *about companies*. Same gotchas as86 `company_search`: `keyword.condition` is required (`"should"` / `"must"`).87 Requires the `funding_search` (or `everything`) API permission.88- `deal_search` — M&A-deal-centric search (target/buyer/seller industries,89 deal value, query, isPeDeal, target PE/VC-backed flags). **Filter90 precedence quirk**: setting `isPeDeal` (any boolean) SUPPRESSES91 `inclusionExclusion.targetsIsPeBacked`/`targetsIsVcBacked`/`buyersAllOfType`92 — don't mix. Default sort: relevance if `query` set, else `timestamp desc`.93 Requires the `deal_search` (or `everything`) API permission.94- `industry_keyword` — autocomplete for keyword IDs (use before `keyword`95 filter when unsure).96- `industry_pics` — list of PICS top-level industry codes.9798**Stage 3 — enrichment (profile_id → full record)**99- `profile` — full **company** profile (revenue, employees, funding, founders,100 contacts, investors, competitors, subsidiaries).101- `general_profile` — full profile for **non-company** entities (Investor,102 Person, etc.). Use when `profile_type != "Company"` from `match`.103- `people` — execs & board on a company.104- `financials` / `revenue_financials` — multi-year statements / revenue-only105 time series.106- `vc_deals` — round-by-round funding (per-round valuation, amount, date,107 investors). Use this for `latestValuation` since `profile` doesn't carry it.108- `ma_deals` — M&A history.109110**Stage 4 — contact reveal (hash → details)**111- `company_contact` — resolve a contact hash to full businessEmail / phone /112 LinkedIn.113- `investor_contact` — same for investor-associated contacts.114- Both take the **opaque hash** from `people_search` row's `hash` field.115 **NOT** from `profile.contacts[]` — that block has no hash, just116 `{title, name, email}` summary.117- `associatedType` on the people_search row tells you which reveal endpoint118 to use: `Company` → `company_contact`, `Investor` → `investor_contact`.119- To find Investor-associated rows, you **must** pass120 `{"filters": {"investorOnly": true}}` to people_search — default is121 Company-only and title filters like `["Partner"]` are not effective.122- A successful call **consumes a contact-reveal credit** on the account —123 don't call speculatively.124125## Filter semantics — the gotchas126127### 1. `location.state` requires the full state name128129```jsonc130// WRONG — silently returns 0 hits131{"location": {"selection": [{"type": "state", "value": "NY"}]}}132// RIGHT133{"location": {"selection": [{"type": "state", "value": "New York"}]}}134```135136The response payload returns `"hq_state": "NY"`, so the response shape does not137telegraph the required input shape. Verified empirically: `"NY"` → 0 hits,138`"New York"` → 6,444 hits on the same query.139140### 2. `industry` vs `keyword` are distinct filters141142| Filter | Vocabulary | Example values | When |143|--------|-----------|----------------|------|144| `industry.rawIndustries` (or `.selection`) | PICS top-level | `"Software & Internet Services"`, `"Biotechnology"` | Broad industry buckets |145| `keyword.rawKeywords` (or `.selection`) | Tagged terms from `industry_keyword` autocomplete | `"Fintech"`, `"Payments"`, `"Generative AI"` | Specific business-model / tech tags |146147Passing a broad word like `"Software"` to `keyword.rawKeywords` returns **0148hits** — it isn't a registered keyword. Call `industry_keyword(query: "...")`149first when unsure.150151### 3. `inclusionExclusion` is a requirement, not an addition152153```jsonc154{"inclusionExclusion": {"pe": "include"}} // REQUIRE PE-backed155{"inclusionExclusion": {"pe": "exclude"}} // EXCLUDE PE-backed156```157158`"include"` means *must be* PE-backed, not *also include*. Combining with other159restrictive filters quickly empties the result set — drop one filter at a time160when debugging zero-result queries.161162### 4. `sorting.field` enum is small163164Valid: `name | state | employee | industry | yearFounded | totalFunding |165revenueGrowthRate1 | revenueGrowthRate3 | hasContact`.166167**Not sortable**: `latestValuation`, `latestRevenue`, `latestEbitda`. For "top168N by valuation" workflows, see Workflow C below.169170### 5. `revenue.includeMissing` quietly changes result composition171172`includeMissing: true` includes companies with null/empty revenue — common for173early-stage, pre-revenue, and stealth companies, which often have174`latestRevenue: null`. When testing "revenue > X" assertions, leave it175**false** (default).176177### 6. `keyword.condition` is required whenever `keyword` filter is used178179```jsonc180// WRONG — 400 Bad Request181{"keyword": {"rawKeywords": ["Fintech"]}}182// RIGHT183{"keyword": {"rawKeywords": ["Fintech"], "condition": "should"}}184```185186`condition` is `"should"` (match any keyword) or `"must"` (match all). It's187match-engine terminology — don't confuse with `inclusionExclusion`'s188`include` / `exclude` (different filter, different vocabulary).189190### 7. `profileType` path param is lowercase191192```jsonc193// WRONG — 400 with cryptic "profileType must be one of the following values: "194GET /profile/Company/44612195// RIGHT196GET /profile/company/44612197```198199`'company' | 'investor' | 'advisor'`. Affects `profile`, `general_profile`,200`people`, `vc_deals`, `ma_deals`. The MCP handlers don't normalize casing.201202### 8. `deal_search.isPeDeal` SUPPRESSES sibling filters203204Setting `filters.isPeDeal` to any boolean **silently disables**205`inclusionExclusion.targetsIsPeBacked`, `inclusionExclusion.targetsIsVcBacked`,206and `buyersAllOfType` in the same request. The precedence is intentional —207pick one approach per query:208209- Want *only* PE deals? → `{"isPeDeal": true}` (alone).210- Want deals where the *target* is PE-backed? → omit `isPeDeal`, use211 `{"inclusionExclusion": {"targetsIsPeBacked": "include"}}`.212213### 9. `deal_search` sort default depends on whether `query` is set214215When `sorting` is omitted: response is ordered by **relevance scoring** if216`filters.query` is set, otherwise by `timestamp desc`. Explicit `sorting`217always wins. Pass `{"sorting": {"field": "timestamp", "order": "desc"}}`218when you want deterministic ordering regardless of query presence.219220## Summary rows: included vs omitted221222`company_search` summary rows contain:223- ✅ `id`, `name`, `state`, `industry`, `url` (permalink), `keywords[]`,224 `classification` (Private/Public), `revenueGrowthRate1`,225 `revenueGrowthRate3`, `latestEbitda`226- ❌ `latestRevenue`, `latestValuation`, `totalFunding`, `yearFounded`,227 `ownersFounders`, `headquarters.city`, `summary`, funding-round detail, M&A228 detail229230If you need any omitted field, follow up with `profile` per company.231Round-level data → also `vc_deals`. M&A detail → also `ma_deals`. **Budget the232round-trips when planning multi-candidate work** — a 22-company dataset is233~22+ profile calls plus targeted vc_deals/ma_deals.234235## Practical workflows236237### A. Named-entity lookup238239```240match(name="Airbnb") → profile_id 78241profile(78) → full record242vc_deals(profileType="company", profileId=78) → all rounds + valuations243ma_deals(profileType="company", profileId=78) → M&A history244```2452463–4 calls per entity for full enrichment.247248### B. Filter-criteria discovery249250```2511. industry_keyword(query="fintech") → keyword id (if using id-based filter)2522. company_search({253 location: {selection: [{type: "state", value: "New York"}]},254 industry: {rawIndustries: ["Software & Internet Services"]},255 revenue: {min: 1000000},256 inclusionExclusion: {pe: "include"}257 }, sorting: {field: "totalFunding", order: "desc"})2583. For each candidate of interest → profile(id) for dollar fields259```260261### C. "Top N by valuation" (two-stage pattern)262263```2641. company_search(filters: {latestValuation: {min: 1000000000}},265 sorting: {field: "totalFunding", order: "desc"})2662. Take top ~50 (totalFunding correlates roughly with valuation tier)2673. profile() each → real latestValuation2684. Sort client-side by latestValuation desc, take top N269```270271The API can filter by valuation but not sort by it.272273### D. Recent funding ("raised >X in year Y")274275```2761. company_search(filters: {latestFundingYear: {min: 2023},277 funding: {min: 10000000}})2782. For round-level confirmation, vc_deals() each → inspect round dates279```280281`latestFundingYear` filters on the *most recent* round; `funding.min` on the282**cumulative total**. If the user means "raised >$10M in 2023 specifically",283confirm with `vc_deals` round dates.284285## Field-shape gotchas286287- Dollar fields are VARCHAR strings on `profile` (`"19400000000"`) — parse288 client-side.289- `profile.investors[]` is aggregated across rounds with duplicates — dedupe290 before display.291- `vc_deals` dates use split-VARCHAR pattern: `{"year": "2017", "month": "3",292 "day": "9"}`. `day` may be `""`. Compare as strings.293- `vc_deals` rows where target is a *different* company are returned when the294 queried company acted as an investor — filter by `target_company_id ==295 queried_id` for self-funding only.296- `profile.summary` is often empty for non-flagship entities — don't promise297 it in assertions.298- There is no `latestValuation` field on `profile`. Get it from299 `vc_deals` (last round's `valuation`).300- Public companies have null/no private-valuation field; their market cap301 isn't in PrivCo.302303## When building a fixture / assertion dataset304305- Every "filter must exclude X" assertion needs a representative X **inside306 the dataset**, otherwise the test passes vacuously. Pair each positive slot307 with at least one negative control on the same axis (CA-software vs308 NY-software, low-rev vs rev>1M, pre-2015 vs post-2015).309- Reuse rich anchor entities across many slots to keep the dataset small.310- Track coverage in an explicit matrix doc so the fixture set can be311 re-validated when the assertion list changes.312313## Presenting company data — the dashboard prompt314315When the user wants a company's data **presented** (a dashboard, one-pager,316or visual intelligence view) rather than answered in prose, invoke the MCP317server's built-in **`company_dashboard`** prompt. It chains the standard318Workflow A data pull (`match → profile → financials → vc_deals / ma_deals →319people`) into a single HTML dashboard widget: header + 6 metric cards +320revenue/headcount charts + valuation range + financial/profile/funding321tables, with the PrivCo house style rules (CSS variables, Chart.js322conventions, sentence case) and the same field-shape gotchas listed above.323324## Access & auth — two transports, same 17 tools325326- **Remote connector (recommended):** PrivCo-hosted endpoint at327 `https://mcp.privco.com/mcp` (MCP Streamable HTTP). Auth is OAuth 2.1 —328 the user signs in with their PrivCo account in a browser; no API key in329 any config. Works in claude.ai, Claude Desktop, Claude Code330 (`claude mcp add --transport http`), ChatGPT (developer mode), and any331 spec-compliant client. Requires API access enabled on the PrivCo account.332- **Local server (npm `privco-data-mcp`, stdio):** reads `PRIVCO_API_KEY`333 from its environment (set in the MCP client config) and sends it to the334 PrivCo v3 API as the `x-api-key` header. For automation/CI or key-based335 setups.336- Tool names and schemas are identical on both; only the client-visible337 prefix differs (it carries whatever server/connector name was registered).338- Full setup steps + troubleshooting for both paths: `INSTALL.md`.339- For the full per-tool field reference, common pitfalls in one-line form, and340 expanded workflow detail, read the `privco://docs/usage-guide` resource from341 the MCP server — the canonical, always-current reference in the contract.342343## When a tool returns an error344345Errors come back as `isError: true` with a JSON body. Since `privco-data-mcp`3461.5.0 that body is classified, so branch on the fields rather than the prose:347348```json349{ "success": false, "error": "<message>", "status": 401,350 "code": "unauthorized", "retryable": false }351```352353- `unauthorized` (401/403) — stop and tell the user. Bad key, out-of-term354 subscription, or a missing per-endpoint grant (`funding_search` and355 `deal_search` need one). **Never retry**; it cannot succeed.356- `rate_limited` (429) — back off, then retry. If the allowance is exhausted357 rather than throttled it stays 429 until the period resets.358- `bad_request` (4xx) — the message relays the API's own validation text. Fix359 the arguments; do not retry unchanged.360- `not_found` (404) — re-run entity resolution instead of retrying.361- `upstream_error` (5xx) / `timeout` / `network_error` — transient, one retry362 is reasonable.363364Two traps:365366- **Before 1.5.0 every failure returned one fixed string** (`"Match request367 failed"` and its per-tool siblings), so an invalid key looked exactly like a368 throttle or a 500. On an older server, do not infer the cause from the text.369- **A successful `tools/list` does not mean the credentials are valid.** The370 catalogue is served to any request carrying a credential header, real or371 not, on both the local and remote transports. The first real tool call is372 the authentication check.373374## When this skill applies — and when not375376**Apply** when the user is doing entity lookup, criteria-driven discovery,377fixture/dataset building, or any multi-tool MCP workflow against PrivCo data.378379**Skip** when the user is editing the `privco-data-mcp` server source itself,380or when they're talking to the PrivCo REST API directly without the MCP381wrapper.