From 13c7db24220e466292e0032bd0790799dd7498c9 Mon Sep 17 00:00:00 2001 From: Theodor Date: Wed, 12 Aug 2026 10:15:42 +0200 Subject: [PATCH] Regenerate tools: House-only coverage note, and the missing filings tool Two things came out of one codegen run against the live spec. The congress tool now tells the model what it is actually querying. We hold House PTRs only -- Senate eFD blocks datacenter traffic -- and until now the tool description advertised "chamber (House|Senate)" with no hint that one of those matches nothing. That is worse here than in the REST API: an assistant reading this description will confidently filter by Senate, get an empty array, and report "no senate trades found" to a user as if it were a fact about Congress. The description and the chamber param both now carry the coverage note and point at the X-Coverage-Note header. list_filings was missing entirely. GET /v1/filings shipped in insiderapi #206 on 2026-08-04 -- built because four distinct users had 404'd that path 382 times -- and the codegen pass on 2026-08-10 did not pick it up, so the endpoint has existed for eight days with no MCP tool. Found by running codegen:check during an unrelated copy sweep, which is not a reliable way to notice a missing tool. Tool count 34 -> 35, free tools 21 -> 22, since /v1/filings is ungated. README counts, the tool table and the expected-name list in the test all updated; that hardcoded count is what caught the addition. codegen:check now reports in sync. 65/65 tests. Not published to npm. --- README.md | 9 +++++---- src/tools/_generated.ts | 29 +++++++++++++++++++++++++++-- test/mcp-test.mjs | 10 +++++++--- 3 files changed, 39 insertions(+), 9 deletions(-) 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`) }