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
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,13 @@ This ensures clients can always see what tools are available in a given version

---

## [Unreleased]

**Tool count: 38** (+1: `search`). Free tools 23 → 24; gated unchanged.

### Added
- `search` (hand-written) — resolve a company name, ticker fragment, or a person's name to a ticker or CIK in one call, returning separately-ranked `companies` and `insiders` lists. Free plan. Wraps `GET /v1/search` (backend insiderapi #314, live since 2026-09-25). Framed in `SERVER_INSTRUCTIONS` and its own description as the obvious first call when an agent has a name/fragment but no identifier yet. Kept hand-written (added to `HANDLED_BY_HANDWRITTEN` in `codegen/generate.mjs`) rather than left to codegen so the description could explicitly steer agents to call it before `get_company_overview`/`get_insider_profile`/etc., matching the pattern used for `research_company`.

## [1.15.0] — 2026-09-24

**Tool count: 37** (+1: `list_schedule13_dg`). Free tools unchanged at 23; gated 13 → 14.
Expand Down
13 changes: 10 additions & 3 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** — 35 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** — 36 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,13 @@ FORM4API_KEY=YOUR_API_KEY npx form4api-mcp

---

## Available tools (35)
## Available tools (36)

### Search

| Tool | Description | Plan |
|---|---|---|
| `search` | Resolve a company name, ticker fragment, or a person's name to a ticker or CIK — the usual first call when you don't already have an identifier. Returns separately-ranked `companies` and `insiders` lists | Free |

### Form 4 insider trading

Expand Down Expand Up @@ -273,12 +279,13 @@ The MCP wraps the same backend as all of the above — every fact your LLM cites

## Plans

**22 of the 35 tools work on the free plan, and every tool that is free today stays free.**
**23 of the 36 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.

| Tool | Free | Pro | Business |
|---|---|---|---|
| `search` | ✓ | ✓ | ✓ |
| `get_transactions`, `get_recent_filings`, `get_filing` | ✓ | ✓ | ✓ |
| `get_insider_profile`, `get_insider_transactions` | ✓ | ✓ | ✓ |
| `get_company_overview`, `get_company_insiders` | ✓ | ✓ | ✓ |
Expand Down
1 change: 1 addition & 0 deletions codegen/generate.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ const HANDLED_BY_HANDWRITTEN = new Set([
'ListHoldings', // → get_holdings (src/tools/holdings.ts)
'ListManagers', // → get_managers (src/tools/managers.ts)
'GetKeyUsage', // → check_usage (src/tools/usage.ts)
'Search', // → search (src/tools/search.ts)
])

