Aggre MCP
Aggre is the org's data platform. Its MCP server exposes governed, read-mostly access to Aggre's datasets — 34 of them today, ~44 million records, fed by a separate legal-api service: Taiwan and China law and case law, US FDA drug/device/safety data, SEC health-sector filings, congressional and insider trading, and market data. The full inventory is below. There are two ways to reach it, and the right one depends on whether you're wiring up a real MCP client or just scripting a call.
The MCP server itself (both the stdio binary and the REST backend it talks to) lives in the Aggre-Infra repo (github.com/OrbbitAI/Aggre-Infra), not this one — this repo just holds the skill that explains how to use it.
Which path to use
- A real MCP client (Claude Desktop, Claude Code's
.mcp.json, or anything else that spawns an MCP server over stdio) → use theaggre-mcpstdio binary (below). This is the standard path. - A one-off script, curl, or anything that isn't an MCP client → call the REST endpoints directly (below). Same tools, same data, just plain HTTP instead of the MCP wire protocol.
Both paths ultimately hit the same backend (apps/api in the Aggre-Infra repo) and need the same API key.
Get an API key first
Every path below needs an aggre_sk_... key. Get one at https://aggre.orbbit.ai:
- Sign in with your Orbbit SSO account.
- Open the app/console page.
- Find the API Keys section and click "Create API key".
(MCP access is gated by the org's billing tier having mcp_enabled — every tier including Free has this on today, so it's not a real blocker, but if a future tier ever turns it off, tool calls will fail with a permission error rather than something confusing.)
Path 1: connect a real MCP client
The stdio server lives at crates/mcp in the Aggre-Infra repo (package name aggre-mcp). From a checkout of that repo, build it once:
cargo build --release -p aggre-mcp
The binary lands at target/release/aggre-mcp (target/release/aggre-mcp.exe on Windows). Point your MCP client at it, passing the API base and key as environment variables — it reads both at startup and has no other config:
{
"mcpServers": {
"aggre": {
"command": "/absolute/path/to/Aggre-Infra/target/release/aggre-mcp",
"env": {
"AGGRE_API_BASE": "https://aggre.orbbit.ai",
"AGGRE_API_KEY": "aggre_sk_..."
}
}
}
}
Two things worth knowing about AGGRE_API_BASE: it defaults to http://127.0.0.1:8080 if you omit it, which is a local dev API, not the hosted instance — always set it explicitly to https://aggre.orbbit.ai unless you're deliberately pointing at a local backend. And AGGRE_API_KEY is only required for actually calling a tool; the server will happily respond to initialize/ping/tools/list without one, but any tools/call will fail with JSON-RPC error -32001 until the key is set.
Once connected, the client should discover the eleven tools listed below via tools/list — no further setup needed.
Path 2: call the REST API directly
Same tools, same auth, no MCP client required:
# List available tools
curl https://aggre.orbbit.ai/v1/mcp/tools \
-H "Authorization: Bearer aggre_sk_..."
# Call a tool
curl -X POST https://aggre.orbbit.ai/v1/mcp/call \
-H "Authorization: Bearer aggre_sk_..." \
-H "Content-Type: application/json" \
-d '{"name": "list_datasets", "arguments": {}}'
Authorization: Bearer <token> accepts either an aggre_sk_... API key or an Orbbit SSO access token (JWT) — whichever you already have on hand. The stdio server (Path 1) instead sends the key as x-aggre-api-key; both header styles work against the same endpoint.
The eleven tools
Arguments are validated strictly: an unrecognised argument name is a hard error, not a silently ignored field. So {"name": "Pelosi"} on get_politician_trades fails — the argument is politician. Get the names right rather than guessing.
| Tool | Arguments | What it does |
|---|---|---|
list_datasets |
category, tags, query (all optional) |
Lists datasets visible to the caller. With 34 of them, filter: category is legal / healthcare / markets; tags is an AND over namespaced facets (`jurisdiction:TW |
search_dataset |
dataset_id (required), query (required), limit (1-50) |
Substring search over each record's JSON. Not full-text search, not semantic — and the total it returns is the count of records in that response, not a corpus-wide match count, so don't quote it as a tally. |
get_record |
id or record_id (alias) |
Fetches one record by UUID, with sensitive fields masked. |
get_dataset_schema |
dataset_id (required), limit (1-100) |
The dataset's inferred schema and version metadata. Worth one call before writing queries against an unfamiliar dataset. |
export_sample |
dataset_id (required), limit (1-100) |
A small governed sample — for deciding whether a dataset is the right shape. No offset, by design. |
export_dataset |
dataset_id (required), limit (1-2000, default 500), offset |
Pages through an ENTIRE dataset for building a local mirror. Follow next_offset until it comes back null. |
retrieve |
query (required), corpus (12 values), jurisdiction, dataset, top_k, depth, plus overrides |
Semantic retrieval (RAG), and usually what you actually want. Two-stage — lexical recall then cross-encoder rerank — returning ranked passages with citations for a natural-language question. See below. |
get_politician_trades |
politician, ticker, chamber (Senate/House), limit (≤200) |
US Congress STOCK Act trades. politician is a substring ("Pelosi"), ticker is exact. |
get_investor_holdings |
investor (required), limit (≤500) |
A famous investor's latest 13F holdings, largest position first. Substring: "Buffett", "ARK". |
get_insider_trades |
person, ticker, limit (≤200) |
Famous corporate insiders' SEC Form 4 trades ("Musk", "Cook"). |
who_traded |
ticker (required), limit (≤300) |
Everyone on the watch-lists — politicians and insiders — who traded a given ticker. |
search_dataset is substring matching; retrieve is semantic search. For a natural-language question reach for retrieve; use search_dataset when you know the literal string.
dataset_id accepts either the UUID or the slug — {"dataset_id": "fund-holdings"} works, so you do not need a list_datasets round-trip just to resolve a name you already know. The five markets/retrieve tools take no dataset argument at all; retrieve uses dataset (not dataset_id) if you want to pin one explicitly.
Using retrieve well
corpus— twelve values, in three families. Corpora are separate datasets, solawscan never return case law and no FDA corpus can return legal text.- General law:
judgments(default, case law),laws(statutes and regulations),both. - Medical law:
medical-laws,medical-judgments, andmedicalfor both together — which is how a malpractice question is actually researched, since 醫療法 §82 means little without the judgments that applied it. - US regulatory and corporate:
drug-labels(openFDA SPL full text: indications, dosage, contraindications, warnings, adverse reactions),fda-safety(recalls, enforcement reports, Complete Response Letters),510k(aliasdevice-summaries— the narrative text of 510(k) summary documents),device-classification(device classes and their regulation definitions),sec-filings(10-K/20-F/S-1/8-K passages from Item 1 Business and Item 1A Risk Factors — drug pipelines, clinical programs, regulatory and IP risk, in the company's own words; hits carryform,section,source_url).
- General law:
jurisdiction—TW(default),CN, orUS. It only means something for the general and medical corpora:USthere resolves to 21/42/45 CFR. The FDA and SEC corpora ignore it, being US-only already. Two combinations deliberately return an explanatory error instead of an answer —medical-judgmentswithCNorUS— because we hold no medical case law for either, and handing back Taiwanese judgments would look like a result.depth—fast/standard(default) /thorough.thoroughdoubles the first-stage time budget and reads more of each candidate; use it when the answer matters more than the wait. The fine-grained overrides (candidates≤32,threshold,rerank_chars,stage1_timeout_secs) beat whateverdepthset.top_k— reranked passages returned, default 8, max 30.
The org's current datasets
34 datasets, ~44M records (counted 2026-08-02). Slugs are stable; UUIDs are not — always resolve via list_datasets. Counts are rounded and grow.
Taiwan law and case law
taiwan-judgments— judicial judgments, 24.4M. The single largest corpus. ⚠️ see the retrieval warning below.taiwan-moj-orders-ch-order(185k) /taiwan-moj-orders-en-order(98k) — MOJ orders, Chinese and English.taiwan-moj-laws-ch-law(51k) /taiwan-moj-laws-en-law(96k) — MOJ law articles, not laws: one record per article, so 民法 alone is hundreds of records.taiwan-medical-judgments(18k) /taiwan-medical-laws(10k) — the medical-liability and health-regulation slices, pre-filtered.taiwan-standard-contracts(365) — 定型化契約, the government's mandatory standard-form contract templates.
China law and case law
china-court-judgments— 2.06M court judgments.china-law-articles(82k) — national laws at article level, the granularity you want for citation.china-national-laws(2.4k) — the same laws as whole documents.china-medical-laws(91) — health and medical regulation.
US FDA (openFDA plus scraped 510(k) summaries)
fda-approvals— 6.91M approvals and registrations.fda-device-identifiers(3.55M) ·fda-adverse-events(1.60M) ·fda-substances(1.03M).fda-drug-labels(262k) — SPL labelling: indications, dosage, warnings, adverse reactions. The prose corpus; reachable viaretrievewithcorpus: "drug-labels".fda-device-clearances(234k) — 510(k) + PMA.fda-drug-approvals(218k) — approvals, listings, shortages.fda-safety(146k) — recalls and safety actions;retrievewithcorpus: "fda-safety".fda-device-summaries(48k) — the full text of 510(k) summary PDFs, extracted.fda-device-classification(7k) — device classes and their regulation definitions.
SEC (health and biotech sector)
sec-health-filings(1.87M) — filing metadata.sec-health-chunks(1.55M) — filing text split into passages, which is what retrieval actually reads.sec-health-documents(100k) — whole filing texts.sec-health-companies(6.1k) — the company registry the above hangs off.
US healthcare regulation
us-cfr-medical(16k) — CFR titles 21 / 42 / 45.us-exclusions(84k) — the OIG LEIE exclusion list. Sensitive fields are masked on read.
Markets and trading
global-stocks(24k) ·economic-indicators(1.3k) ·crypto-markets(534).fund-holdings(11k) — 13F positions; preferget_investor_holdings.congress-trades(9.9k) — STOCK Act; preferget_politician_trades.insider-trades(567) — Form 4; preferget_insider_trades.
⚠️ Read complete before you conclude anything
Two different things can happen to a large corpus, and both look like an ordinary answer unless you check.
A corpus can be cut short. Its first-stage retrieval exceeds its time budget
and contributes nothing, while the other corpora in the same call answer
normally. timed_out names it, complete is false, and note explains. This
is reported as an empty or partial answer, not an error — so say the search
was incomplete, rather than that no law exists, and retry with a longer, more
specific query.
A corpus can answer from a sample. taiwan-judgments is 24.4M documents, and
a short query matches so many of them that ranking them all is unaffordable —
酒後駕車 (4 characters) admits over half a million index candidates. Rather
than return nothing, retrieval falls back to reranking an unordered sample of
the matching documents. The hits are genuinely relevant (measured: the top 8 for
酒後駕車 were all 公共危險 judgments, the offence drunk driving is charged
under) but they are not the best matches, and not exhaustive — so do not
present them as "the leading cases" or count them. recall_only names the
corpus, complete is false, and note says so.
A longer, more specific query gets the properly ranked set: 違反個人資料保護法之損害賠償
(14 characters) ranks normally in ~23s. China case law ranks normally above
~7 characters.
So: complete: true means what you got is the ranked, whole answer. Anything
else, read timed_out and recall_only before writing a sentence about what
the law says.
Examples
Ask a legal question. This is the common case, and it needs no dataset lookup:
{"name": "retrieve", "arguments": {"query": "醫療機構未盡告知義務的損害賠償責任", "corpus": "both", "jurisdiction": "TW", "depth": "thorough"}}
→ ranked passages from statutes and case law together, each with a citation. Check complete before you summarise: false means the search was cut short, not that nothing exists.
Ask what a drug's label says:
{"name": "retrieve", "arguments": {"query": "semaglutide contraindications thyroid", "corpus": "drug-labels", "top_k": 5}}
Look up a literal string in a specific dataset. The slug works directly — no list_datasets round-trip:
{"name": "search_dataset", "arguments": {"dataset_id": "taiwan-moj-laws-ch-law", "query": "個人資料", "limit": 10}}
→ up to 10 law-article records whose JSON contains 個人資料, e.g. articles of 個人資料保護法.
Find datasets you don't already know the slug of:
{"name": "list_datasets", "arguments": {"tags": ["jurisdiction:CN", "type:case-law"]}}
Who traded a ticker:
{"name": "who_traded", "arguments": {"ticker": "NVDA"}}