# Aggre MCP

> Connect to and query Orbbit's Aggre MCP server — the org's data platform, holding Taiwan and China law and case law, US FDA drug/device data, SEC filings, congressional and insider trading, and market data. Use this whenever the user wants to search or browse Aggre's ingested data, connect an MCP client (Claude Desktop, Claude Code, etc.) to Aggre, build or debug the aggre-mcp stdio server, or call Aggre's MCP tools (list_datasets, search_dataset, retrieve, get_record, get_dataset_schema, export_sample, export_dataset, get_politician_trades, get_investor_holdings, get_insider_trades, who_traded) directly via REST. Also trigger for questions like "how do I query our legal data from Claude", "what does the FDA say about this drug", "which senators traded NVDA", "connect MCP to aggre.orbbit.ai", or "what MCP tools does Aggre expose."

- Skill: `orbbitai/aggre-mcp` (Agent Skill, multi-file: 2 files)
- Install (CLI): `npx skillmds@latest add orbbitai/aggre-mcp`
- Raw SKILL.md: https://api.skillmd.com/api/skills/orbbitai/aggre-mcp/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: AI & ML
- Author: OrbbitAI (https://skillmd.com/u/orbbitai)
- Updated: 2026-09-22
- Page: https://skillmd.com/skills/orbbitai/aggre-mcp

---


# 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 the `aggre-mcp` stdio 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:

1. Sign in with your Orbbit SSO account.
2. Open the app/console page.
3. 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:

```bash
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:

```json
{
  "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:

```bash
# 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|CN|US|global`, `lang:zh-Hant|zh-Hans|en`, `type:statute|regulation|case-law|contract-template|drug-label|approval|recall|adverse-event|filing|company-profile|quote|indicator|holding|trade`, `modality:fulltext|structured`, `source:…`, `sector:…`, and `rag:retrievable` for the ones `retrieve` can search); `query` substring-matches slug/name/description. |
| `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, so `laws` can 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`, and `medical` for 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` (alias `device-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 carry `form`, `section`, `source_url`).
- `jurisdiction` — `TW` (default), `CN`, or `US`. It only means something for the general and medical corpora: `US` there 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-judgments` with `CN` or `US` — because we hold no medical case law for either, and handing back Taiwanese judgments would look like a result.
- `depth` — `fast` / `standard` (default) / `thorough`. `thorough` doubles 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 whatever `depth` set.
- `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 via `retrieve` with `corpus: "drug-labels"`.
- `fda-device-clearances` (234k) — 510(k) + PMA.
- `fda-drug-approvals` (218k) — approvals, listings, shortages.
- `fda-safety` (146k) — recalls and safety actions; `retrieve` with `corpus: "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; prefer `get_investor_holdings`.
- `congress-trades` (9.9k) — STOCK Act; prefer `get_politician_trades`.
- `insider-trades` (567) — Form 4; prefer `get_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:

```json
{"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:**

```json
{"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:

```json
{"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:**

```json
{"name": "list_datasets", "arguments": {"tags": ["jurisdiction:CN", "type:case-law"]}}
```

**Who traded a ticker:**

```json
{"name": "who_traded", "arguments": {"ticker": "NVDA"}}
```