// Override the default snake_case mapping for a clearer LLM tool name on a
Expand Down
2 changes: 1 addition & 1 deletion server.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json",
"name": "io.github.theodor90/form4api-mcp",
"title": "Form4API — SEC Insider Trading",
"description": "Real-time SEC Form 4, Form 144, 13F-HR & congressional STOCK Act insider data. 37 tools + 6 prompts.",
"description": "Real-time SEC Form 4, Form 144, 13F-HR & congressional STOCK Act insider data. 38 tools + 6 prompts.",
"version": "1.15.0",
"websiteUrl": "https://www.form4api.com",
"repository": {
Expand Down
17 changes: 16 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import { getInsiderCareerSummarySchema, getInsiderCareerSummary } from './tools/
import { checkUsageSchema, checkUsage } from './tools/usage.js'
import { verifySetupSchema, verifySetup } from './tools/verify-setup.js'
import { researchCompanySchema, researchCompany } from './tools/research.js'
import { searchSchema, search } from './tools/search.js'
import { GENERATED_TOOLS } from './tools/_generated.js'
import { RECIPE_PROMPTS } from './prompts/recipes.js'

Expand All @@ -32,7 +33,7 @@ const { version } = JSON.parse(

const SERVER_INSTRUCTIONS = `Form4API insider trading data: amendment-aware Form 4 transactions, 10b5-1 plan flags, Form 144 intent-to-sell, institutional 13F-HR overlay, and congressional STOCK Act trades — including the insider-cluster-buy x congressional-purchase convergence signal.

Start with \`research_company\` to get bundled insider context for any ticker. If something fails, run \`verify_setup\` first. Try \`get_public_stats\` without a key to preview the data.
Only have a company name, ticker fragment, or a person's name? Call \`search\` first to resolve it to a ticker or CIK. Start with \`research_company\` to get bundled insider context for any ticker. If something fails, run \`verify_setup\` first. Try \`get_public_stats\` without a key to preview the data.

Try these prompts:
• "What insider trades happened at NVDA in the last 30 days, excluding 10b5-1 plans?"
Expand Down Expand Up @@ -279,6 +280,20 @@ server.tool(
},
)

server.tool(
'search',
'Resolve free-text input — a company name, ticker fragment, or a person\'s name — to a ticker or CIK. This is usually the right FIRST call when a user names a company or insider you don\'t already have an identifier for; feed the resulting ticker into get_company_overview/get_transactions or the CIK into get_insider_profile/get_insider_transactions. Returns two independently-ranked lists: `companies` (only ones with a resolved public ticker, ranked exact ticker match, then ticker-prefix, then name-prefix, then name-contains) and `insiders` (matched by splitting the query on whitespace so every token must match the name, e.g. "tim cook" matches SEC-style "Cook Timothy D"; ranked by total filing count, most active first). Each insider\'s `ticker` is a ticker associated with that insider (may be null). `q` must be 2-64 characters after trimming (400 QUERY_TOO_SHORT/QUERY_TOO_LONG). `limit` applies independently to each list, default 8, max 20. Free plan, no caching.',
searchSchema.shape,
READ_ONLY,
async (input) => {
try {
return wrapResult(await search(client, input as Parameters<typeof search>[1]))
} catch (err) {
return wrapError(err)
}
},
)

server.tool(
'research_company',
"Bundled insider-research context for one ticker in a single call — company profile, recent insider transactions (10b5-1 flagged), cluster buy/sell signals, monthly sentiment score, and a computed net buy/sell direction summary. Use this FIRST for any company insider-research question; it replaces 4 separate calls (get_company_overview, get_transactions, get_signals, get_sentiment) and degrades gracefully — if signals/sentiment require a plan you don't have, they come back null with an explanatory `_unavailable` note instead of failing the whole call. Company/transaction data works on Free; signals/sentiment sections require Business. Fetches all sections concurrently. Research and decision-support only, not investment advice.",
Expand Down
27 changes: 27 additions & 0 deletions src/tools/search.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { z } from 'zod'
import type { Form4ApiClient } from '../client.js'
import type { SearchResponse } from '../types.js'

export const searchSchema = z.object({
q: z
.string()
.describe(
'Search text — a company name/ticker fragment and/or a person\'s name, in any order (e.g. "tim cook" matches insider "Cook Timothy D"). Trimmed before validation; must be 2-64 characters after trimming.',
),
limit: z
.number()
.int()
.min(1)
.max(20)
.optional()
.describe('Maximum number of results per section (companies, insiders), applied independently to each. Defaults to 8, clamped to 1-20.'),
})

export type SearchInput = z.infer<typeof searchSchema>

export async function search(client: Form4ApiClient, input: SearchInput): Promise<SearchResponse> {
return client.get<SearchResponse>('/v1/search', {
q: input.q,
limit: input.limit,
})
}
18 changes: 18 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,24 @@ export interface CompanyInsider {
totalTransactions: number
}

export interface SearchCompanyResult {
ticker: string
name: string
cik: string
}

export interface SearchInsiderResult {
cik: string
name: string
title: string | null
ticker: string | null
}

export interface SearchResponse {
companies: SearchCompanyResult[]
insiders: SearchInsiderResult[]
}

export interface Signal {
signalId: number
ticker: string
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 37 tools with correct names
* 2. tools/list returns all 38 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 @@ -69,6 +69,10 @@ const EXPECTED_TOOLS = [
// Auto-generated after the Schedule 13D/13G ownership-crossings endpoint
// shipped; picked up by the 2026-09-24 OpenAPI sync (v1.15.0).
'list_schedule13_dg',
// Hand-written combined company/insider name resolver (insiderapi #314,
// 2026-09-25) — framed as the obvious first call to resolve a name to a
// ticker/CIK before calling other tools.
'search',
]

// Recipe prompts (v1.9.0, 2026-07-11) — the MCP "prompts" capability.
Expand Down Expand Up @@ -153,11 +157,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 — 37 tools registered')
console.log('\n[2] tools/list — 38 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 === 37, `37 tools returned (got ${toolNames.length})`)
assert(toolNames.length === 38, `38 tools returned (got ${toolNames.length})`)
for (const name of EXPECTED_TOOLS) {
assert(toolNames.includes(name), `tool "${name}" present`)
}
Expand Down
Loading