Inven MCP Server
Description
The Inven MCP Server gives AI assistants (Claude, Cursor, ChatGPT, and others) direct access to Inven's database of over 20 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 spend export credits 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, 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.
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 credits are spent 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").- 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. A request that does not fit is not rejected: 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. Only a call that cannot fit even one contact per domain is rejected — split that one 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,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.run_sql(sql, limit, offset)runs the full paginated query (1 screening credit per call + 1 export credit per returned row, ≤1000 rows/page, 60-second timeout).- If the query is too heavy to finish in 60 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 no screening credits. 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 companies, so its cap counts those rather than people.
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 screening credits in proportion to the LLM tokens used. |
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 screening credits in proportion to the LLM tokens used. |
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 1 screening credit 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 1 screening credit per call plus 1 export credit per row returned. |
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 screening credits in proportion to the LLM tokens used, plus 1 export credit per row returned. |
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 screening credits in proportion to the LLM tokens used. |
run_deal_search | side-effecting | Execute a deal search and return paginated deal rows. Costs 1 screening credit per call plus 1 export credit per row returned. |
get_deal_info | side-effecting | Retrieve structured data for specific deal IDs. Costs screening credits in proportion to the LLM tokens used, plus 1 export credit per row returned. |
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 screening credits in proportion to the LLM tokens used. |
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 1 screening credit per call plus 1 export credit per row returned. |
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 1 screening credit 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 1 screening credit 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 1 screening credit per call plus 1 export credit per row returned. |
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 request that does not fit has max_contacts_per_domain reduced to what does and the change reported in the response note, and only a call that cannot fit one contact per domain is rejected. 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). |
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, 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.
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) → 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 1 screening credit and no export credits — rows are charged for when a query returns them, so the same rows are not paid for twice.
Queries that need longer than a minute
run_sql waits for the statement inside the tool call, which caps it at a 60-second Snowflake timeout: a wide scan or an aggregate over a whole view is killed mid-flight, and you wait the full minute 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 60 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, 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 | side-effecting | 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. The table lives in a session Snowflake caps at four hours. Costs 1 screening credit per call and no export credits — rows are charged for 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. Free; 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 60-second timeout. Costs 1 screening credit per call plus 1 export credit per returned row (no export credits charged on error). 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 60 seconds. The statement is compiled first, so guard rejections and compilation errors return an error and create no job. Costs 1 screening credit 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. Costs 1 export credit per row; re-reading the same job is free, since the rows are charged for 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 export_credits, contact_credits, and ai_enrichment_credits balances for the authenticated user. ai_enrichment_credits is the balance the other tools charge as "screening credits" — one pool, two names. |
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/
