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
12 changes: 12 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,18 @@ This ensures clients can always see what tools are available in a given version
### 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.1] — 2026-10-02

Tool count unchanged. No tools added, renamed, or removed.

### Changed
- The `upgrade_required` payload no longer contains prices. `unlocks` is now a neutral description of what the plan includes (for example "The Business plan includes cluster-buy signals and sentiment scores, ..."), with no dollar amounts.
- `upgrade_url` is now optional: it is the backend's `upgradeUrl` when the error body has one, and is omitted when the backend sends none or null (previously it fell back to a hard-coded billing URL).
- `research_company` and `verify_setup` no longer embed a hard-coded billing link in their plan-required messages.

### Removed
- `pricing_url` from the `upgrade_required` payload and its type.

## [1.15.0] — 2026-09-24

**Tool count: 37** (+1: `list_schedule13_dg`). Free tools unchanged at 23; gated 13 → 14.
Expand Down
7 changes: 2 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -311,9 +311,7 @@ A gated call never surfaces a bare HTTP error. The MCP returns a structured
"required_plan": "business",
"current_plan": "Free",
"message": "This endpoint requires the Business plan or higher. Your current plan is Free.",
"unlocks": "Business ($149/mo) adds cluster-buy signals and sentiment scores, 13F institutional holdings and managers, Form 144 notices, bulk CSV export, and 250,000 requests/day.",
"upgrade_url": "https://www.form4api.com/dashboard/billing",
"pricing_url": "https://www.form4api.com/pricing"
"unlocks": "The Business plan includes cluster-buy signals and sentiment scores, 13F institutional holdings and managers, Form 144 notices, bulk CSV export, and 250,000 requests/day."
}
```

Expand All @@ -322,8 +320,7 @@ parameter that stopped the call, which is usually what the model needs to sugges
alternative. The same shape is returned when a Pro-only *parameter* is used on an otherwise
free tool, so the model can simply retry without that filter.

Upgrade at [form4api.com/dashboard/billing](https://www.form4api.com/dashboard/billing), or
compare tiers at [form4api.com/pricing](https://www.form4api.com/pricing).
`upgrade_url` is included only when the API supplies one; it is omitted otherwise.

---

Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "form4api-mcp",
"version": "1.15.0",
"version": "1.15.1",
"mcpName": "io.github.theodor90/form4api-mcp",
"description": "MCP server for Form4API — SEC Form 4 insider trading, Form 144 intent-to-sell, institutional 13F-HR, and congressional STOCK Act trading data, including the insider/Congress convergence signal. Lets Claude / Cursor / Windsurf / Zed query real-time US insider and congressional trading data via natural language.",
"keywords": [
Expand Down
4 changes: 2 additions & 2 deletions server.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"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. 38 tools + 6 prompts.",
"version": "1.15.0",
"version": "1.15.1",
"websiteUrl": "https://www.form4api.com",
"repository": {
"url": "https://github.com/theodor90/form4api-mcp",
Expand All @@ -13,7 +13,7 @@
{
"registryType": "npm",
"identifier": "form4api-mcp",
"version": "1.15.0",
"version": "1.15.1",
"transport": { "type": "stdio" },
"environmentVariables": [
{
Expand Down
35 changes: 14 additions & 21 deletions src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,47 +15,40 @@ const { version: PKG_VERSION } = JSON.parse(
export type RequiredPlan = 'starter' | 'pro' | 'business' | 'enterprise' | 'higher'

// Structured payload the MCP returns when an upstream call needs a plan
// upgrade. The LLM router can read the JSON shape and surface the upgrade
// path to the user cleanly (e.g. "you need Business for sentiment scores,
// upgrade at form4api.com/dashboard/billing") instead of swallowing an
// opaque 402 text. PLAN_MCP_DEFENSE Decision 1 (2026-06-01).
// upgrade. The LLM router can read the JSON shape and explain to the user
// cleanly which plan a call needs, instead of swallowing an opaque 402 text. PLAN_MCP_DEFENSE Decision 1 (2026-06-01).
//
// `message` is the backend's own text, verbatim. That matters: the API
// explains each gate specifically (which page depth was exceeded, which
// filter param is Pro-only, that /v1/transactions/export exists for bulk
// pulls), and that detail is far more useful to a calling agent than any
// sentence this client could synthesise. `unlocks` adds what the target plan
// buys, so the agent can relay a complete upgrade pitch in one turn instead
// of making the user go read the pricing page to find out.
// sentence this client could synthesise. `unlocks` states what the target plan
// includes. `upgrade_url` is present only when the backend supplies one.
export interface UpgradeRequiredPayload {
error: 'upgrade_required'
required_plan: RequiredPlan
current_plan?: string
message: string
unlocks?: string
upgrade_url: string
pricing_url: string
upgrade_url?: string
}

// What each paid tier adds, phrased as the answer to "why would I pay for
// this". Kept in sync with form4api-web/app/lib/plans.ts, which is the
// single source of truth for plan contents.
// A neutral, factual description of what each plan includes. No prices: the
// site does not publish them. Kept in sync with form4api-web/app/lib/plans.ts,
// which is the single source of truth for plan contents.
const PLAN_UNLOCKS: Record<Exclude<RequiredPlan, 'higher'>, string> = {
starter:
'Starter ($19/mo) adds a commercial-use license, 7,500 requests/day, and 100 pages of query depth.',
'The Starter plan includes a commercial-use license, 7,500 requests/day, and 100 pages of query depth.',
pro:
'Pro ($49/mo) adds insider scorecards and career summaries, congressional trading data and convergence signals, ' +
'The Pro plan includes insider scorecards and career summaries, congressional trading data and convergence signals, ' +
'return and trade-size filters, unlimited query depth, and 50,000 requests/day.',
business:
'Business ($149/mo) adds cluster-buy signals and sentiment scores, 13F institutional holdings and managers, ' +
'The Business plan includes cluster-buy signals and sentiment scores, 13F institutional holdings and managers, ' +
'Form 144 notices, bulk CSV export, and 250,000 requests/day.',
enterprise:
'Enterprise ($499/mo) adds unlimited requests, unlimited webhooks, and Slack support with an SLA.',
'The Enterprise plan includes unlimited requests, unlimited webhooks, and Slack support with an SLA.',
}

const PRICING_URL = 'https://www.form4api.com/pricing'
const BILLING_URL = 'https://www.form4api.com/dashboard/billing'

export class Form4ApiError extends Error {
constructor(
message: string,
Expand Down Expand Up @@ -148,8 +141,8 @@ export class Form4ApiClient {
...(currentPlan ? { current_plan: currentPlan } : {}),
message,
...(requiredPlan !== 'higher' ? { unlocks: PLAN_UNLOCKS[requiredPlan] } : {}),
upgrade_url: upgradeUrl ?? BILLING_URL,
pricing_url: PRICING_URL,
// Only the backend's own link; omitted when it sends none/null.
...(upgradeUrl ? { upgrade_url: upgradeUrl } : {}),
}
// The structured payload is JSON-encoded into the error message so
// the MCP server's text-channel response carries the full shape.
Expand Down
4 changes: 2 additions & 2 deletions src/tools/research.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,13 +79,13 @@ export async function researchCompany(
const signals = signalsResult.status === 'fulfilled' ? signalsResult.value : null
const signalsUnavailable =
signalsResult.status === 'rejected'
? 'Cluster signals require Business plan. Upgrade at https://www.form4api.com/dashboard/billing'
? 'Cluster signals require the Business plan.'
: null

const sentiment = sentimentResult.status === 'fulfilled' ? sentimentResult.value : null
const sentimentUnavailable =
sentimentResult.status === 'rejected'
? 'Sentiment scores require Business plan. Upgrade at https://www.form4api.com/dashboard/billing'
? 'Sentiment scores require the Business plan.'
: null

// Compute summary from real fetched data only
Expand Down
2 changes: 1 addition & 1 deletion src/tools/verify-setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ export async function verifySetup(_input: VerifySetupInput): Promise<VerifySetup
status: 'fail',
detail: `Requires ${body.required_plan ?? 'higher'} plan; current plan is ${body.currentPlan ?? 'unknown'}`,
})
nextSteps.push('Upgrade your plan at https://www.form4api.com/dashboard/billing to access this tool')
nextSteps.push('This tool requires a higher plan than your current one; see your account dashboard at https://www.form4api.com/dashboard')
} else {
let errorText = res.statusText
try {
Expand Down
4 changes: 3 additions & 1 deletion test/client-errors.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,9 @@ async function run() {
p?.message === 'This endpoint requires the Business plan or higher. Your current plan is Free.',
'preserves the backend message verbatim',
)
assert(p?.pricing_url === 'https://www.form4api.com/pricing', 'includes the pricing URL')
assert(p?.pricing_url === undefined, 'has no pricing_url')
assert(p?.upgrade_url === undefined, 'omits upgrade_url when the backend sends none')
assert(!/\$|\/mo\b/.test(p?.unlocks ?? ''), 'unlocks carries no prices')
assert(JSON.parse(err.message).required_plan === 'business', 'message is the JSON payload for the LLM router')
}

Expand Down
Loading