Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 5 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# form4api-mcp

> Production-grade SEC Form 4 insider trading data for any MCP-compatible AI assistant — **amendment-aware, 10b5-1 clean, with Form 144 + institutional 13F-HR overlay, plus congressional STOCK Act trades and insider/Congress convergence** — 34 tools + 6 ready-made research prompts
> Production-grade SEC Form 4 insider trading data for any MCP-compatible AI assistant — **amendment-aware, 10b5-1 clean, with Form 144 + institutional 13F-HR overlay, plus congressional STOCK Act trades and insider/Congress convergence** — 35 tools + 6 ready-made research prompts

[![npm version](https://badge.fury.io/js/form4api-mcp.svg)](https://www.npmjs.com/package/form4api-mcp)
[![Available on mcp.so](https://img.shields.io/badge/mcp.so-form4api-blue)](https://mcp.so)
Expand Down Expand Up @@ -136,7 +136,7 @@ FORM4API_KEY=YOUR_API_KEY npx form4api-mcp

---

## Available tools (34)
## Available tools (35)

### Form 4 insider trading

Expand All @@ -145,6 +145,7 @@ FORM4API_KEY=YOUR_API_KEY npx form4api-mcp
| `research_company` | Bundled insider-research context for one ticker in a single call — company profile, recent transactions, cluster signals, sentiment, and a computed buy/sell direction summary. Replaces 4 separate calls and degrades gracefully when a section needs a higher plan | Free (signals/sentiment sections need Business) |
| `get_transactions` | Search insider transactions — filter by ticker, insider, date range, transaction codes or whole categories (`exclude_category=derivatives`), 10b5-1 plan trades, a dollar floor (`min_value`), the 13F ownership trend (`inst_ownership_trend`), or use `significant=true` for real discretionary buys/sells only. Pro adds the remaining trade-size screens (`max_value`, `min_shares`, `max_shares`) and post-trade-return screening (`min_return_1d`…`max_return_6m`, `has_returns`; returns are fractions, 0.05 = +5%). Paging depth is plan-limited — see Plans | Free |
| `get_recent_filings` | Most recent Form 4 filings, optionally filtered by ticker | Free |
| `list_filings` | Form 4 filings as a paginated list, newest filed first — filter by ticker, cik, or a filed-date window. Use this to page through filings; `get_recent_filings` is the unfiltered head of the same feed | Free |
| `get_filing` | Single filing by accession number | Free |
| `get_insider_profile` | Insider profile — name, title, director/officer/10pct owner flags | Free |
| `get_insider_transactions` | All transactions for a specific insider (by CIK) | Free |
Expand Down Expand Up @@ -176,7 +177,7 @@ FORM4API_KEY=YOUR_API_KEY npx form4api-mcp

| Tool | Description | Plan |
|---|---|---|
| `list_congress_trades` | Congressional STOCK Act trades (periodic transaction reports) — filter by ticker, politician, party, chamber, state, transaction type, min amount, or date range. Every row carries `amountLow`/`amountHigh` (disclosed ranges, never a fabricated midpoint) and `disclosureLagDays` — up to 45 days under the STOCK Act, so "real-time" here means minutes-after-disclosure, not minutes-after-trade | Free (30-day disclosure window; Starter 366 days; Pro+ unlimited history) |
| `list_congress_trades` | Congressional STOCK Act trades (periodic transaction reports) — filter by ticker, politician, party, chamber, state, transaction type, min amount, or date range. **Coverage is U.S. House only** — Senate eFD blocks datacenter traffic, so `chamber=Senate` matches nothing and the response carries `X-Coverage-Note: chamber-not-covered`. Every row carries `amountLow`/`amountHigh` (disclosed ranges, never a fabricated midpoint) and `disclosureLagDays` — up to 45 days under the STOCK Act, so "real-time" here means minutes-after-disclosure, not minutes-after-trade | Free (30-day disclosure window; Starter 366 days; Pro+ unlimited history) |
| `list_congress_politicians` | Ranked rollup of politicians by congressional trade activity — total/buy/sell counts, most recent disclosure | Pro |
| `get_congress_politician` | One politician's full profile by bioguide ID — totals, top traded tickers, most recent trades | Pro |
| `get_congress_ticker_rollup` | Which politicians traded a given ticker, with net buy/sell counts | Pro |
Expand Down Expand Up @@ -272,7 +273,7 @@ The MCP wraps the same backend as all of the above — every fact your LLM cites

## Plans

**21 of the 34 tools work on the free plan, and every tool that is free today stays free.**
**22 of the 35 tools work on the free plan, and every tool that is free today stays free.**
New premium capability gets tiered as it ships; nothing that already works on your key is
taken away later.

Expand Down
29 changes: 27 additions & 2 deletions src/tools/_generated.ts
Original file line number Diff line number Diff line change
Expand Up @@ -238,12 +238,12 @@ export const GENERATED_TOOLS: GeneratedTool[] = [
operationId: 'ListCongressTrades',
method: 'GET',
path: '/v1/congress/trades',
description: "Query congressional STOCK Act trades (Free+, plan-clamped disclosure window). Returns a paginated JSON list of congressional periodic-transaction-report trades, most recently DISCLOSED first, with non-superseded rows only (amended-away rows never appear). PLAN-CLAMPED WINDOW: this endpoint is open to every plan, but how far back you can see is clamped on disclosureDate — Free sees only trades disclosed in the last 30 days, Starter the last 366 days, Pro/Business/Enterprise unlimited history. Passing an older disclosure_date_from than your plan allows does not extend the window — the floor always wins. Filters: ticker, politician (bioguideId, exact), party (free-text, case-insensitive exact match — not a fixed enum), chamber (House|Senate), state (2-letter code), transaction_type (purchase|sale|partial_sale|exchange), min_amount (range-aware — matches AmountLow >= value, never a fabricated midpoint), transaction_date_from/to, disclosure_date_from/to. Every row always carries BOTH amountLow and amountHigh (STOCK Act discloses ranges, never exact figures) and disclosureLagDays = (disclosureDate - transactionDate) — the STOCK Act allows up to 45 days of lag, so \"real-time\" here means minutes-after-disclosure, not minutes-after-trade. For per-politician or per-ticker rollups use GET /v1/congress/politicians, /v1/congress/politicians/{idOrSlug}, or /v1/congress/tickers/{ticker} (all Pro+). Query runs live against the database — no caching.",
description: "Query congressional STOCK Act trades (Free+, plan-clamped disclosure window). Returns a paginated JSON list of congressional periodic-transaction-report trades, most recently DISCLOSED first, with non-superseded rows only (amended-away rows never appear). COVERAGE — HOUSE ONLY TODAY: every trade in this dataset comes from the U.S. House Clerk's PTR index. Senate eFD (efdsearch.senate.gov) returns 403 to datacenter traffic, so no Senate filings are ingested yet. chamber=Senate remains a valid filter but matches nothing and returns the response header X-Coverage-Note: chamber-not-covered, so an empty result is never ambiguous. Scanning by chamber should treat that header as \"not covered\", not as \"no trades\". PLAN-CLAMPED WINDOW: this endpoint is open to every plan, but how far back you can see is clamped on disclosureDate — Free sees only trades disclosed in the last 30 days, Starter the last 366 days, Pro/Business/Enterprise unlimited history. Passing an older disclosure_date_from than your plan allows does not extend the window — the floor always wins. Filters: ticker, politician (bioguideId, exact), party (free-text, case-insensitive exact match — not a fixed enum), chamber (House|Senate — see the coverage note above), state (2-letter code), transaction_type (purchase|sale|partial_sale|exchange), min_amount (range-aware — matches AmountLow >= value, never a fabricated midpoint), transaction_date_from/to, disclosure_date_from/to. Every row always carries BOTH amountLow and amountHigh (STOCK Act discloses ranges, never exact figures) and disclosureLagDays = (disclosureDate - transactionDate) — the STOCK Act allows up to 45 days of lag, so \"real-time\" here means minutes-after-disclosure, not minutes-after-trade. For per-politician or per-ticker rollups use GET /v1/congress/politicians, /v1/congress/politicians/{idOrSlug}, or /v1/congress/tickers/{ticker} (all Pro+). Query runs live against the database — no caching.",
schema: {
ticker: z.string().optional().describe(`Ticker symbol, case-insensitive exact match (e.g. "AAPL").`),
politician: z.string().optional().describe(`Politician's bioguide ID, exact match (e.g. "P000197").`),
party: z.string().optional().describe(`Party as disclosed by the source, case-insensitive exact match (e.g. "D", "R", "Democratic"). Free-text — not a fixed enum, so this matches whatever string the source reported.`),
chamber: z.string().optional().describe(`"House" or "Senate", case-insensitive.`),
chamber: z.string().optional().describe(`"House" or "Senate", case-insensitive. COVERAGE: this dataset currently holds House PTRs only — Senate eFD blocks datacenter traffic, so chamber=Senate is a valid filter over data we do not yet have and returns an empty array with the response header X-Coverage-Note: chamber-not-covered.`),
state: z.string().optional().describe(`Two-letter US state/territory code, case-insensitive exact match (e.g. "CA").`),
transaction_type: z.string().optional().describe(`"purchase", "sale", "partial_sale", or "exchange", case-insensitive.`),
min_amount: z.number().optional().describe(`Minimum disclosed amount, range-aware: matches trades whose AmountLow >= this value. Never matched against a fabricated midpoint — see the amountLow/amountHigh honesty rule.`),
Expand All @@ -270,6 +270,31 @@ export const GENERATED_TOOLS: GeneratedTool[] = [
per_page: input.per_page as never,
}),
},
{
name: 'list_filings',
operationId: 'ListFilings',
method: 'GET',
path: '/v1/filings',
description: "List Form 4 filings with optional ticker, CIK and date filters. Returns a paginated list of Form 4 filings, newest filed first. Filter by ticker, cik, and a from/to filed-date window. Each entry carries the accession number, company ticker/name, period of report, filed date, amendment type (Original/Amendment), and the count of non-superseded transactions in that filing. Use this for a company's filing HISTORY; use GET /v1/filings/recent for a live newest-first feed (it has no page parameter), and GET /v1/transactions when you want the individual trades rather than the filings that contain them. `limit` is accepted as an alias for `per_page`. Not plan-gated.",
schema: {
ticker: z.string().optional().describe(`Company ticker symbol, case-insensitive (e.g. "AAPL").`),
cik: z.string().optional().describe(`Company CIK (SEC identifier), e.g. "0000320193". Leading zeros optional.`),
from: z.string().optional().describe(`Inclusive start of the filed-date window, format YYYY-MM-DD.`),
to: z.string().optional().describe(`Inclusive end of the filed-date window, format YYYY-MM-DD.`),
page: z.number().int().optional().describe(`1-based page number. Defaults to 1.`),
per_page: z.number().int().optional().describe(`Filings per page. Defaults to 20, maximum 100. \`limit\` is accepted as an alias; if both are given, per_page wins.`),
limit: z.number().int().optional().describe(`Alias for per_page. Accepted because every caller who hit this path before it existed sent \`limit\`.`),
},
handler: async (client, input) => client.get<unknown>('/v1/filings', {
ticker: input.ticker as never,
cik: input.cik as never,
from: input.from as never,
to: input.to as never,
page: input.page as never,
per_page: input.per_page as never,
limit: input.limit as never,
}),
},
{
name: 'list_webhooks',
operationId: 'ListWebhooks',
Expand Down
10 changes: 7 additions & 3 deletions test/mcp-test.mjs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/**
* MCP protocol test — spawns the server and verifies:
* 1. initialize handshake
* 2. tools/list returns all 34 tools with correct names
* 2. tools/list returns all 35 tools with correct names
* 3. prompts/list returns all 6 recipe prompts
* 4. prompts/get returns rendered messages for a sample of prompts
* 5. (optional) live tool call if FORM4API_KEY is set
Expand Down Expand Up @@ -60,6 +60,10 @@ const EXPECTED_TOOLS = [
'get_congress_politician',
'get_congress_ticker_rollup',
'get_convergence_signals',
// Auto-generated after the /v1/filings listing endpoint shipped (insiderapi
// #206, 2026-08-04). Missed by the 2026-08-10 codegen pass and caught by the
// 2026-08-12 coverage sweep — the endpoint existed for 8 days with no tool.
'list_filings',
]

// Recipe prompts (v1.9.0, 2026-07-11) — the MCP "prompts" capability.
Expand Down Expand Up @@ -144,11 +148,11 @@ async function runTest() {
server.stdin.write(JSON.stringify({ jsonrpc: '2.0', method: 'notifications/initialized', params: {} }) + '\n')

// ── Test 2: tools/list ────────────────────────────────────────────────
console.log('\n[2] tools/list — 34 tools registered')
console.log('\n[2] tools/list — 35 tools registered')
const listRes = await send('tools/list', {})
const toolNames = (listRes.result?.tools ?? []).map(t => t.name)
assert(!listRes.error, 'no error in tools/list response')
assert(toolNames.length === 34, `34 tools returned (got ${toolNames.length})`)
assert(toolNames.length === 35, `35 tools returned (got ${toolNames.length})`)
for (const name of EXPECTED_TOOLS) {
assert(toolNames.includes(name), `tool "${name}" present`)
}
Expand Down
Loading