{"schema_version": "2025-06-18", "name": "steadybase", "version": "1.2.0", "description": "SteadyBase: the public-company intelligence API for AI agents. For every US public filer it holds the company, its subsidiaries and ultimate parent, its executives, its filings and earnings calls, and what it says about itself on the open web -- every answer carries a verbatim quote and a receipt link that opens. Start with resolve_entity (turn a name, domain or ticker into the one canonical entity), get_account (the full account card for it) and whats_new (what changed recently).", "endpoint": "https://mcp.steadybase.io/mcp", "transport": "streamable-http (stateless)", "protocolVersion": "2025-06-18", "authentication": {"type": "http", "scheme": "bearer", "token_prefix": "gtms_", "how_to_get_one": "https://steadybase.io/docs/get-a-key/ or andrew@steadybase.io"}, "documentation": "https://andrewcmcguire.com/gtmsignals/mcp/", "status": "https://andrewcmcguire.com/gtmsignals/status/", "tool_count": 55, "tools": [{"name": "resolve_entity", "description": "Resolve any alias (ticker, company name, domain, or an entity_id) to the ONE canonical entity: entity_id, ticker, name, aliases, domains.", "inputSchema": {"type": "object", "properties": {"query": {"type": "string", "description": "ticker, name, domain, or entity_id"}}, "required": ["query"]}}, {"name": "get_account", "description": "THE ACCOUNT CARD for one covered account: the receipted statements with their clips (account.signals, each with quote, source_url, receipt, transcript_url, page_ref, event_date, speaker and verified), the buying committee under `contacts` when the feed carries it (get_contacts returns the full list), the travel spend estimate with its drivers, green and red flags, the next earnings date and the rank. Echoes the canonical entity_id and ticker. WHAT IS QUOTABLE: only account.signals[].quote, and only with that row's receipt. The card's headline `quote` is a feed convenience: quote_receipt carries its receipt when one of the receipted statements matches it and quote_is_quotable says so, and when it does not, quote_receipt is null, quote_is_quotable is false and quote_receipt_note says to quote from signals[] instead. `why_it_matters` is a DERIVED sentence (why_it_matters_is_derived is always true): it is why the account ranks, not words the company said, and must never be put in quotation marks to a customer. There is no warm intro path on this card; the committee, where the feed carries it, is the nearest thing to one, and it is identity only.", "inputSchema": {"type": "object", "properties": {"entity": {"type": "string", "description": "ticker, name, domain, or entity_id"}}, "required": ["entity"]}}, {"name": "get_signals", "description": "Verified earnings-call quotes/signals for one account, each with date, source, and a verified flag. since=YYYY-MM-DD keeps only signals on or after that date. WHAT IS QUOTABLE: each row's `quote`, verbatim, with that row's own receipt (source_url, receipt, transcript_url, or the clip). `why_it_matters` on the same row is a DERIVED sentence (why_it_matters_is_derived is always true): it says why the statement matters and is not part of the quotation, so never put it inside the quotation marks. receipt_note says the same in words.", "inputSchema": {"type": "object", "properties": {"entity": {"type": "string"}, "since": {"type": "string", "description": "optional YYYY-MM-DD floor"}}, "required": ["entity"]}}, {"name": "whats_changed", "description": "The recent-signals feed scoped to ONE account: its board row with rank, score, evidence tier, signal count, next earnings date, the headline quote and why_it_matters. WHAT IS QUOTABLE: the headline `quote` only when quote_is_quotable is true, and then with quote_receipt, which is the receipt of the one receipted statement it was cut from. When quote_is_quotable is false, quote_receipt is null, nothing on the row is quotable, and quote_receipt_note says so in words. `why_it_matters` is a DERIVED sentence (why_it_matters_is_derived is always true), not words the company said. For the verbatim statements with their receipts call get_signals(ticker), named on the row as verbatim_statement_from.", "inputSchema": {"type": "object", "properties": {"entity": {"type": "string"}}, "required": ["entity"]}}, {"name": "whats_new", "description": "WHAT IS NEW SINCE I LAST CHECKED. Call it with no arguments for the classic board feed (top 50 board rows). On those rows WHAT IS QUOTABLE is the headline `quote` only when quote_is_quotable is true, with quote_receipt; when it is false, quote_receipt is null and quote_receipt_note says nothing on the row is quotable. `why_it_matters` on a board row is a DERIVED sentence (why_it_matters_is_derived is always true); the verbatim statements with their receipts come from get_signals(ticker), named on the row as verbatim_statement_from. Call it with `since` and it becomes the delta feed: every signal FIRST SEEN since that point, newest first, each row carrying event_key, ticker, company, kind, matched_rule, the verbatim quote, why_it_matters, source_url and page_ref, the receipt link, the signal date, evidence tier, and call_first (the one person to call, identity only, no email or phone). Poll it on a schedule: pass the previous response's `next_since` straight back as `since` and you get exactly what landed in between, with no duplicates and no gaps. When `truncated` is true the page is the OLDEST slice of the backlog and `next_since` is the newest event in it, so polling again immediately walks forward through the backlog; `next_action` says in words which of the two to do. `limit` is a soft cap on that path: a page is never cut in the middle of a group of events sharing one first_seen, because the cursor cannot resume inside one. `since` accepts an ISO timestamp (exclusive, so a cursor never replays its last row), a plain date YYYY-MM-DD (inclusive), or a number of days (7). event_key is the same identity the Slack and webhook push lanes use, so a pushed alert and a pulled row for one signal match by string equality. The feed carries direct-evidence signals and T&E spend-change matches (the grade a human gets told about); use search_signals for the wider inferential corpus. Retention is 30 days. THE LIVE FEED: everything above is the nightly board, so the freshest thing it can say is \"last night\". Pass feed=\"events\" and the same tool answers from the box's live event log instead: WHAT HAS HAPPENED SINCE MY LAST CURSOR, oldest first, so a polling agent walks forward through it. Each row is event_seq, kind, event_key, occurred_at and payload. kind is one of trigger_hit (a trigger matched something written to your book), live_alert (a trigger matched what was said DURING a call, while the call is still running), clip_ready (the audio for an earlier live_alert has been cut, and the row carries the same alert_id plus the finished clip_url) and account_changed (an account on your book moved). A live_alert payload carries the VERBATIM QUOTE, the trigger it matched, the account, receipt_url and offset_sec, with clip_url null until the clip exists, so the quote always arrives with the receipt that opens it and you quote it as is or not at all. ON THIS FEED `since` IS AN EVENT_SEQ CURSOR, a string of digits, not a timestamp and not a number of days: pass the previous answer's next_cursor straight back and you get exactly what landed after it, because the cursor is EXCLUSIVE, so nothing is replayed and nothing is skipped. Start with no `since` at all, keep next_cursor from every reply, and poll. next_action says in words whether to poll again at once (truncated, there is a backlog) or on your schedule. `kinds` on this feed is that list of four and an unknown one is refused rather than ignored. The envelope stamps feed (\"events\" here, \"board\" on every other path) so you can tell the two apart on one key. The tenant is the one your key resolved to and no argument can name another.", "inputSchema": {"type": "object", "properties": {"feed": {"type": "string", "description": "\"board\" (default) is the nightly board and its delta feed, exactly as it has always answered. \"events\" is the box's LIVE event log since an event_seq cursor, oldest first, for a polling agent. It is named and never guessed, because `since` is a number of days on the board feed and an event_seq on the events feed and a bare number cannot be told apart."}, "since": {"type": "string", "description": "On the board feed: an ISO timestamp (use the previous response's next_since), a date YYYY-MM-DD, or a number of days like \"7\"; omit for the classic board feed. On feed=\"events\": an event_seq cursor, a string of digits taken from the previous response's next_cursor, EXCLUSIVE; omit to start at the oldest retained event. A timestamp is refused there rather than guessed at."}, "limit": {"type": "integer", "description": "max rows to return (default 50, hard cap 200). On the board `since` path this is a SOFT cap: a page is never cut inside a group of events sharing one first_seen, because the cursor is a timestamp and could not resume inside one, so such a group comes back whole and a page can exceed limit. On feed=\"events\" the cursor is an event_seq, which can resume anywhere, so the cap is hard."}, "kinds": {"type": "array", "items": {"type": "string"}, "description": "optional filter on kind. On the board feed: \"direct\" (the company said it) or \"te_spend\" (a travel and expense spend-change match). On feed=\"events\": \"trigger_hit\", \"live_alert\", \"clip_ready\" or \"account_changed\"; an unknown one there is refused, never dropped."}}}}, {"name": "upcoming_calls", "description": "Upcoming earnings calls with listen_for and your_move, stamped with the canonical entity_id. For the full capture calendar with confirmed times, webcast links and capture status, use upcoming_calendar.", "inputSchema": {"type": "object", "properties": {}}}, {"name": "upcoming_calendar", "description": "WHEN THE CALLS ARE. The capture calendar for this board: earnings calls and investor presentations with the date, the confirmed start time when the company's IR page published one, the webcast link, and our own capture status. Use it to answer \"who reports this week\" and to time outreach around a call. Status moves scheduled -> registered -> capturing -> captured -> transcribed -> extracted, or ends at missed (the event passed with no audio) or failed (we tried and got nothing); status_reason says why in a phrase. Two numbers in `summary` decide how much of this is actionable: with_webcast_url (no public link means the call cannot be captured at all) and with_confirmed_time (without it the start is a before-open / after-close guess, not a real clock time). Defaults to the next 30 days plus the last 7 so \"what did we capture this week\" is answerable in the same call.", "inputSchema": {"type": "object", "properties": {"days": {"type": "integer", "description": "how far forward, 1 to 180 (default 30)"}, "ticker": {"type": "string", "description": "optional: one ticker"}, "status": {"type": "string", "description": "optional: scheduled, registered, capturing, captured, transcribed, extracted, missed, or failed"}, "event_type": {"type": "string", "description": "optional: earnings_call, presentation, conference, investor_day, shareholder_meeting"}, "include_past_days": {"type": "integer", "description": "how far back to include (default 7, max 90)"}, "limit": {"type": "integer", "description": "max events (default 200, cap 1000)"}}}}, {"name": "next_earnings_call", "description": "THE NEXT CALL FOR ONE COMPANY. Give it a ticker (a company name, domain or entity_id also resolves) and it returns the next scheduled earnings call on this board: date, days_until, the confirmed time in US Eastern when one is published, time_source (ir_page means a real published time; nasdaq_bmo / nasdaq_amc mean before-open / after-close and are a guess), the webcast_url to listen on, the IR page, whether it is capturable, and our capture status. Also returns the two calls after it and any other investor events for that company. Pair it with whats_new: a fresh signal plus a call inside the week is the moment to reach out.", "inputSchema": {"type": "object", "properties": {"ticker": {"type": "string", "description": "ticker, company name, domain, or entity_id of a board account"}}, "required": ["ticker"]}}, {"name": "get_actions", "description": "The System of Action queue: ranked do-next moves for the top of your board, each with its canonical entity_id, a sentence, the headline quote, and the artifact that opens it when there is one. `sentence` is the row's why_it_matters, a DERIVED sentence (why_it_matters_is_derived is always true): it is why the account ranks, never words the company said, and must not be put in quotation marks. WHAT IS QUOTABLE: `quote` only when quote_is_quotable is true; then quote_receipt is the receipt of the one receipted statement it was cut from and artifact_url_or_text is the link that opens it. When quote_is_quotable is false, artifact_url_or_text and quote_receipt are null and quote_receipt_note says nothing on the row is quotable. The verbatim statements with their receipts come from get_signals(ticker), named on the row as verbatim_statement_from. Pass entity to scope to one account.", "inputSchema": {"type": "object", "properties": {"entity": {"type": "string", "description": "optional; empty for the whole book"}}}}, {"name": "get_contacts", "description": "The buying-committee contacts for one account on your board: identity only (name, title, persona, LinkedIn). Never email or phone. Pass a ticker.", "inputSchema": {"type": "object", "properties": {"ticker": {"type": "string", "description": "ticker, name, domain, or entity_id of a board account"}}, "required": ["ticker"]}}, {"name": "get_priorities", "description": "THE TIERED PRIORITY LIST (P1 act now, P2 direct evidence smaller prize, P3 big prize watch the trigger, P4 monitor). Each account carries its rank score decomposition (signal strength x qualification x spend tier), its green flags, its red flags (which demote and never disqualify), the business line, the motion, the travel spend estimate and top_rule, the rule that fired. NOTHING ON THESE ROWS IS QUOTABLE: why_it_matters and business are derived sentences describing why the row ranks, why_it_matters_is_derived is always true, quote and source_url are always null, and receipt_note says the same in words. For the verbatim statement behind top_rule and the receipt that opens it, call get_signals(ticker) or get_account(ticker) and quote from signals[]. Filter by tier, cap with limit.", "inputSchema": {"type": "object", "properties": {"tier": {"type": "string", "description": "optional: P1, P2, P3, or P4"}, "limit": {"type": "integer", "description": "max accounts to return (default 25, cap 200)"}}}}, {"name": "research_company", "description": "Live universe pull for ANY public company, even one NOT on the book: latest earnings-call transcript excerpt, labeled not-on-book. For a covered account use get_account instead.", "inputSchema": {"type": "object", "properties": {"name": {"type": "string", "description": "company name or ticker"}}, "required": ["name"]}}, {"name": "search_content", "description": "Search the GTM content library (battlecards, decks, one-pagers, the vault) and return the top matching docs with name, url, and tags.", "inputSchema": {"type": "object", "properties": {"query": {"type": "string"}}, "required": ["query"]}}, {"name": "today", "description": "THE LIVE CONSOLE (steadybase.io/today/). Today's calls with arm state (scheduled, armed, capturing, captured, or missed with a reason), the last 24 hours of company-level signals, today's landed filings/decks/transcripts with counts and receipts, the platform heartbeat strip, and the local director's newest operating note. Same JSON as https://steadybase.io/today/index.json, refreshed every 15 minutes. Public and NOT board-scoped: carries no tenant name, trigger text, contact, or email, whichever key calls it.", "inputSchema": {"type": "object", "properties": {}}}, {"name": "search_signals", "description": "Full-text search across every signal receipt on your board - verbatim quotes from earnings calls, deck slides (page-linked), filings, and job postings. Each hit returns the quote and its source link (audio clip with timestamp when captured).", "inputSchema": {"type": "object", "properties": {"query": {"type": "string", "description": "what to search for"}, "ticker": {"type": "string", "description": "optional: restrict to one account's signals"}, "limit": {"type": "integer", "description": "max hits (default 10, cap 50)"}}, "required": ["query"]}}, {"name": "find_companies", "description": "FIND, RESOLVE OR LOAD COMPANIES. Four modes on one tool, by which argument you give, in this precedence: resolve, then bulk, then q, then query. (1) resolve is THE IDENTITY RESOLVER and wins over q and query. Give it an object of identifiers, any combination of account_id, cik, lei, ticker, exchange_ticker, domain, linkedin_company, legal_name (with country or state to disambiguate), parent_hint and duns, and you get back ONE account_id with method (which rung matched), confidence, corroborated_by, the account row, the corporate tree (parent, ultimate_parent, is_ultimate_parent) and the evidence list of every rung that was tried. A miss is an EXPLICIT no match: resolved false, reason no_match or ambiguous or conflict, and the nearest candidates so you can see what it nearly was. TWO CANDIDATES IS AMBIGUITY AND NEVER A MATCH: this tool does not promote a candidate into an answer, and you must not either. Send a stronger identifier or a jurisdiction and ask again. (2) external_id with external_system resolves YOUR OWN id (a Salesforce 18 character id, a CRM account key) through the same route, answered from your tenant's own id map first so a mapped id does not get re-matched. Both arguments go together, and the map read is scoped to the tenant your key resolves to; a key that is not mapped to a tenant is told so and the box is never called. (3) bulk true is THE ACCOUNT UNIVERSE, paged: identity only, no signal, no score, no tier, no tenant lens, in account_id order. Each row carries account_id, legal_name, display_name, primary_ticker, primary_domain, account_type, lifecycle_status, resolution_status, is_company, parent_account_id, ultimate_parent_id, depth, hq, sector, industry, exchange, employees_est, ir_url, identifiers (cik, lei, ticker, exchange_ticker, domain, linkedin_company, former_name, duns) and updated_at. Page on next_cursor until has_more is false; take deltas by passing since = the previous answer's as_of. include_unresolved adds the 209,093 EX-21 exhibit nodes we hold as evidence and have not identified (they come back resolution_status unresolved and is_company false, and they are NOT companies we assert exist under that name); the default scope without it is 10,140 resolved rows out of 219,233 total. book_only narrows to your own book. YOU DEDUPE ON account_id AND NOTHING ELSE. Ticker collisions (two companies that have held one symbol at different times), name variants, share classes and a subsidiary sharing its parent's domain are OURS to collapse and are already collapsed onto one account_id before a row leaves here. account_id is stable across a rename, a ticker change, an exchange move and an acquisition. Never dedupe on ticker or name. If two rows ever carry the same identifier under the same id_type, that is a bug to report to us, not a merge for you to perform. PAGE SIZE: the MCP page defaults to 200 rows and is capped at 500, because one tools/call result is one text block that lands whole in your context and 1,000 of these rows is about 200,000 tokens. The box itself will serve 5,000, so for the FIRST FULL LOAD of all 219,233 rows ask for the nightly export instead: one gzipped JSONL file per tenant in the private snapshots bucket, delivered by a presigned URL, built by the same function as this page so the two cannot drift. Use bulk for deltas and for reading a slice. (4) query is the PUBLIC DIRECTORY substring search over all 7,034 published companies by name, ticker or slug, case insensitive, exact ticker first, the same answer for every key: it returns slug (the durable key for the other company_* tools), name, ticker, url, page_url and the signal, quote and people counts. Use it when you have a company name and need the slug. (5) q is the IDENTIFIER LOOKUP behind your key: a domain, ticker, LEI or name, and each row is the company profile, account_id, name, legal_name, ticker, domain, sector, industry, exchange, cik, lei, hq_city, hq_state, hq_country, ir_url, linkedin_url, employees_est, on_book (whether that account sits on YOUR book), matched_on (which identifier hit) and identifiers (the rest of them). THE RECEIPT on a q row is cik and lei, the registry identifiers you can check the company against, plus ir_url and linkedin_url. include_tree true adds the corporate tree stats: parent, ultimate_parent, children_count, subsidiaries_count, and the people_count, roles_count, signals_count, documents_count and transcripts_count held. LIMITS: limit is max rows, default 10 and cap 100 on q and query, default 200 and cap 500 on bulk. The tenant on every keyed path is the one your key resolved to and cannot be overridden by an argument. bulk cannot be combined with resolve, q or query and says so rather than answering a narrower question. IT NEVER RETURNS an email, a phone, a tier, a score, a rank or any prioritisation of any kind.", "inputSchema": {"type": "object", "properties": {"query": {"type": "string", "description": "company name, ticker, or slug; case insensitive substring over the public directory"}, "q": {"type": "string", "description": "a domain, ticker, LEI or name to look up behind your key; answered with the company profile"}, "include_tree": {"type": "boolean", "description": "with q: add the corporate tree stats (parent, ultimate parent, child and subsidiary counts, and the people, roles, signals, documents and transcripts held)"}, "resolve": {"type": "object", "description": "THE RESOLVER, and it wins over q and query: an object of identifiers resolved to ONE account_id or to an explicit no match with candidates. Accepts account_id, cik, lei, ticker, exchange_ticker, domain, linkedin_company, legal_name, country, state, parent_hint, duns. Two candidates is ambiguity and never a match", "properties": {"account_id": {"type": "string"}, "cik": {"type": "string", "description": "SEC central index key"}, "lei": {"type": "string", "description": "20 character legal entity identifier"}, "ticker": {"type": "string"}, "exchange_ticker": {"type": "string", "description": "exchange qualified, for example NYSE:CRM"}, "domain": {"type": "string"}, "linkedin_company": {"type": "string", "description": "the linkedin company slug"}, "legal_name": {"type": "string", "description": "weakest rung; send country or state with it"}, "country": {"type": "string", "description": "jurisdiction for legal_name"}, "state": {"type": "string", "description": "jurisdiction for legal_name"}, "parent_hint": {"type": "string", "description": "the parent this entity sits under, to break a name tie"}, "duns": {"type": "string"}}}, "external_id": {"type": "string", "description": "YOUR OWN id for this company (a Salesforce id, a CRM key). Goes with external_system and is answered from your tenant's own id map first"}, "external_system": {"type": "string", "description": "the system external_id belongs to, for example salesforce. Required with external_id"}, "bulk": {"type": "boolean", "description": "true loads THE ACCOUNT UNIVERSE, paged: identity only, in account_id order, with next_cursor and has_more. Cannot be combined with resolve, q or query"}, "cursor": {"type": "string", "description": "with bulk: the account_id of the last row you read. Keyset paged, so a deep page costs the same as the first"}, "since": {"type": "string", "description": "with bulk: ISO date or timestamp, an inclusive floor on updated_at. For a delta pass the previous answer's as_of"}, "include_unresolved": {"type": "boolean", "description": "with bulk: add the 209,093 EX-21 exhibit nodes we hold as evidence and have not identified. They are evidence, not companies we assert exist"}, "book_only": {"type": "boolean", "description": "with bulk: only the accounts on your own book"}, "limit": {"type": "integer", "description": "max rows. q and query: default 10, cap 100. bulk: default 200, cap 500, because one tools/call result lands whole in your context"}}}}, {"name": "get_company", "description": "THE COMPANY PAGE, AS DATA. One company from the public directory: identity (canonical ticker, aliases, domain, sector, industry, size band, IR and LinkedIn URLs), counts, the calendar (next event and recent events with webcast links), recent hiring, leadership_changes from 8-K filings, why_now and family when the export publishes them, and the newest 25 signals each with a verbatim quote and a receipt. Accepts a slug or a canonical ticker; a share-class ticker such as BAC^N resolves to its canonical company. people are OFF by default: pass include_people=true and you get name, title, role, linkedin_url, aliases and their sourced quotes, and nothing else. Never returns an email or a phone number, and carries no prioritisation of any kind.", "inputSchema": {"type": "object", "properties": {"slug_or_ticker": {"type": "string", "description": "directory slug (salesforce-inc) or canonical ticker (CRM)"}, "include_people": {"type": "boolean", "description": "include the published public figures (default false)"}}, "required": ["slug_or_ticker"]}}, {"name": "company_why_now", "description": "WHY THIS COMPANY, NOW. Returns the published why_now packages when the export carries them, with derived_from set to \"why_now\". When no package is published it does NOT invent one: it returns the 5 most recent public statements the company made, each with a verbatim quote and an https source_url, and marks the response derived_from=\"signals\" so a caller can always tell an authored why-now from raw evidence. Read that marker before you quote this anywhere.", "inputSchema": {"type": "object", "properties": {"slug_or_ticker": {"type": "string", "description": "directory slug or canonical ticker"}}, "required": ["slug_or_ticker"]}}, {"name": "company_signals", "description": "THE RECEIPTS. Every public signal on one company, newest first, each with the verbatim quote, an https source_url, page_ref, event_date and lane. Filter with lane (earnings_call, filing, ir_statement, exec_social, hiring, engineering) and with since (YYYY-MM-DD, inclusive floor on event_date). lanes_available tells you which lanes this company actually has. Duplicate statements are collapsed, keeping the entry with the best receipt. Use it after get_company when 25 signals is not enough or you want one lane only.", "inputSchema": {"type": "object", "properties": {"slug_or_ticker": {"type": "string", "description": "directory slug or canonical ticker"}, "lane": {"type": "string", "description": "optional: earnings_call, filing, ir_statement, exec_social, hiring, engineering"}, "since": {"type": "string", "description": "optional YYYY-MM-DD inclusive floor on event_date"}, "limit": {"type": "integer", "description": "max signals (default 50, cap 200)"}}, "required": ["slug_or_ticker"]}}, {"name": "search_company_signals", "description": "SEARCH EVERY COMPANY AT ONCE. Full-text search across the public signal index: every verified signal plus every signal on a lane other than earnings_call and earnings_calendar, across all companies in the public directory, each with a verbatim summary, an https source_url, page_ref, event_date, lane and the company page URL. This is the tool for \"which companies said anything about a hiring freeze\", where find_companies and company_signals both need you to know the company first. Matching is exact lowercased token match, no stemming and no synonyms, so use literal words. Ranked by how many of your query terms a signal carries, then by recency. Filter with lane (call list_lanes for what exists), since (YYYY-MM-DD inclusive floor on event_date) and ticker_or_slug (one company). Not board scoped: the same answer for every key, because the index it reads is public by contract.", "inputSchema": {"type": "object", "properties": {"query": {"type": "string", "description": "the words to search for, for example \"restructuring\" or \"travel expense\""}, "lane": {"type": "string", "description": "optional: one lane, for example cost, expansion, leadership_change, sec_filing, efficiency, headcount. list_lanes returns them all with counts"}, "since": {"type": "string", "description": "optional YYYY-MM-DD inclusive floor on event_date"}, "ticker_or_slug": {"type": "string", "description": "optional: restrict to one company by directory slug or ticker"}, "limit": {"type": "integer", "description": "max hits (default 20, cap 100)"}}, "required": ["query"]}}, {"name": "list_lanes", "description": "THE LANES, WITH COUNTS. Every signal lane in the public cross-company index and how many signals each holds, plus the total rows, the number of companies covered, the date window, and when the index was generated. Call it before search_company_signals when you want to know what a lane filter can legally be, or to tell a caller how fresh the corpus is.", "inputSchema": {"type": "object", "properties": {}}}, {"name": "get_receipt", "description": "RESOLVE A RECEIPT. Every earnings-call quote in this MCP and in the public company directory carries a source_url. When that URL has the form https://api.steadybase.io/v1/receipt/<transcript_id>?start=<a>&end=<b> the transcript is licensed text that is not public, and this tool is how a key holder reads it: pass the url (or transcript_id plus start and end) and get back exactly the quoted span from the stored transcript, with the character offsets it was cut at and the transcript's size. Omit start and end to read the whole transcript (capped at 400k characters). Use it to verify a quote verbatim before repeating it, or to read the paragraph around it (widen start and end). Receipts on our own captures are plain public S3 URLs and do not need this tool.", "inputSchema": {"type": "object", "properties": {"url": {"type": "string", "description": "the receipt URL exactly as the signal carries it"}, "transcript_id": {"type": "string", "description": "alternative to url: the transcript uuid"}, "start": {"type": "integer", "description": "character offset of the first character (with transcript_id)"}, "end": {"type": "integer", "description": "character offset one past the last character (with transcript_id)"}}}}, {"name": "get_stated_te", "description": "Best stated T&E/travel spend for a public ticker (amount, quote, source URL). Never invents. Jets out of scope.", "inputSchema": {"type": "object", "properties": {"ticker": {"type": "string"}}, "required": ["ticker"]}}, {"name": "list_stated_te", "description": "List stated T&E calibration rows. Sparse receipts; full product is the active public book.", "inputSchema": {"type": "object", "properties": {"qa_status": {"type": "string", "default": "PASS"}, "require_amount": {"type": "boolean", "default": true}, "limit": {"type": "integer", "default": 100}}}}, {"name": "get_navan_te_book_row", "description": "One row from the active public-company Navan T&E book.", "inputSchema": {"type": "object", "properties": {"ticker": {"type": "string"}}, "required": ["ticker"]}}, {"name": "list_navan_te_book", "description": "Page the active public-company Navan T&E book.", "inputSchema": {"type": "object", "properties": {"limit": {"type": "integer", "default": 50}, "has_stated_only": {"type": "boolean", "default": false}, "offset": {"type": "integer", "default": 0}}}}, {"name": "get_gbv_estimate", "description": "Annual managed air+hotel GBV range (jets excluded) for a public ticker. public_company v0, or pending if that ticker has no fill.", "inputSchema": {"type": "object", "properties": {"ticker": {"type": "string"}, "model_id": {"type": "string"}}, "required": ["ticker"]}}, {"name": "get_gbv_priors", "description": "Versioned GBV priors by stratum when published.", "inputSchema": {"type": "object", "properties": {"stratum": {"type": "string"}}}}, {"name": "gbv_feed_stats", "description": "Stated T&E and public book feed counts loaded by MCP.", "inputSchema": {"type": "object", "properties": {}}}, {"name": "sf_why_now", "description": "YOUR WHY-NOW LIST FROM SNOWFLAKE. The Why Now catalog ranked through your tenant's prefs, rules and book, read as your tenant's read-only role from GTM_SIGNALS.TERRITORY.V_TENANT_WHY_NOW: TENANT_RANK, TICKER, COMPANY, SIGNAL_TYPE, AS_OF, TENANT_SCORE, WHY_PLAIN and the receipt (RECEIPT_URL, RECEIPT_KIND, SIGNAL_KEY). Same rows a direct Snowflake connection with role GTM_SIGNALS_RO_<TENANT> returns.", "inputSchema": {"type": "object", "properties": {"limit": {"type": "integer", "description": "max rows (default 25, cap 500)"}}}}, {"name": "sf_account_signals", "description": "VERIFIED SIGNALS ON ONE ACCOUNT FROM SNOWFLAKE. Every verified shared signal on one account in your book, weighted by your lens, from V_TENANT_ACCOUNT_SIGNALS: SIGNAL_TYPE, EVENT_DATE, SUMMARY (verbatim), DIRECTION, TENANT_SIGNAL_SCORE and the receipt (SOURCE_URL, PAGE_REF, VERIFIED). Give a ticker, or an account_id (acc_...) which is resolved inside your own views.", "inputSchema": {"type": "object", "properties": {"ticker": {"type": "string", "description": "the account's ticker"}, "account_id": {"type": "string", "description": "alternative to ticker: the acc_<32 hex> id"}, "since_days": {"type": "integer", "description": "floor on EVENT_DATE in days back (default 90)"}, "limit": {"type": "integer", "description": "max rows (default 50, cap 500)"}}}}, {"name": "sf_future_events", "description": "UPCOMING CALLS ON YOUR BOOK FROM SNOWFLAKE. One row per upcoming event on your book accounts from V_TENANT_FUTURE_EVENTS: REPORT_DATE, CALL_TIME_ET, TIME_SOURCE, WEBCAST_URL, IR_PAGE_URL, REGISTERED, CAPTURE_PLAN (armed, deferred, no_time, no_url) and the calendar source. capture_plan filters to one plan; capture_plan_counts summarises the page.", "inputSchema": {"type": "object", "properties": {"days": {"type": "integer", "description": "how far forward from today, 1 to 365 (default 30)"}, "capture_plan": {"type": "string", "description": "optional: armed, deferred, no_time or no_url"}, "limit": {"type": "integer", "description": "max rows (default 200, cap 500)"}}}}, {"name": "sf_people", "description": "PEOPLE AT A BOOK ACCOUNT FROM SNOWFLAKE. Identity only (FULL_NAME, TITLE, LINKEDIN_URL, persona match, time in role) from V_TENANT_PEOPLE, never an email or a phone. Give a ticker or account_id, and optionally a persona (a persona key such as cfo, controller, treasury, procurement, or a fragment of its label) to filter; persona alone lists that persona across your book.", "inputSchema": {"type": "object", "properties": {"ticker": {"type": "string", "description": "the account's ticker"}, "account_id": {"type": "string", "description": "alternative to ticker: the acc_<32 hex> id"}, "persona": {"type": "string", "description": "optional persona key or label fragment"}, "limit": {"type": "integer", "description": "max rows (default 100, cap 500)"}}}}, {"name": "sf_search_transcripts", "description": "SEARCH YOUR BOOK'S CALL TRANSCRIPTS. Hybrid (vector plus keyword) search over about 120 word windows of every earnings call transcript on your book accounts. Answered by the box search service (Postgres plus local embeddings, same as search_transcripts_local; envelope mode local_*) when it is configured first, otherwise by your tenant's own Snowflake Cortex Search service (mode cortex_search). Each hit is verbatim TEXT with its receipt: TRANSCRIPT_URL plus CHAR_START/CHAR_END (offsets into the transcript), START_SEC when the call is timed, TRANSCRIPT_ID, EVENT_ID, TICKER, CALL_DATE. Quote TEXT as is or not at all. Filter with ticker and since_days.", "inputSchema": {"type": "object", "properties": {"query": {"type": "string", "description": "what to search for, in words"}, "ticker": {"type": "string", "description": "optional: one ticker"}, "since_days": {"type": "integer", "description": "floor on CALL_DATE in days back (default 365)"}, "limit": {"type": "integer", "description": "max hits (default 10, cap 50)"}}, "required": ["query"]}}, {"name": "sf_ask", "description": "ASK THE SEMANTIC VIEW A QUESTION. Runs a natural language question through Snowflake Cortex Analyst over GTM_SIGNALS.SEMANTIC.GTM_SIGNALS_SV when the account exposes that view to your tenant role, and executes the generated SQL as your tenant role; the reply carries the interpretation, the SQL, the rows and their receipt columns. When the view is not exposed to your role it does not guess: it returns the view's tables, dimensions and metrics with a note, so you can put the question to the other sf_ tools.", "inputSchema": {"type": "object", "properties": {"question": {"type": "string", "description": "the question, in words"}}, "required": ["question"]}}, {"name": "search_transcripts_local", "description": "SEARCH YOUR BOOK'S CALL TRANSCRIPTS ON THE BOX (no Snowflake). Hybrid full text plus local embedding (nomic-embed-text-v1.5) search over the same 120 word windows as sf_search_transcripts, served from Postgres on AI-Powerhouse and scoped to your book there. Same hit fields and receipts (TRANSCRIPT_URL plus CHAR_START/CHAR_END, START_SEC when timed, TRANSCRIPT_ID, EVENT_ID, TICKER, CALL_DATE) plus scores {fts_rank, cosine, rrf}. Quote TEXT as is or not at all. mode: hybrid (default), keyword or vector.", "inputSchema": {"type": "object", "properties": {"query": {"type": "string", "description": "what to search for, in words"}, "ticker": {"type": "string", "description": "optional: one ticker"}, "since_days": {"type": "integer", "description": "floor on CALL_DATE in days back (default 365)"}, "limit": {"type": "integer", "description": "max hits (default 10, cap 50)"}, "mode": {"type": "string", "description": "hybrid (default), keyword or vector"}}, "required": ["query"]}}, {"name": "define_trigger", "description": "DEFINE WHAT YOU NEED TO HEAR ABOUT, IN YOUR OWN WORDS. Creates or updates (by name) one trigger for your tenant: criteria_text is plain language (\"consolidating expense or travel tools\"), scope is transcripts, signals, documents or all, filters is an optional JSON object (tickers, sectors, date_from, date_to, min_score, min_lexical, lookback_days). Written through your tenant's own owner's rights procedure SP_DEFINE_TRIGGER_<TENANT>, so it can only ever belong to your tenant. The hourly runner then searches the call transcripts on your book (Cortex Search, receipts kept) and the verified signal set by signal type, and writes hits you read with trigger_hits. Returns the TRIGGER_ID and the row.", "inputSchema": {"type": "object", "properties": {"name": {"type": "string", "description": "short label, unique per tenant (2 to 120 chars); the same name updates the trigger"}, "criteria_text": {"type": "string", "description": "what to trigger on, in words (3 to 2000 chars)"}, "scope": {"type": "string", "description": "transcripts | signals | documents | all (default all)"}, "filters": {"type": "object", "description": "optional: {\"tickers\": [...], \"sectors\": [...], \"date_from\": \"YYYY-MM-DD\", \"date_to\": \"YYYY-MM-DD\", \"min_score\": 0.4, \"min_lexical\": 0.34, \"lookback_days\": 90}"}, "active": {"type": "boolean", "description": "false pauses the hourly run without losing hits (default true)"}}, "required": ["name", "criteria_text"]}}, {"name": "list_triggers", "description": "YOUR TRIGGERS. Every trigger defined for your tenant from V_TENANT_TRIGGERS: TRIGGER_ID, NAME, CRITERIA_TEXT, SCOPE, FILTERS, the signal types the runner mapped the words to, ACTIVE, LAST_RUN_AT, LAST_HIT_AT and HIT_COUNT.", "inputSchema": {"type": "object", "properties": {"include_inactive": {"type": "boolean", "description": "also list paused triggers (default false)"}, "limit": {"type": "integer", "description": "max rows (default 100, cap 500)"}}}}, {"name": "trigger_hits", "description": "HITS WITH RECEIPTS. What your triggers matched, from V_TENANT_TRIGGER_HITS, newest first: TRIGGER_NAME, TICKER, SOURCE_KIND (transcript, signal, document), EVENT_DATE, SCORE, the verbatim QUOTE, CHAR_START/CHAR_END and RECEIPT_URL (the transcript's receipt URL, keyed for purchased text and readable with get_receipt, or the signal's source URL). Quote QUOTE as is or not at all. since floors MATCHED_AT (default 30 days back); trigger_id narrows to one trigger.", "inputSchema": {"type": "object", "properties": {"trigger_id": {"type": "string", "description": "optional: one TRIGGER_ID from list_triggers"}, "since": {"type": "string", "description": "optional: YYYY-MM-DD or ISO timestamp floor on MATCHED_AT (default 30 days back)"}, "source_kind": {"type": "string", "description": "optional: transcript, signal or document"}, "limit": {"type": "integer", "description": "max rows (default 50, cap 500)"}}}}, {"name": "run_trigger_now", "description": "PREVIEW A TRIGGER NOW. Runs the transcript part of one trigger synchronously as your tenant role over the call transcripts on your book (your tenant's Cortex Search service, or a keyword scan of the secure transcript view when that service is not serving), capped at 20 hits, nothing written. Each hit is verbatim TEXT with TRANSCRIPT_URL, CHAR_START/CHAR_END and a SCORE. The hourly runner persists the real hits; read those with trigger_hits.", "inputSchema": {"type": "object", "properties": {"trigger_id": {"type": "string", "description": "the TRIGGER_ID from define_trigger or list_triggers"}}, "required": ["trigger_id"]}}, {"name": "pg_why_now", "description": "YOUR WHY-NOW LIST FROM THE BOX (Postgres, no Snowflake). The Why Now catalog ranked through your tenant's prefs, rules and book, from gtm_signals.v_tenant_why_now on AI-Powerhouse: TENANT_RANK, TICKER, COMPANY, SIGNAL_TYPE, AS_OF, TENANT_SCORE, WHY_PLAIN and the receipt (RECEIPT_URL, RECEIPT_KIND, SIGNAL_KEY). Same rows and order as sf_why_now.", "inputSchema": {"type": "object", "properties": {"limit": {"type": "integer", "description": "max rows (default 25, cap 500)"}}}}, {"name": "pg_account_signals", "description": "VERIFIED SIGNALS ON YOUR BOOK FROM THE BOX (Postgres), one account or all of them. Every verified shared signal on your book accounts, weighted by your lens, from gtm_signals.v_tenant_account_signals: SIGNAL_TYPE, EVENT_DATE, SUMMARY (verbatim), DIRECTION, TENANT_SIGNAL_SCORE and the receipt (SOURCE_URL, PAGE_REF, VERIFIED). Give a ticker or an account_id (acc_...) for one account, or give NEITHER and it answers across the whole book, which is how you ask \"which companies ON MY BOOK announced a cost programme\", \"who changed an expense or travel vendor\", \"who is hiring into finance\". signal_type is the lane: cost, cost_cutting, efficiency, expansion, geo, headcount, hiring, vendor_change, budget_spend, initiative, guidance, leadership_change, sec_filing, pain_point, other. q narrows to signals whose verbatim SUMMARY contains every word you give (up to 8 words, no stemming), so signal_type=cost with q=travel is the T&E cost question. This is the BOOK door onto lane search; search_company_signals is the same idea over the anonymously public cross company directory, and by contract that surface cannot tell you whose book an account sits on. An empty answer says which filter emptied it and nothing is widened to fill it.", "inputSchema": {"type": "object", "properties": {"ticker": {"type": "string", "description": "optional: one account's ticker (omit for the whole book)"}, "account_id": {"type": "string", "description": "alternative to ticker: the acc_<32 hex> id"}, "since_days": {"type": "integer", "description": "floor on EVENT_DATE in days back (default 90)"}, "signal_type": {"type": "string", "description": "optional lane, exactly one of: cost, cost_cutting, efficiency, expansion, geo, headcount, hiring, vendor_change, budget_spend, initiative, guidance, leadership_change, sec_filing, pain_point, other"}, "q": {"type": "string", "description": "optional: words that must all appear in the verbatim SUMMARY, e.g. travel, per diem, corporate card"}, "limit": {"type": "integer", "description": "max rows (default 50, cap 500)"}}}}, {"name": "pg_deals", "description": "M&A ON YOUR BOOK, WITH THE FILING (Postgres). Every deal touching an account on your book from gtm_signals.v_deal_status, newest event first: DEAL_STATUS (announced, pending, closed, terminated), OTHER_PARTY_NAME, ANNOUNCED_DATE, EXPECTED_CLOSE, LAST_EVENT_TYPE and LAST_EVENT_DATE, PERCENT when the filing prints an ownership percent (no row carries one today, measured 0 of 604), the SOURCE_FORM and ITEM it was read from (8-K, 425, S-4, DEFM14A, 13D, tender offers), and two sentences quoted verbatim from the filing at RECEIPT_URL: RECEIPT_SENTENCE, the sentence the deal was read from, on every row, and CONSIDERATION_SENTENCE, what is being paid, when the filing states it (344 of 604 rows today). Ask it when you want to know which accounts are mid deal, because an acquisition is when a travel and expense programme gets consolidated onto one vendor. A deal with no receipt is NOT returned. Nothing is authored here: the status and the sentence are what the filing says, and OTHER_PARTY_MATCH tells you how the counterparty was resolved, so a name with no match is a name from a filing and not an account of ours. Filter with ticker or account_id, status, and since (YYYY-MM-DD floor on the last event).", "inputSchema": {"type": "object", "properties": {"ticker": {"type": "string", "description": "optional: one account's ticker (omit for the whole book)"}, "account_id": {"type": "string", "description": "alternative to ticker: the acc_<32 hex> id"}, "status": {"type": "string", "description": "optional: announced, pending, closed or terminated"}, "since": {"type": "string", "description": "optional YYYY-MM-DD floor on the last event date"}, "limit": {"type": "integer", "description": "max rows (default 100, cap 500)"}}}}, {"name": "pg_future_events", "description": "UPCOMING CALLS ON YOUR BOOK FROM THE BOX (Postgres). One row per upcoming event on your book accounts from gtm_signals.v_tenant_future_events (fin45.future_events_current, refreshed hourly): REPORT_DATE, CALL_TIME_ET, TIME_SOURCE, WEBCAST_URL, IR_PAGE_URL, REGISTERED, CAPTURE_PLAN (armed, deferred, no_time, no_url) and the calendar source; capture_plan_counts summarises the page; cursor is the newest change_id for pg_future_events_changes.", "inputSchema": {"type": "object", "properties": {"days": {"type": "integer", "description": "how far forward from today, 1 to 365 (default 30)"}, "capture_plan": {"type": "string", "description": "optional: armed, deferred, no_time or no_url"}, "limit": {"type": "integer", "description": "max rows (default 200, cap 500)"}}}}, {"name": "pg_future_events_changes", "description": "WHAT CHANGED ON YOUR BOOK'S UPCOMING CALLS SINCE A CURSOR (the replacement for the Snowflake FUTURE_EVENTS_STREAM). Rows of fin45.future_events_changes on your book accounts after change_id `cursor`: CHANGE_KIND (insert, update, delete), CHANGED_AT, TICKER, REPORT_DATE, CHANGED_COLUMNS, BEFORE_ROW, AFTER_ROW. Keep next_cursor and pass it back next time; nothing is consumed server side.", "inputSchema": {"type": "object", "properties": {"cursor": {"type": "integer", "description": "the change_id you last saw (default 0 = from the beginning)"}, "since": {"type": "string", "description": "optional YYYY-MM-DD or ISO timestamp floor on CHANGED_AT"}, "limit": {"type": "integer", "description": "max rows (default 200, cap 500)"}}}}, {"name": "pg_people", "description": "WHO IS AT AN ACCOUNT, AND WHO HOLDS A PERSONA ACROSS YOUR BOOK (Postgres on the box). Answers \"who are the finance leaders at UBER\", \"every controller on my book\", \"who has treasury in their title at uber.com\". TENANT SCOPED: the tenant is the slug your key resolved to and cannot be overridden by an argument. Two shapes, one tool. Give ticker or account_id alone, with or without persona, and you get the classic gtm_signals.v_tenant_people read, unchanged: the upper case columns FULL_NAME, TITLE, LINKEDIN_URL, PERSONA_KEY, PERSONA_LABEL, time in role and SOURCE / SOURCE_REF. Add any of title, seniority, function, q, include_email or cursor and it answers the roster search instead, one row per person: person_id, full_name, title, headline, seniority, function, account_id, account_name, primary_ticker, linkedin_url, persona_key, persona_label, first_seen, last_seen, source, source_ref. THE RECEIPT on a roster row is source plus source_ref, the filing or vendor record the row came from, with linkedin_url as the identity you can check the person against. ticker also accepts a domain or a company name on the roster search. NEW IN ROLE: new_in_role_days answers \"which of my accounts got a new CFO or controller in the last 90 days\" on the book view; it returns only people whose current role started inside the window and never a person whose start date is unknown. ONE ROW PER PERSON PER PERSONA on the book view, which the box guarantees: a book that carries several tickers for one account (XOMA, XOMAO, XOMAP) does not repeat a person. LIMITS: limit is max rows, default 100, cap 500; paging is by cursor, which is EXCLUSIVE, so keep next_cursor from the reply and pass it back to get the next page without replaying or skipping a row; persona, title, seniority and function narrow, q is free text over name, title and headline. IT NEVER RETURNS a phone, a personal email, or any email at all unless your key carries contact entitlement AND you passed include_email true; when it does, business_email arrives with email_validated_at and email_risk. When it does not, the field is simply absent and `entitlement` in the envelope says why.", "inputSchema": {"type": "object", "properties": {"ticker": {"type": "string", "description": "the account's ticker (a domain, account_id or company name also resolves on the roster search)"}, "account_id": {"type": "string", "description": "alternative to ticker: the acc_<32 hex> id"}, "persona": {"type": "string", "description": "optional persona key or label fragment"}, "title": {"type": "string", "description": "optional: match the job title (roster search)"}, "seniority": {"type": "string", "description": "optional: seniority band, e.g. c_level, vp, director (roster search)"}, "function": {"type": "string", "description": "optional: function, e.g. finance, procurement, travel (roster search)"}, "q": {"type": "string", "description": "optional: free text over name, title and headline (roster search)"}, "include_email": {"type": "boolean", "description": "ask for business_email; served only to a key with contact entitlement, and the envelope's entitlement says why not when it is not"}, "cursor": {"type": "string", "description": "the next_cursor you last saw; EXCLUSIVE, so the next page starts after it"}, "new_in_role_days": {"type": "integer", "description": "only people whose CURRENT role started inside this many days, e.g. 90 for \"which accounts got a new CFO this quarter\"; a person with no known role start date is not returned, because an unknown date is not evidence of a recent start. Book view only: not available with title, seniority, function, q, include_email or cursor"}, "limit": {"type": "integer", "description": "max rows (default 100, cap 500)"}}}}, {"name": "pg_ask", "description": "ASK THE POSTGRES SEMANTIC LAYER A QUESTION (no Snowflake, no model call). A plain question is mapped through the context layer's synonyms to one metric plus dimensions and run by scripts/semantic_query.py as your tenant; or send it structured: metric (e.g. signals.verified_signal_count), dimensions (e.g. [accounts.ticker]), filters (e.g. [\"signals.event_date >= this_quarter\"]). When the words name no single metric the reply is the catalog (tables, dimensions, metrics) and a note, never a guess. The reply carries mode, the SQL and the rows.", "inputSchema": {"type": "object", "properties": {"question": {"type": "string", "description": "the question, in words"}, "metric": {"type": "string", "description": "structured form: table.metric"}, "dimensions": {"type": "array", "items": {"type": "string"}, "description": "structured form: table.dimension names"}, "filters": {"type": "array", "items": {"type": "string"}, "description": "structured form: 'table.dim op value' (value may be this_quarter, this_year, today, last_N_days)"}, "limit": {"type": "integer", "description": "max rows (default 25, cap 500)"}}}}, {"name": "pg_define_trigger", "description": "DEFINE WHAT YOU NEED TO HEAR ABOUT, ON THE BOX (Postgres). Creates or updates (by name) one trigger for your tenant in gtm_signals.tenant_triggers: criteria_text in plain language, scope transcripts | documents | signals | all, filters an optional JSON object (tickers, sectors, date_from, date_to, min_score, min_lexical, lookback_days). The tenant is fixed from your key. The hourly runner then searches your book's call transcripts (hybrid search on the box), documents (filings, decks) and verified signals and writes hits you read with pg_trigger_hits. Returns the TRIGGER_ID and the row.", "inputSchema": {"type": "object", "properties": {"name": {"type": "string", "description": "short label, unique per tenant (2 to 120 chars); the same name updates the trigger"}, "criteria_text": {"type": "string", "description": "what to trigger on, in words (3 to 2000 chars)"}, "scope": {"type": "string", "description": "transcripts | documents | signals | all (default all)"}, "filters": {"type": "object", "description": "optional: {\"tickers\": [...], \"sectors\": [...], \"date_from\": \"YYYY-MM-DD\", \"date_to\": \"YYYY-MM-DD\", \"min_score\": 0.4, \"min_lexical\": 0.34, \"lookback_days\": 90}"}, "active": {"type": "boolean", "description": "false pauses the hourly run without losing hits (default true)"}}, "required": ["name", "criteria_text"]}}, {"name": "pg_list_triggers", "description": "YOUR TRIGGERS FROM THE BOX (Postgres). Every trigger defined for your tenant: TRIGGER_ID, NAME, CRITERIA_TEXT, SCOPE, FILTERS, the signal types the runner mapped the words to, ACTIVE, LAST_RUN_AT, LAST_HIT_AT and HIT_COUNT.", "inputSchema": {"type": "object", "properties": {"include_inactive": {"type": "boolean", "description": "also list paused triggers (default false)"}, "limit": {"type": "integer", "description": "max rows (default 100, cap 500)"}}}}, {"name": "pg_trigger_hits", "description": "HITS WITH RECEIPTS FROM THE BOX (Postgres). What your triggers matched, from gtm_signals.v_tenant_trigger_hits, newest first: TRIGGER_NAME, TICKER, SOURCE_KIND (transcript, signal, document), EVENT_DATE, SCORE, the verbatim QUOTE, CHAR_START/CHAR_END and RECEIPT_URL (the transcript's receipt URL, keyed for purchased text and readable with get_receipt, the document's page URL, or the signal's source URL). Quote QUOTE as is or not at all. since floors MATCHED_AT (default 30 days back); trigger_id narrows to one trigger.", "inputSchema": {"type": "object", "properties": {"trigger_id": {"type": "string", "description": "optional: one TRIGGER_ID from pg_list_triggers"}, "since": {"type": "string", "description": "optional: YYYY-MM-DD or ISO timestamp floor on MATCHED_AT (default 30 days back)"}, "source_kind": {"type": "string", "description": "optional: transcript, signal or document"}, "limit": {"type": "integer", "description": "max rows (default 50, cap 500)"}}}}, {"name": "pg_run_trigger_now", "description": "PREVIEW A TRIGGER NOW ON THE BOX (Postgres). Runs one trigger synchronously over its whole lookback window on your book (transcripts by hybrid search, documents and signals by keyword), capped at 20 hits, nothing written. Each hit is verbatim QUOTE with RECEIPT_URL, CHAR_START/CHAR_END and a SCORE (DETAILS.score_kind says which). The hourly runner persists the real hits; read those with pg_trigger_hits.", "inputSchema": {"type": "object", "properties": {"trigger_id": {"type": "string", "description": "the TRIGGER_ID from pg_define_trigger or pg_list_triggers"}}, "required": ["trigger_id"]}}, {"name": "set_trigger_active", "description": "PAUSE OR RESUME ONE OF YOUR TRIGGERS. Sets active on one trigger of your tenant in gtm_signals.tenant_triggers: false stops the hourly runner on it without losing a single hit already written, true starts it again. trigger_id is a trg_<64 hex> from list_triggers or pg_list_triggers. Returns the trigger row read back (TRIGGER_ID, NAME, CRITERIA_TEXT, SCOPE, FILTERS, ACTIVE, LAST_RUN_AT, LAST_HIT_AT, HIT_COUNT) so you can confirm the new state. The tenant is fixed from your key: a trigger_id that is not yours is refused, never silently applied to another tenant, and nothing is deleted.", "inputSchema": {"type": "object", "properties": {"trigger_id": {"type": "string", "description": "the TRIGGER_ID to pause or resume"}, "active": {"type": "boolean", "description": "false pauses the hourly run, true resumes it"}}, "required": ["trigger_id", "active"]}}, {"name": "search", "description": "ONE SEARCH ACROSS EVERYTHING THE BOX KNOWS (Postgres on AI-Powerhouse, no Snowflake, no model call, no paid API). The same unified search the portal's Search page runs, so the two surfaces cannot disagree: one query, four groups in one envelope. TENANT SCOPED. companies are the public company directory, the same identity for every tenant, with on_book saying whether that account sits on YOUR book; people, transcripts and documents are your book only and nothing else. Groups and their receipts: companies carry account_id, display_name, primary_ticker, domain, sector, exchange, on_book, and receipt.identifiers (each id_type ticker, cik or domain) plus receipt.cik and receipt.edgar_url; people carry FULL_NAME, TITLE, headline, account, persona and time in role, with receipt.linkedin_url, receipt.source and receipt.source_ref; transcripts carry the verbatim call window with TRANSCRIPT_URL plus CHAR_START/CHAR_END (START_SEC when the call is timed), TRANSCRIPT_ID, TICKER, CALL_DATE; documents carry the verbatim chunk with receipt_url or source_url, page number and char offsets. A transcript or document with no receipt URL is not returned at all. Quote the verbatim text as is or not at all. kinds is a list or comma string of companies, people, transcripts, documents (company, person, calls and docs are accepted too) and defaults to all four; limit is PER GROUP, default 10, cap 50; since (YYYY-MM-DD) or since_days bound transcripts and documents only, default 365 days back, and companies and people have no date; tickers narrows transcripts and documents. The envelope carries groups, counts, row_count, receipt_note, scope_note, timings and a per group errors list, so one slow group failing never loses the rest. It does NOT return a tier, a rank, an account score, an email or a phone, and it never returns another tenant's people, transcripts, documents or book membership.", "inputSchema": {"type": "object", "properties": {"query": {"type": "string", "description": "what to search for, in words (2 to 500 characters)"}, "kinds": {"type": "array", "items": {"type": "string"}, "description": "optional subset: companies, people, transcripts, documents (default all four)"}, "limit": {"type": "integer", "description": "max hits PER GROUP (default 10, cap 50)"}, "since": {"type": "string", "description": "optional YYYY-MM-DD floor on transcripts and documents"}, "since_days": {"type": "integer", "description": "alternative to since: days back (default 365)"}, "tickers": {"type": "array", "items": {"type": "string"}, "description": "optional: narrow transcripts and documents to these tickers"}, "mode": {"type": "string", "description": "transcript ranking: hybrid (default), keyword or vector"}}, "required": ["query"]}}, {"name": "bulk_company", "description": "EVERYTHING THE BOX HOLDS ON ONE COMPANY, IN ONE CALL (Postgres). The whole account as your tenant sees it, so an agent does not have to make eight calls: account (display_name, primary_ticker, domain, on_book), identity (sector, industry, country, exchange, cik, hq, ir_url, linkedin_url, employees_est, ipo_year), identifiers (id_type, id_value, is_primary), family (parent and children), people from your book view (identity only, with LINKEDIN_URL and persona), verified signals with SOURCE_URL and PAGE_REF, transcripts one row per call with its receipt_url and call_date, documents one row per document with doc_type, accession and receipt_url, upcoming events with WEBCAST_URL and IR_PAGE_URL, and YOUR trigger hits on the account with their quotes and receipts. Give a ticker or an account_id (acc_<32 hex>); an unknown one is refused, not guessed. since (YYYY-MM-DD) floors the dated lists. Each list is capped at limit separately, default 200, cap 500, and counts plus row_count say what came back. It does NOT return an email, a phone, a tier, a rank or an account score, and the people, signals and trigger hits are only ever your tenant's.", "inputSchema": {"type": "object", "properties": {"ticker": {"type": "string", "description": "the account's ticker"}, "account_id": {"type": "string", "description": "alternative to ticker: the acc_<32 hex> id"}, "since": {"type": "string", "description": "optional YYYY-MM-DD floor on the dated lists"}, "limit": {"type": "integer", "description": "max rows PER LIST (default 200, cap 500)"}}}}, {"name": "bulk_since", "description": "EVERYTHING NEW SINCE A CURSOR (Postgres). The poll an agent runs on a schedule to stay current without re-reading the book: transcripts loaded, documents loaded, verified signals ingested, trigger hits matched and people first seen, all on your book, oldest first. cursor is an ISO timestamp and is EXCLUSIVE; keep next_cursor from the reply and pass it back next time, which never replays a row and never skips one. Omit cursor for the last 24 hours. A cursor older than 90 days is refused, so backfill with bulk_company instead. Rows carry their receipts (transcripts receipt_url and call_date, documents receipt_url, doc_type and accession, signals SOURCE_URL and PAGE_REF, trigger hits the verbatim quote with its receipt). kinds narrows which lists are built. Each list is capped at limit separately, default 200, cap 500, and truncated says which list was cut so you know to poll again at once rather than waiting; next_action spells that out. It does NOT return an email, a phone, a tier or a rank, and never another tenant's rows.", "inputSchema": {"type": "object", "properties": {"cursor": {"type": "string", "description": "the next_cursor you last saw, an ISO timestamp, exclusive (omit for the last 24 hours, max 90 days back)"}, "limit": {"type": "integer", "description": "max rows PER LIST (default 200, cap 500)"}, "kinds": {"type": "array", "items": {"type": "string"}, "description": "optional subset: companies, people, transcripts, documents (default all)"}}}}], "note": "the tools a given key actually sees come from tools/list, which is scoped to that key. This manifest lists the catalog an ordinary key sees; private-lane tools are not listed."}