diff --git a/README.md b/README.md index 3e8acd2..597bb26 100644 --- a/README.md +++ b/README.md @@ -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) @@ -136,7 +136,7 @@ FORM4API_KEY=YOUR_API_KEY npx form4api-mcp --- -## Available tools (34) +## Available tools (35) ### Form 4 insider trading @@ -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 | @@ -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 | @@ -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. diff --git a/src/tools/_generated.ts b/src/tools/_generated.ts index af6ee0e..4acec4f 100644 --- a/src/tools/_generated.ts +++ b/src/tools/_generated.ts @@ -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.`), @@ -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('/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', diff --git a/test/mcp-test.mjs b/test/mcp-test.mjs index 2fce826..6be50b8 100644 --- a/test/mcp-test.mjs +++ b/test/mcp-test.mjs @@ -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 @@ -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. @@ -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`) }