Inven MCP Server
Description
The Inven MCP Server gives AI assistants (Claude, Cursor, ChatGPT, and others) direct access to Inven's database of over 60 million private companies, investors, public companies, and M&A / funding deals.
Through natural language alone you can search for companies, retrieve structured financial and operational data, explore your saved Inven lists, and look up detailed profiles for people and deals — all without leaving your AI client.
Features
- Company search: Translate a natural language description into a structured search across 20M+ private companies. Filter by industry, geography, headcount, revenue, funding stage, ownership type, founding year, and dozens more criteria.
- Investor search: Discover VCs, PE firms, angels, and family offices by geography, stage focus, portfolio themes, and fund type.
- Public company search: Build a multiples universe of listed companies by sector, exchange, size, and valuation.
- Deal search: Find M&A deals and funding rounds by deal type, size, date, acquirer/target characteristics, and geography.
- People search: Find professionals by current role, title, employer characteristics, past company, or founder background. Returns paginated member preview rows (name, title, employer, location) ready for further enrichment.
- Contact lookup: Resolve verified emails, phone numbers, LinkedIn URLs, and job titles for contacts — by company domain, by member/experience pair, by LinkedIn URL, or by name + domain, with optional title filtering.
- Rich data columns: Choose exactly which data fields to retrieve per result — financials, headcount, web traffic, funding history, contacts, and more.
- Saved lists: Access your existing Inven company, deal, and people lists and enrich them with structured data.
- Upstream dedupe: Exclude one or more saved lists (or explicit domains) from a company search at build time, so the search only ever returns companies you do not already have — and you never consume export volume on names you would discard. The same works in reverse: restrict a search to a saved list or an exact set of domains.
- Point-lookup: Retrieve detailed profiles for specific companies, people, or deals by name, domain, or ID.
-
Public-company filings: Ask
get_company_infofor "annual reports", "10-K", "10-Q", "earnings release", or similar and it appends a list of available filings (with directly fetchable external URLs, e.g. SEC EDGAR) for resolved listed companies. -
Raw SQL (opt-in): Run paginated Snowflake
SELECTstatements directly against curatedMCP_PUBLICviews (companies, members, member experiences, member metrics, M&A transactions, company locations, private financials). Includes a free two-step schema-discovery tool with units and enum hints, plus a free 5-row dry-run for iterating on filters before paying credits. Available only to organisations with the SQL tools package enabled.
Setup
Option A — OAuth 2.1 (recommended for Claude.ai and ChatGPT)
- Visit the Anthropic MCP Directory at claude.com/connectors and find Inven.
- Click Connect and complete the OAuth sign-in using your existing Inven account.
- No additional configuration is required — the client discovers all endpoints automatically.
The connector serves users whose Inven account is active: an invited member, or a catch-all SSO user holding an active trial or an activated account. A user whose trial has ended, or who has never started one, is refused at connect time and on every call with a message pointing to https://app.inven.ai, where the trial is started or the account activated; reconnecting afterwards needs no admin step.
Option B — Legacy Bearer key (for Claude Desktop, Cursor, and programmatic access)
-
Obtain an MCP key from your Inven administrator (
POST /admin-api/mcp-key). - Add the following block to your MCP client configuration:
{
"mcpServers": {
"inven": {
"type": "streamable-http",
"url": "https://api.inven.ai/mcp/v1",
"headers": {
"Authorization": "Bearer <your-mcp-key>"
}
}
}
}
Authentication
Two authentication methods are supported simultaneously:
OAuth 2.1 (Authorization Code + PKCE)
The server presents itself as an OAuth 2.1 Authorization Server and proxies
the flow to Cognito. PKCE is required and S256 is the only
supported challenge method.
| Endpoint | Description |
|---|---|
GET /.well-known/oauth-protected-resource |
Protected resource metadata (RFC 9728) |
GET /.well-known/oauth-authorization-server |
Authorization server metadata |
POST /register |
Dynamic Client Registration |
GET /authorize |
Starts the OAuth flow |
GET /auth/callback |
Cognito redirect target |
POST /token |
Token exchange and refresh |
An unauthenticated request to /mcp/v1 returns
401 with a WWW-Authenticate header whose
resource_metadata parameter points at the protected resource
document, so a client can discover the whole flow from the MCP endpoint
alone.
Legacy static MCP key
Pass a pre-issued key in every request:
Authorization: Bearer <your-mcp-key>
Examples
Example 1: Search for B2B SaaS companies in the Nordics
User prompt: "Find B2B SaaS companies in the Nordic countries with 50–500 employees that have raised Series A or B funding"
What happens:
-
build_company_searchtranslates the description into a structured search and returns asearch_idwith an estimated result count. -
build_columns(entity"company") selects relevant columns (company name, country, headcount, funding stage, last round date). -
run_company_searchexecutes the search and returns a paginated table of matching companies.
Example 2: Look up financials (and filings) for a list of known companies
User prompt: "Get the latest revenue, EBITDA, and headcount for Stripe, Klarna, and Revolut"
What happens:
-
get_company_inforesolves each company name against the Inven database, calls an LLM to select the relevant data fields, and returns structured rows for all three companies in a single call — no search or column selection step needed. -
If the data description mentions filings (e.g. "annual report", "10-K",
"10-Q", "earnings release"), the response also appends a list of available
public-company filings (with directly fetchable external URLs) for any
resolved listed companies. This requires the
profile_public_financialspermission.
Example 3: Explore recent fintech M&A deals
User prompt: "Show me European fintech acquisitions from 2023 and 2024 with deal values above €50M, including buyer names and deal size"
What happens:
-
build_deal_searchtranslates the deal criteria into a structured deal search and returns asearch_id. -
build_columns(entity"deal") selects the requested columns (buyer name, target name, deal size, close date). -
run_deal_searchexecutes the search and returns a paginated table of matching deals.
Example 4: Enrich a saved Inven list
User prompt: "Take my 'Pipeline Q2' company list and give me the headcount trend and last funding round for each company"
What happens:
-
get_lists(entity"company") returns a page of the user's saved lists with IDs and names; passname_containswhen you know which one you want rather than paging through everything. -
get_list_contents(entity"company") fetches the website domains of positively-marked companies on the chosen list, one page at a time (checkhas_more). -
get_company_info(called in batches of up to 100 domains) retrieves headcount and funding data for every company and returns the results as a structured table.
Example 5: Find only companies that are not already on a saved list
User prompt: "Find UK accounting firms with 50–500 employees, but skip anything already on my master list"
What happens:
-
get_lists(entity"company") returns the user's saved lists; the master list'slist_idis picked from the result. -
build_company_searchis called with the description andexclude_list_ids=[<list_id>]. The exclusion is part of the search itself, so theestimated_total_resultsalready has the list's companies removed — both the ones marked positive and the ones marked negative, matching what the Inven UI does when you exclude a list from a search. -
run_company_searchreturns only new names, so no export volume is consumed on companies the user already has.
Notes:
-
exclude_list_idsis a structured parameter, not an instruction insidedescription. Asking for a list exclusion in the description alone is interpreted by the language model and is not guaranteed to be applied; the parameter always is. An id you cannot access is returned as an error rather than being ignored. -
Use
exclude_domainsfor domains you already hold (e.g. from a spreadsheet). They are resolved by exact lookup rather than by name similarity, so a domain is never swapped for a similarly-named company's. -
Refining a search (calling
build_company_searchwith asearch_id) carries the original search's exclusions over automatically and unions in any new ones, so exclusions cannot be lost by refining. -
There is no need to call
get_list_contentsfirst when the exclusion source is a saved list. -
include_list_idsandinclude_domainsare the mirror image: they restrict the search to a saved list or an exact set of domains. Each inclusion source narrows the search, so passing both returns only companies that are on one of the lists and in the domain set. Refining replaces inclusions rather than accumulating them (pass nothing to keep the current one), and a company matched by both an inclusion and an exclusion is dropped. -
Do not use
include_domainsfor reference companies you want lookalikes of — put those indescription, where semantic ranking runs over the whole universe.
Example 6: Find European growth-stage VCs focused on climate tech
User prompt: "Find European VCs that invest in climate tech or cleantech at Series A and B stage"
What happens:
-
build_company_search(dataset="investor")translates the description into a structured investor search and returns asearch_id. -
build_columns(entity"company") selects overview columns (fund name, country, focus areas, AUM). -
run_company_searchreturns a paginated list of matching investors.
Example 7: Find CFOs and finance leaders at European fintechs
User prompt: "Find current CFOs and VPs of Finance at European fintech companies with 100–1000 employees"
What happens:
-
build_people_searchtranslates the description into a structured people search and returns asearch_id. -
run_people_searchexecutes the search and returns paginated rows with name, title, employer domain, and location — no column selection step needed, and no emails or phone numbers. -
Optionally, pass the returned
member_id/experience_idpairs toget_people_infofor full profile data.
To get one company's people list instead — the equivalent of the
People section of an Inven company profile — name that
company in the description: "list the people at acme.com". Scoping to a
single company raises the per-company cap from 3 to a full roster of up to
500, so run_people_search returns the staff list with names and
titles, which the assistant can then filter by title. Names and titles cost
no contact credits; resolve the shortlisted people's emails and phone
numbers afterwards with get_company_contacts.
Example 8: Look up CEO contacts for a short list of companies
User prompt: "Find verified email addresses and phone numbers for the CEOs of stripe.com, klarna.com, and revolut.com"
What happens:
-
get_company_contactsis called withdomainsand atitles=["CEO"]filter. Common abbreviations are expanded server-side (so "CEO" also matches "Chief Executive Officer"). All three titled domains at once cost3 * (2 + 1) = 9against the budget below, so the lookup goes out as two calls —["stripe.com", "klarna.com"], then["revolut.com"]. - The tool returns the top contacts per domain with verified email, phone, LinkedIn URL, job title, and location. One contact credit is deducted per newly-resolved contact.
You can also resolve contacts for specific people instead of (or in addition to) domains:
-
people: member_id/experience_id pairs fromrun_people_searchorget_list_contents(entity"people"). -
linkedin_urls: objects with the required keylinkedin_url(noturl), e.g.{"linkedin_url": "https://linkedin.com/in/jane", "name": "Jane Smith"}. -
named_people: a person name + company domain for fuzzy matching (falls back to Inven people data if contact providers miss the name).
Each call must fit a provider-call budget of 8 (contact providers are rate
limited): a domain costs 1 + max_contacts_per_domain, a
people/linkedin_urls lookup costs 1, and a
named_people lookup costs 5. Setting titles adds a
second provider search per domain, so a titled domain costs
2 + max_contacts_per_domain. When a domain request does not
fit, max_contacts_per_domain is reduced to the largest value
that does, and the response note says what it became and why.
It is capped at 7 to begin with, and a value outside 1–7 is clamped into
range and reported the same way. A call is still rejected when even one
contact per domain cannot fit, or when the
people/linkedin_urls/named_people
costs alone exceed 8 — those are fixed, so split them across several calls.
Example 9: Quantitative analysis directly against the Inven data warehouse (SQL tools)
User prompt: "What's the median headcount growth over the last 12 months for SaaS companies in the Nordics with 50–500 employees?"
What happens (requires the SQL tools package to be enabled for the organisation):
-
get_sql_schema()returns the available schema selection groups (companies,people,deals,locations,private-financials) without column metadata. -
get_sql_schema(selections=["companies"])returns column metadata, units, and known enum values for just the requested groups. -
dry_run_sql(sql)runs the candidate SnowflakeSELECTcapped at 5 rows for free, so the query can be iterated on without paying credits (it still needs a balance above zero). -
run_sql(sql, limit, offset)runs the full paginated query (2 AI credits per call; returned rows count against export volume; ≤1000 rows/page, 20-second timeout). -
If the query is too heavy to finish in 20 seconds,
start_sql_query(sql, limit, offset)runs it in the background instead and returns ajob_idto collect the rows with once it finishes.
Tools
Company search
Investor and listed-company searches run through these same tools by passing
a dataset argument —
build_company_search(dataset="investor") or
build_company_search(dataset="public") (default
"company"), and likewise for previews (build_company_search(dataset=..., save=false)). There are no separate build_investor_search /
build_public_company_search tools; results always come back via
run_company_search. The argument is authoritative:
"investor" and "public" restrict the universe even
when the description carries no criteria specific to it, and the result says
so in interpretation_notes.
Every build/preview/refine result carries parameters (the
filters that were actually applied) alongside
interpretation_notes — the caveats the query interpreter
raised: criteria it could not express as a filter, assumptions it had to
make, and criteria it dropped. A criterion that appears in neither
parameters nor interpretation_notes was understood
and applied; one that appears only in the notes was not. A criterion you
stated that no filter reflects at all is named outright, as a
missing_criterion: entry quoting the phrase — so a criterion
that was dropped rather than applied is reported to you instead of being
left to be noticed in the filters.
A note is free text the interpreter wrote about its own reasoning, so it can
describe a mapping it then failed to make — claiming it read "European" as
the Europe location group when nothing by that name exists and validation
dropped it. Where the filter family a note belongs to ended up empty, the
note carries a
[correction: this category produced no filter at all …] marker.
parameters.filters remains the only ground truth: read the
notes for what might be missing, then confirm against the filters.
estimated_total_results is 0 when the filters
match nothing; null means the estimate was unavailable. The
same reading applies to total_results on the
run_* tools: a page that came back empty because the search
matched nothing reports 0, so null there is a
missing count rather than an empty result set. An interpretation is reused
for 30 minutes per user — for company, people and deal searches alike — so
repeating a description — previewing it and then building it, most commonly
— returns the same filters and the same estimate, and the repeat call spends
no tokens and costs only the 2-credit per-call minimum. Estimates from two
different descriptions are still not comparable: each is
interpreted separately into a different semantic prompt, which moves the
estimate on its own, so measure a filter's effect by holding the description
fixed and varying only the structured parameters.
The run_* tools report retrievable_results next to
total_results: the number of rows paging can actually reach,
which a large search caps below the true match count. The rows past it are
not behind one more page — when retrievable_results is the
lower of the two, narrow the filters rather than paging toward a total you
cannot reach. Pagination for run_people_search is over people,
so its cap is a people count like total_results.
Structured filters decide which companies are in the result set; the
semantic prompt only decides what order they come back in. There is no
relevance cut-off, so a broad description over a large filter set puts the
on-topic matches first and keeps going well past them. Narrow with
structured filters rather than paging deeper, and treat
estimated_total_results as the size of the filter match, not as
a count of relevant companies.
The topical half of a description is the part most likely to be lost in
interpretation, and a search that lost it looks normal: a plausible count
over a set nothing has ranked. When no descriptivePrompt,
keywords or example companies survive, the result says so in
interpretation_notes under search_relevance. An
investor search carries its subject on portfolioParams — what
the portfolio companies do is what the search is about — so a description
there counts and the note is not raised. Treat that note as a failed search
rather than a broad one — no page of it is more relevant than any other —
and re-run with the business activity stated plainly. It is not raised when
you pinned the search with include_domains or
include_list_ids: you chose the members yourself, so there is
no ranking question to answer.
Some filters are inferred from the description and restrict results more
than their wording suggests. portfolioTenure: N is the one to
watch: it does not merely prefer companies that might sell, it keeps only
companies currently held in a private-equity portfolio whose last equity
deal was N–15 years ago, dropping every company that is not PE-owned. It
appears in parameters.filters and, when applied, in
interpretation_notes. Re-run without the holding-period wording
if you did not intend it.
Each company is stored under exactly one domain, and it is not always the
one you would type. Spotify is lifeatspotify.com;
spotify.com is an alias that identifies the company but is not
a company row itself. Both get_company_info and the
include_domains / exclude_domains filters follow
aliases to the domain the company is stored under, and both say when they
did — potential_issues for the first,
interpretation_notes for the second. A domain that is a company
in its own right is never remapped, so a subsidiary keeps matching itself
rather than its parent group. Reuse the resolved domain, since it is the one
that appears in run_company_search rows.
A domain matching no company at all is reported the same way rather than
passing silently, which is what previously made a typo indistinguishable
from a filter that worked. potential_issues also names the
identifier behind each caveat, so a bulk lookup tells you which of your
inputs failed rather than how many.
dataset accepts only company,
investor and public. Anything else is an error
rather than a fallback to the default, since a misspelled dataset would
otherwise return a plausible result from the wrong universe.
Ambiguous criteria are flagged for you to ask about
Some phrasings have more than one filter reading, and picking one silently produces the worst kind of result: a plausible count over a criterion the user never asked for. "No VC funding" is the canonical case — it could mean never having raised from a VC fund, never having raised institutional money at all, or not having raised recently, and those return different companies.
build_company_search (including
save=false previews) audits its interpretation against the
description it came from. When one criterion turns out to hang on a reading
like this, the search is still built on the first reading and the question
is handed to you: interpretation_notes carries a
clarification_needed: entry naming the phrase, listing the
candidate readings, and stating which one the filters committed to. Put that
question to the user before presenting the results as matching the
criterion, then call again with their answer stated plainly in the
description.
Up to three criteria are flagged per build, most result-changing first, and
only when a different reading would return a materially different set. An
explicit country, a numeric range you stated, or a named company is never
flagged. The server cannot ask the user itself — it has no interactive
channel to them — so a clarification_needed: note that you
resolve yourself is the only thing standing between a guessed filter and a
confident-looking answer built on it. Resolve every note, not just the
first: a description like "fast growing SaaS with no VC funding" carries two
guesses, and stopping after one leaves the other unconfirmed.
| Tool | Annotation | Description |
|---|---|---|
build_company_search |
side-effecting |
Persist a natural language company search to history (pass
dataset="investor" or "public" for those
universes). Pass save=false to preview without saving,
or an existing search_id to refine that search (its
refinement text goes in description); on refine the
original search's exclusions carry over and accumulate, inclusions
carry over unless you pass new ones. Optional
exclude_list_ids / exclude_domains remove
saved lists or explicit domains inside the search itself;
include_list_ids /
include_domains restrict it to them. Returns
search_id and interpretation_notes. Costs
AI credits in proportion to the LLM tokens used, at least 2 per
call.
|
build_columns |
side-effecting |
Select data columns via natural language for entity
company or deal. Returns
column_selection_id for the matching
run_*_search. Pass an existing
column_selection_id to add the described columns to it
(a new id is returned). Costs AI credits in proportion to the LLM
tokens used, at least 2 per call.
|
get_available_columns |
side-effecting |
Browse a field catalogue for entity
company, deal or people. For
company, call with no categories to list
category names with field counts, then with
categories=[...] for the ids; the drill-down returns
the ids alone and does not repeat the category list. The smaller
deal and people catalogues return every
field id in one call. Costs 2 AI credits per call.
|
run_company_search |
side-effecting |
Execute a search and return paginated company rows. Each row's
domain is the matched company's own domain, which for a
subsidiary is not its parent group's domain, and
inven_url is a deep link to that company's Inven
profile. A company the data store cannot hydrate is omitted rather
than returned as an empty row, so a full page can return fewer rows
than limit. Costs 2 AI credits per call. Returned rows
count against the organization's export volume.
|
get_company_info |
side-effecting |
Retrieve structured data for up to 100 specific companies by name or
domain. Returns a companies list pairing each resolved
company with a url deep link to its Inven profile. When
the data description mentions filings (e.g. "annual report", "10-K",
"10-Q", "earnings release"), also appends available public-company
filings (with fetchable external URLs) for resolved listed companies
— requires the profile_public_financials permission.
Costs AI credits in proportion to the LLM tokens used, at least 2
per call. Returned rows count against the organization's export
volume.
|
Deal search
A deal search filters up to five things at once: the deal itself (transactionParams
— type, size, date, multiples) and the companies on each side of it
(targetParams, investorParams for the buyer,
sellerParams, advisorParams, or
anyPartyParams when one company definition should match
whichever side it appears on). parameters.filters reports only
what the interpretation actually set, so a party you did not describe is
absent rather than present and empty — if investorParams is
missing, nothing constrains the buyer. summary names each party
by the filters applied to it instead of restating their values, which are in
parameters.filters directly.
| Tool | Annotation | Description |
|---|---|---|
build_deal_search |
side-effecting |
Persist a natural language deal search. Returns
search_id. Pass save=false to preview
without saving, or an existing search_id to refine that
search (its refinement text goes in description).
Select columns with build_columns(entity="deal"). Costs
AI credits in proportion to the LLM tokens used, at least 2 per
call.
|
run_deal_search |
side-effecting | Execute a deal search and return paginated deal rows. Costs 2 AI credits per call. Returned rows count against the organization's export volume. |
get_deal_info |
side-effecting | Retrieve structured data for specific deal IDs. Costs AI credits in proportion to the LLM tokens used, at least 2 per call. Returned rows count against the organization's export volume. |
People search
run_people_search rows never carry an email address or a phone
number, whether or not the organisation has resolved that person before. A
search tells you who someone is and where they work;
get_company_contacts is what hands out contact details, and it
charges nothing for a person the organisation already resolved — so routing
through it costs nothing extra and keeps every contact detail attributable
to a call that asked for one.
| Tool | Annotation | Description |
|---|---|---|
build_people_search |
side-effecting |
Persist a natural language people search. Returns
search_id. Pass save=false to preview
without saving, or an existing search_id to refine that
search (its refinement text goes in description).
Naming a specific employer (by company name or domain) or an Inven
saved list scopes the search to it and raises the per-company cap to
a full roster of up to 500 people; an unscoped search returns only
the top 3 people per company. Costs AI credits in proportion to the
LLM tokens used, at least 2 per call.
|
run_people_search |
side-effecting |
Execute a people search and return paginated member preview rows. No
column selection needed. Each row includes an
inven_url deep link to the person's employer profile in
Inven (when the employer resolves). Costs 2 AI credits per call.
Returned rows count against the organization's export volume.
|
Conference search
Conference tools need the organisation's conference search feature;
organisations without it do not see them. A conference search returns
conference editions — one event in one year — in the search's own ranking,
the same order an opened Inven tab shows. An event topic or attending
companies rank editions by match (an event's editions share its score,
upcoming ones first); a search with neither ranks them by how many
companies attend. Rows are not sorted by date. Describing the attending
companies ("events where Nordic fintech companies exhibit") runs a company
search inside the conference search, and editions where the matching
companies attend rank first. As with the other searches, read
interpretation_notes for criteria that could not be expressed,
and parameters.filters for what was applied.
| Tool | Annotation | Description |
|---|---|---|
build_conference_search |
side-effecting |
Persist a natural language conference search (topic, attending
companies, partners, organizers, location, dates, in-person or
virtual, size). Returns search_id and
interpretation_notes. Pass save=false to
preview without saving, or an existing search_id to
refine that search (its refinement text goes in
description). Costs AI credits in proportion to the LLM
tokens used, at least 2 per call.
|
run_conference_search |
side-effecting |
Execute a conference search and return paginated edition rows
(edition_id plus edition, series, organizer, dates,
location, format, company count, industry, description, website, and
the matching attendees when the search filtered on attending
companies). No column selection needed; the full attendee roster is
left to get_conference_info. Costs 2 AI credits per
call. Returned rows count against the organization's export volume.
|
get_conference_info |
side-effecting |
Read the conference profile of up to 25 editions by
edition_id, section by section (overview
by default; related_editions, attendees,
conference, editions,
attendance on request). Sections the organisation's plan
does not include, and ids that resolve to no edition, are named in
potential_issues. Costs 2 AI credits per call. Each
returned edition counts against the organization's export volume.
|
Saved lists
| Tool | Annotation | Description |
|---|---|---|
get_lists |
side-effecting |
List the user's saved Inven lists for entity
company, deal or people, with
IDs, names and positive_count, one page at a time:
name_contains narrows by name (case-insensitive
substring), limit/offset page through the
rest, and the result carries total_count and
has_more. Ordered by name rather than recency, so
editing a list cannot move it to another page mid-browse. Each list
includes a url deep link that opens it in Inven. Costs
2 AI credits per call.
|
get_list_contents |
side-effecting |
Return the positively-marked contents of a list, keyed by
entity: company → domains,
deal → transaction_ids,
people → members (member/experience
pairs). Paged: limit (default 100, the batch the
matching get_*_info tool accepts) and
offset, with count,
total_count and has_more. The result
includes a url deep link to the list in Inven. Costs 2
AI credits per call.
|
get_people_info |
side-effecting |
Load full profile data for specific member/experience pairs. The
person's own LinkedIn page is person_linkedin_url;
employer_linkedin_url is their employer's company page
— a row can carry one without the other. Costs 2 AI credits per
call. Returned rows count against the organization's export volume.
|
Contacts
| Tool | Annotation | Description |
|---|---|---|
get_company_contacts |
side-effecting |
Resolve verified emails, phone numbers, LinkedIn URLs, and job
titles. Input modes: domains (optional
titles filter and
max_contacts_per_domain),
people (member_id/experience_id pairs from people
search), linkedin_urls (objects with required key
linkedin_url, not url; optional
name/title/company_name hints), or named_people (name +
company domain, with Inven people DB fallback). At least one input
mode required. Contact providers are rate limited, so each call must
fit a provider-call budget (a domain costs
1 + max_contacts_per_domain, a people/LinkedIn lookup
costs 1, a named_people lookup costs 5, and a
titles filter adds one more search per domain); a
domain request that does not fit has
max_contacts_per_domain reduced to what does and the
change reported in the response note, while a call is
rejected when even one contact per domain cannot fit or when the
people/LinkedIn/named_people costs alone exceed the budget.
Long-running calls may return partial results (truncated=true
with
unprocessed_domains/unprocessed_people_count) — call again for the
remainder. Costs one contact credit per newly-resolved contact
(already-resolved contacts are not charged again), plus 2 AI credits
per call and, when titles is given, AI credits in
proportion to the tokens the per-domain title matching uses.
|
Raw SQL (opt-in, organisation-gated)
Available only to organisations with the SQL tools package enabled. Targets
curated MCP_PUBLIC_DB.MCP_PUBLIC.* Snowflake views
(COMPANIES, MEMBERS,
MEMBER_EXPERIENCES, MEMBER_METRICS,
MNA_TRANSACTIONS, GMAPS_POI_INFO, plus
private-financial views). Multi-statement scripts are rejected and trailing
semicolons are stripped. Only SELECT /
WITH statements are allowed.
Statements you supply are additionally screened server-side and rejected if
they reference INFORMATION_SCHEMA, the
SNOWFLAKE shared database, Snowflake history functions,
RESULT_SCAN / LAST_QUERY_ID,
IDENTIFIER(...), GET_DDL,
SYSTEM$ functions, or session-context functions that expose the
shared MCP identity (CURRENT_USER, SESSION_USER,
CURRENT_ROLE, CURRENT_ACCOUNT,
CURRENT_WAREHOUSE, …; CURRENT_DATE /
CURRENT_TIMESTAMP remain allowed). This applies to your SQL
only — get_sql_schema reads
INFORMATION_SCHEMA.COLUMNS itself to describe the views, which
is why it can return column metadata that your own queries cannot reach.
GMAPS_POI_INFO (the locations group) is queryable
one company at a time. Every statement referencing it must constrain
GROUP_MAIN_SITE — = '<domain>',
IN ('<domain>', …), IN (<subquery>),
or an equality join to another table's domain column such as
COMPANIES.CLEAN_WEBSITE. A statement that reads the view
unscoped is rejected before it runs, so retrieve site data for the companies
you are working with rather than scanning the view.
Recommended flow: get_sql_schema() (free; lists selection
groups) → get_sql_schema(selections=[...]) (free; columns for
the requested groups) → dry_run_sql(sql) (free 5-row preview,
but needs a balance above zero) →
run_sql(sql, limit, offset) for paginated results. When
paginating, ORDER BY must end in a unique column (an id): each
page is a separate execution, so tied rows otherwise move between pages.
Querying your own lists and searches
The views hold Inven's data, not yours: a saved list and a search you just
built exist outside them, so SQL alone cannot filter, rank or aggregate
either. upload_to_sql(list_id=…, …) and
upload_to_sql(search_id=…, …) close that gap by loading those
rows into a table of your own that the SQL tools read alongside the views —
which is what turns "give me the next page of this list" into "bucket this
list by country and revenue band, and count the deals its companies appear
in".
Both take the columns to load either as fields (column ids) or
as a column_selection_id from
build_columns (entity "company" /
"deal"); people have no selection builder, so a people list or
people search names its fields, which
get_available_columns (entity "people") lists.
Only positively-marked list rows are loaded, and a search is re-run at
upload time, so the table holds the results as they stand then.
What the table looks like:
-
ROW_IDis the row's identity and its join key into the views: a company's own domain (COMPANIES.CLEAN_WEBSITE), a deal id, ormember_id,experience_id. -
One column per field, typed from the field itself — numbers are numeric,
dates are
DATE, list-valued fields areARRAY— so aggregates work without casting. -
Entity-link fields get a
<COLUMN>_IDcompanion holding the linked entity's domain or id, because the visible column holds its name. Money fields get a<COLUMN>_CURRENCYcompanion naming the currency each amount is in — standardized fields likelatest_revenueare USD, a specific year's filed figure is in the currency it was filed in — because the bare number is otherwise ambiguous. -
The table name is prefixed
TMP_USER_TABLE_, so it can never shadow a view. Query thetable_namethat comes back.
What to expect of its lifetime:
-
The table is private to you and lives in a session Snowflake caps at four
hours;
session_expires_in_secondssays how much is left.get_sql_schemalists the tables still there, and a query against an expired one returns anerrortelling you to upload again rather than "table not found". -
An upload loads at most 1,000,000 cells (so 50,000 rows only at 20 columns
wide) and reports
truncatedwhen the range was cut short. Load the rest withrow_start/row_endplusappend, which requires the same source and the same columns. -
Uploading to a
table_namethat already exists replaces it, so re-running an upload refreshes the data rather than doubling it. - Uploads cost 2 AI credits and do not count against export volume — rows are counted when a query returns them, so the same rows are not paid for twice.
Queries that need longer than run_sql grants
run_sql waits for the statement inside the tool call, which
caps it at a 20-second Snowflake timeout: a wide scan or an aggregate over a
whole view is killed mid-flight, and you wait the full 20 seconds to find
out.
For those queries, submit the work instead of waiting for it.
start_sql_query(sql, limit, offset) hands the statement to the
warehouse and returns a job_id straight away, with a 300-second
(5-minute) ceiling instead of 20 seconds
(MCP_SQL_ASYNC_TIMEOUT_S). Poll
get_sql_query_status(job_id) every
poll_interval_seconds until status is no longer
working, then call get_sql_query_result(job_id) to
collect the rows. cancel_sql_query(job_id) stops a query you no
longer want — worth doing, because nothing else is waiting on it to stop it
consuming warehouse time.
A few things worth knowing:
-
The statement is compiled before it is submitted, so syntax errors,
unknown columns and permission problems are reported by
start_sql_queryitself: a malformed query fails immediately, creates no job, and costs nothing. -
When a job does fail,
errorcarries Snowflake's own message — the compilation error and its position — not just a status name, so you can correct the query and resubmit. -
elapsed_secondsstops advancing once the query stops, so it tells you how long the query actually ran rather than how long ago you submitted it. -
cancel_sql_queryreportscancelledonly when Snowflake confirms the query stopped, and passes Snowflake's own words through instatus_message. Snowflake will only cancel a statement it has started executing, so a job still queued for warehouse capacity staysworkingand theerrorasks you to call again once the status says it is running; a job that turns out to have finished in the meantime comes backcompletedwith no error. -
columnsholds the output column names in row order, soSELECT *is readable without knowing the view's layout. -
get_sql_query_resultchecks the job's state itself, so you do not have to pollget_sql_query_statusbefore collecting a finished job — polling is for watching progress and for reading a failure's message. -
A job returns the single page you submitted it for. To read further rows,
start another job with a higher
offset;has_moretells you whether there are any. Because each page is a separate execution,ORDER BYmust end in a unique column (an id) or tied rows will repeat on one page and be skipped on another.pagination_warningis set only when you are in a position to page (offset > 0orhas_more) and the ordering could not be shown to be total, so ordering by an id — or by everyGROUP BYkey — silences it. - Polling is free and does not re-run anything. Rows are charged for once, so re-reading a completed job's result costs nothing.
-
Jobs are forgotten 24 hours after submission, after which the
job_idis reported as unknown.
| Tool | Annotation | Description |
|---|---|---|
get_sql_schema |
read-only |
List available schema selection groups (companies,
people, deals, locations,
private-financials) when called with no
selections, or return live
INFORMATION_SCHEMA.COLUMNS metadata for the requested
groups, augmented with curated per-column descriptions (units,
format conventions, ARRAY / VARIANT notes) and
enum_values for known categorical columns. The
drill-down returns the columns alone and does not repeat the
selection list. Both steps also return session_tables —
the tables you have uploaded from your own lists and searches, each
with its source, row count and column count. Pass
include_session_columns=true for the per-column list;
it is off by default because upload_to_sql already
returns a table's columns when it creates it. Free.
|
upload_to_sql |
destructive |
Load a saved list (list_id, positively-marked rows
only) or a built search (search_id from
build_company_search / build_deal_search /
build_people_search, re-run into its ranked rows) into
a private SQL table the SQL tools can query and join against the
views. Pass exactly one of list_id /
search_id. Columns come from fields or a
column_selection_id. At most 1,000,000 cells per
upload; row_start / row_end with
append load the rest. Uploading to an existing
table_name replaces that table unless
append is set. The table lives in a session
Snowflake caps at four hours. Costs 2 AI credits per call and does
not count against export volume — rows are counted when a query
returns them.
|
dry_run_sql |
read-only |
Run a Snowflake SELECT against the MCP_PUBLIC views and
your uploaded tables, capped at 5 rows. Charges nothing but requires
an AI-credit balance above zero; surfaces Snowflake errors (syntax,
timeout, etc.) via the error field. Use for fast
iteration on filters before calling run_sql.
|
run_sql |
side-effecting |
Run a paginated Snowflake SELECT against the MCP_PUBLIC
views and your uploaded tables (≤1000 rows/page), waiting for it
under a 20-second timeout. Costs 2 AI credits per call. Returned
rows count against the organization's export volume (nothing is
counted when the query errors). Errors are surfaced via the
error field.
|
start_sql_query |
side-effecting |
Submit the same SELECT to run in the background under a
300-second (5-minute) timeout
(MCP_SQL_ASYNC_TIMEOUT_S), returning a
job_id without waiting for it. Use for queries
run_sql cannot finish in 20 seconds. The statement is
compiled first, so guard rejections and compilation errors return an
error and create no job. Costs 2 AI credits to submit;
rows are charged for by get_sql_query_result.
|
get_sql_query_status |
read-only |
Report whether a background query is working,
completed, failed, or
cancelled, with a
poll_interval_seconds hint and a message distinguishing
"queued for warehouse capacity" from "scanning". A failure carries
Snowflake's own error message; elapsed_seconds freezes
once the query stops. Reads query metadata only — free and safe to
poll in a loop.
|
get_sql_query_result |
side-effecting |
Return the rows of a completed background query, with
columns naming them in row order. Returned rows count
against the organization's export volume; re-reading the same job is
free, since the rows are counted once. Returns an
error rather than rows for a job that has not
completed, and sets pagination_warning when further
pages would not be a stable partition.
|
cancel_sql_query |
side-effecting |
Stop a running background query so it stops consuming warehouse
time, reporting cancelled only when Snowflake confirms
it stopped. A query Snowflake has not started executing yet cannot
be cancelled: the job stays working and the
error asks you to call again once it is running.
Idempotent — cancelling a finished job changes nothing. Free.
|
Utility
| Tool | Annotation | Description |
|---|---|---|
status_tool |
read-only | Health check — confirms the server is reachable and that the credentials resolve to an Inven user, returning that username and organisation. Free. |
get_credit_balance |
read-only |
Return current contact_credits and
ai_credits balances for the authenticated user.
ai_credits pays for tool calls and search
interpretation; it is not specific to AI enrichment columns, which
are read already-computed and cost nothing extra.
|
open_search_in_inven |
side-effecting | Open a previously built search as a tab in the user's Inven web app. No credits are charged. |
Data handling
What the server reads. Only the arguments passed to its tools. It does not read your conversation, chat history, memory, uploaded files, or anything else in the client's context — there is no tool here that asks for any of them, and the server never asks the client for them.
What those arguments are used for. A natural-language
description, data_description or
sql is what the request is made of, so it goes to the Inven
search pipeline and, for the tools that interpret language, to the language
model that turns it into filters. Contact lookups send the domains, names or
LinkedIn URLs you asked about to Inven's contact data providers. Nothing
else is forwarded.
What is logged. One line per tool call: who called it, which tool, and enough of the arguments to identify the call — caller-supplied text is cut to its first 200 characters. Error reports carry the username and the failure, never the request body or the tool arguments.
What is retained. Built searches and column selections are
held for 24 hours so a search_id stays runnable; the
language-model interpretation of a description is cached for 30 minutes so a
preview and the build that follows it agree. A background SQL job keeps its
statement for 24 hours, matching how long Snowflake keeps the results it
refers to. Tables you load with upload_to_sql live in a
Snowflake session that expires within four hours. Usage records — for
credits and billing — hold the caller, the tool name and token counts, not
the text of the query. A saved search also appears in your Inven search
history, which is the same history the web app writes.
Privacy policy
See our privacy policy at https://www.inven.ai/privacy-policy.
Support
- Email: info@inven.ai
- Documentation: https://inven.ai/mcp-docs
- Issues / feedback: https://help.inven.ai/
