@cyanheads/secedgar-mcp-server 0.15.9 → 0.15.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +33 -24
- package/CLAUDE.md +33 -24
- package/Dockerfile +95 -38
- package/README.md +7 -7
- package/changelog/0.15.x/0.15.10.md +39 -0
- package/dist/mcp-server/tools/definitions/company-search.tool.d.ts.map +1 -1
- package/dist/mcp-server/tools/definitions/company-search.tool.js +1 -4
- package/dist/mcp-server/tools/definitions/company-search.tool.js.map +1 -1
- package/dist/mcp-server/tools/definitions/compare-companies.tool.d.ts.map +1 -1
- package/dist/mcp-server/tools/definitions/compare-companies.tool.js +1 -2
- package/dist/mcp-server/tools/definitions/compare-companies.tool.js.map +1 -1
- package/dist/mcp-server/tools/definitions/dataframe-describe.tool.d.ts.map +1 -1
- package/dist/mcp-server/tools/definitions/dataframe-describe.tool.js +1 -3
- package/dist/mcp-server/tools/definitions/dataframe-describe.tool.js.map +1 -1
- package/dist/mcp-server/tools/definitions/dataframe-drop.tool.d.ts.map +1 -1
- package/dist/mcp-server/tools/definitions/dataframe-drop.tool.js +1 -3
- package/dist/mcp-server/tools/definitions/dataframe-drop.tool.js.map +1 -1
- package/dist/mcp-server/tools/definitions/dataframe-query.tool.d.ts.map +1 -1
- package/dist/mcp-server/tools/definitions/dataframe-query.tool.js +1 -3
- package/dist/mcp-server/tools/definitions/dataframe-query.tool.js.map +1 -1
- package/dist/mcp-server/tools/definitions/find-holders.tool.d.ts.map +1 -1
- package/dist/mcp-server/tools/definitions/find-holders.tool.js +2 -5
- package/dist/mcp-server/tools/definitions/find-holders.tool.js.map +1 -1
- package/dist/mcp-server/tools/definitions/get-beneficial-owners.tool.d.ts.map +1 -1
- package/dist/mcp-server/tools/definitions/get-beneficial-owners.tool.js +2 -8
- package/dist/mcp-server/tools/definitions/get-beneficial-owners.tool.js.map +1 -1
- package/dist/mcp-server/tools/definitions/get-filing.tool.d.ts.map +1 -1
- package/dist/mcp-server/tools/definitions/get-filing.tool.js +0 -2
- package/dist/mcp-server/tools/definitions/get-filing.tool.js.map +1 -1
- package/dist/mcp-server/tools/definitions/get-financials.tool.d.ts.map +1 -1
- package/dist/mcp-server/tools/definitions/get-financials.tool.js +3 -11
- package/dist/mcp-server/tools/definitions/get-financials.tool.js.map +1 -1
- package/dist/mcp-server/tools/definitions/get-fund-holdings.tool.d.ts.map +1 -1
- package/dist/mcp-server/tools/definitions/get-fund-holdings.tool.js +7 -17
- package/dist/mcp-server/tools/definitions/get-fund-holdings.tool.js.map +1 -1
- package/dist/mcp-server/tools/definitions/get-insider-transactions.tool.d.ts.map +1 -1
- package/dist/mcp-server/tools/definitions/get-insider-transactions.tool.js +2 -6
- package/dist/mcp-server/tools/definitions/get-insider-transactions.tool.js.map +1 -1
- package/dist/mcp-server/tools/definitions/get-institutional-holdings.tool.d.ts.map +1 -1
- package/dist/mcp-server/tools/definitions/get-institutional-holdings.tool.js +8 -14
- package/dist/mcp-server/tools/definitions/get-institutional-holdings.tool.js.map +1 -1
- package/dist/mcp-server/tools/definitions/get-material-events.tool.d.ts.map +1 -1
- package/dist/mcp-server/tools/definitions/get-material-events.tool.js +3 -11
- package/dist/mcp-server/tools/definitions/get-material-events.tool.js.map +1 -1
- package/dist/mcp-server/tools/definitions/get-snapshot.tool.d.ts.map +1 -1
- package/dist/mcp-server/tools/definitions/get-snapshot.tool.js +3 -11
- package/dist/mcp-server/tools/definitions/get-snapshot.tool.js.map +1 -1
- package/dist/mcp-server/tools/definitions/search-filings.tool.d.ts.map +1 -1
- package/dist/mcp-server/tools/definitions/search-filings.tool.js +3 -3
- package/dist/mcp-server/tools/definitions/search-filings.tool.js.map +1 -1
- package/dist/services/canvas-bridge/canvas-bridge.js +2 -2
- package/dist/services/canvas-bridge/canvas-bridge.js.map +1 -1
- package/package.json +10 -9
- package/server.json +11 -5
package/AGENTS.md
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
# Agent Protocol
|
|
2
2
|
|
|
3
3
|
**Server:** secedgar-mcp-server
|
|
4
|
-
**Version:** 0.15.
|
|
5
|
-
**Framework:** [@cyanheads/mcp-ts-core](https://www.npmjs.com/package/@cyanheads/mcp-ts-core) `^0.13.
|
|
4
|
+
**Version:** 0.15.10
|
|
5
|
+
**Framework:** [@cyanheads/mcp-ts-core](https://www.npmjs.com/package/@cyanheads/mcp-ts-core) `^0.13.11`
|
|
6
6
|
**Engines:** Bun ≥1.4.0, Node ≥24.0.0
|
|
7
|
+
**MCP SDK:** `@modelcontextprotocol/server` ^2.2.0
|
|
8
|
+
**Zod:** ^4.6.5
|
|
7
9
|
|
|
8
10
|
Query SEC EDGAR filings, XBRL financials, and company data through MCP. Read-only, no API keys required. Full design: `docs/sec-edgar-mcp-design.md`.
|
|
9
11
|
|
|
@@ -18,6 +20,7 @@ Query SEC EDGAR filings, XBRL financials, and company data through MCP. Read-onl
|
|
|
18
20
|
- **Use `ctx.state`** for tenant-scoped storage. Never access persistence directly.
|
|
19
21
|
- **Ask for missing input** with `return ctx.requestInput(...)`, reading the answers back through `ctx.inputs` when the handler is re-entered. Never await a caller mid-handler.
|
|
20
22
|
- **Secrets in env vars only** — never hardcoded.
|
|
23
|
+
- **Cut noise.** Add only what earns its place: no speculative generality, no guards for states the framework already prevents (Zod-validated params, classified errors), no abstraction until a third caller proves it, no option nothing sets.
|
|
21
24
|
- **Close the loop on issues.** When implementing work tracked by a GitHub issue, comment on the issue with what landed and close it. Do both — a comment without a close leaves stale issues open; a close without a comment leaves no record of what shipped. The comment is for future readers — state the concrete changes, not the conversation that produced them.
|
|
22
25
|
|
|
23
26
|
---
|
|
@@ -117,7 +120,7 @@ Tailor suggestions to what's actually missing or stale — don't recite the full
|
|
|
117
120
|
| `EDGAR_RATE_LIMIT_COOLDOWN_SECONDS` | No | `600` | Seconds to stop sending to SEC after a 429. Calls are refused locally for this long, then the first one goes out alone as a probe. SEC lifts its block only after ten quiet minutes, so a shorter value just probes into it. |
|
|
118
121
|
| `EDGAR_TICKER_CACHE_TTL` | No | `3600` | Seconds to cache the ticker index (company_tickers.json + company_tickers_mf.json). A failed fund-file load is retried after 60 s (or the rate-limit cool-down) instead of standing for the TTL. |
|
|
119
122
|
| `EDGAR_DATASET_TTL_SECONDS` | No | `86400` | Per-table TTL for canvas-registered dataframes. Sliding window touched on every dataframe op. |
|
|
120
|
-
| `EDGAR_DATAFRAME_DROP_ENABLED` | No | `false` | Set to `true` to expose `secedgar_dataframe_drop`. TTL handles cleanup otherwise. Off, the tool is registered through `disabledTool()`: absent from `tools/list` and uncallable, but rendered on the HTTP landing page in a `disabled` group carrying the reason and `EDGAR_DATAFRAME_DROP_ENABLED=true`, so an operator can see the capability exists. The list `createApp()` receives — and `buildServerManifest()`'s `definitionCounts.tools` — is 17 either way; `/.well-known/mcp.json`
|
|
123
|
+
| `EDGAR_DATAFRAME_DROP_ENABLED` | No | `false` | Set to `true` to expose `secedgar_dataframe_drop`. TTL handles cleanup otherwise. Off, the tool is registered through `disabledTool()`: absent from `tools/list` and uncallable, but rendered on the HTTP landing page in a `disabled` group carrying the reason and `EDGAR_DATAFRAME_DROP_ENABLED=true`, so an operator can see the capability exists. The list `createApp()` receives — and `buildServerManifest()`'s `definitionCounts.tools` — is 17 either way; `/.well-known/mcp.json` carries no per-tool definitions at all, only capability flags, so nothing changes there (#103). |
|
|
121
124
|
| `EDGAR_MIRROR_ENABLED` | No | `false` | Enable the local SQLite mirror of company_tickers + XBRL company-facts. Node/Bun only (skipped on Workers). Bootstrap once with `bun run mirror:init`. |
|
|
122
125
|
| `EDGAR_MIRROR_PATH` | No | `./data/edgar-mirror` | Directory holding the mirror SQLite databases (tickers + companyfacts). |
|
|
123
126
|
| `EDGAR_MIRROR_REFRESH_CRON` | No | — | In-process nightly refresh cron (HTTP transport only). Recommended `0 9 * * *`. Omit to refresh out-of-band via `bun run mirror:refresh`. |
|
|
@@ -156,13 +159,14 @@ Handlers receive a unified `ctx` object. Key properties:
|
|
|
156
159
|
| Property | Description |
|
|
157
160
|
|:---------|:------------|
|
|
158
161
|
| `ctx.log` | Request-scoped logger — `.debug()`, `.info()`, `.notice()`, `.warning()`, `.error()`. Auto-correlates requestId, traceId, tenantId. Dual-sink: Pino **and** `notifications/message` to the client, so treat it as client-visible. |
|
|
159
|
-
| `ctx.state` | Tenant-scoped KV — `.get(key)`, `.set(key, value, { ttl? })`, `.delete(key)`, `.getMany(keys)`, `.list(prefix, { cursor, limit })`. Accepts any serializable value. |
|
|
162
|
+
| `ctx.state` | Tenant-scoped KV — `.get(key)`, `.set(key, value, { ttl? })`, `.delete(key)`, `.getMany(keys)`, `.list(prefix, { cursor, limit })`. Accepts any JSON-serializable value; reads return its JSON form (a `Date` comes back as an ISO string). |
|
|
160
163
|
| `ctx.requestInput` | Suspend and ask the caller for more input — `return ctx.requestInput({ inputRequests: { key: inputRequired.elicit({ message, requestedSchema }) } })`. Never returns; the handler is re-entered with the answers. Always present. |
|
|
161
|
-
| `ctx.inputs` |
|
|
164
|
+
| `ctx.inputs` | The request's responses — `.accepted(key, schema)`, `.view(key)`, `.state()`, `.dropped` — limited to what the client declared (`elicitation` and its form/url modes, `sampling`, `roots`). Client-supplied: a consent gate trusts only a `ctx.state` record it stored when it asked, bound to the operation, caller, and target (see the `api-context` skill). |
|
|
165
|
+
| `ctx.clientCapabilities` | What the client declared for this request, `undefined` when no view exists. Decides whether to ask for optional context (e.g. roots); never a reason to skip a consent prompt. |
|
|
162
166
|
| `ctx.enrich` | Success-path agent context (empty-result notices, query echo, pagination totals) — `ctx.enrich(...)` or `.notice()` / `.total()` / `.echo()` / `.truncated()`. Reaches `structuredContent` and `content[]`; lands only when the definition declares an `enrichment` block (no-op otherwise). |
|
|
163
167
|
| `ctx.content` | Non-text content blocks — `.image(data, mimeType)`, `.audio(data, mimeType)`, or `ctx.content(block)` for a raw block. Prepended to `content[]` after `format()`; never enters `structuredContent`. |
|
|
164
168
|
| `ctx.signal` | `AbortSignal` for cancellation. |
|
|
165
|
-
| `ctx.requestId` |
|
|
169
|
+
| `ctx.requestId` | Request ID — the one every log record of the call carries and its error envelope returns as `data.requestId`. |
|
|
166
170
|
| `ctx.tenantId` | Tenant ID from JWT; `'default'` for stdio or HTTP with auth off. |
|
|
167
171
|
|
|
168
172
|
---
|
|
@@ -171,7 +175,7 @@ Handlers receive a unified `ctx` object. Key properties:
|
|
|
171
175
|
|
|
172
176
|
Handlers throw — the framework catches, classifies, and formats.
|
|
173
177
|
|
|
174
|
-
**Recommended: typed error contract.** Declare `errors: [{ reason, code, when, recovery, retryable?, severity?, thrownBy? }]` on `tool()`. The handler then receives `ctx.fail(reason, msg?, data?)` typed against the reason union, and `data.reason` is auto-populated for observability. The `recovery` field is required (≥5 words, lint-validated).
|
|
178
|
+
**Recommended: typed error contract.** Declare `errors: [{ reason, code, when, recovery, retryable?, severity?, thrownBy? }]` on `tool()`. The handler then receives `ctx.fail(reason, msg?, data?)` typed against the reason union, and `data.reason` is auto-populated for observability. The `recovery` field is required (≥5 words, lint-validated). The framework puts it on the wire whenever a failure carrying that `reason` arrives without a hint — a bare `ctx.fail('reason')` or a service throw with `data: { reason }` — as `data.recovery.hint`, mirrored into `content[]` text unless the message already contains it verbatim; override with an explicit `{ recovery: { hint } }` when runtime context matters. The fill happens at the handler boundary, so a test asserting the hint on a direct `definition.handler(...)` throw either keeps the throw site's `...ctx.recoveryFor('reason')` or asserts through `runToolContract`. Every error envelope also carries `data.requestId`, the id the server's log records for that call carry. Mark an entry raised below the handler — a module-level helper, a service, the canvas bridge — with `thrownBy: 'service'` so `error-contract-unthrown` skips it; lint-only metadata, nothing at runtime reads it. Baseline codes (`InternalError`, `ServiceUnavailable`, `Timeout`, `ValidationError`, `SerializationError`, `RequestCancelled`) bubble freely without declaration. **All EDGAR tools that have known failure modes use this pattern.**
|
|
175
179
|
|
|
176
180
|
```ts
|
|
177
181
|
errors: [
|
|
@@ -180,13 +184,11 @@ errors: [
|
|
|
180
184
|
recovery: 'Use a ticker symbol or 10-digit CIK number for an exact match.' },
|
|
181
185
|
],
|
|
182
186
|
async handler(input, ctx) {
|
|
183
|
-
if (!match) throw ctx.fail('company_not_found', `Company '${input.company}' not found
|
|
184
|
-
...ctx.recoveryFor('company_not_found'),
|
|
185
|
-
});
|
|
187
|
+
if (!match) throw ctx.fail('company_not_found', `Company '${input.company}' not found.`);
|
|
186
188
|
}
|
|
187
189
|
```
|
|
188
190
|
|
|
189
|
-
**Service-layer pattern (no `ctx`).** Throw an `McpError` with `data: { reason, recovery: { hint } }`, and mark the tool's matching `errors[]` entry `thrownBy: 'service'`. The auto-classifier preserves `data` on the wire so clients see the same `error.data.reason` they'd see from `ctx.fail`. The tool error text closes with a term line rendering only what `data` carries — `(reason <reason>)` for a reason alone, `· retryable` / `· not retryable` appended when `data.retryable` is a boolean — so tests assert that `content[0].text` contains the diagnostic rather than pinning it exactly. A contract entry's `retryable` reaches `data` only through `ctx.fail`; a service throw sets `data.retryable` itself (#122).
|
|
191
|
+
**Service-layer pattern (no `ctx`).** Throw an `McpError` with `data: { reason, recovery: { hint } }`, and mark the tool's matching `errors[]` entry `thrownBy: 'service'`. The auto-classifier preserves `data` on the wire so clients see the same `error.data.reason` they'd see from `ctx.fail`. The tool error text closes with a term line rendering only what `data` carries — `(reason <reason>)` for a reason alone, `· retryable` / `· not retryable` appended when `data.retryable` is a boolean, then `· request <id>` — so tests assert that `content[0].text` contains the diagnostic rather than pinning it exactly. A contract entry's `retryable` reaches `data` only through `ctx.fail`; a service throw sets `data.retryable` itself (#122).
|
|
190
192
|
|
|
191
193
|
**Fallback for ad-hoc throws** (no contract entry fits, prototype code): use error factories or plain `Error`.
|
|
192
194
|
|
|
@@ -318,29 +320,34 @@ Available skills:
|
|
|
318
320
|
| `api-utils` | Formatting, parsing, security, pagination, scheduling, telemetry helpers |
|
|
319
321
|
| `api-workers` | Cloudflare Workers runtime |
|
|
320
322
|
|
|
323
|
+
**Chaining skills into pipelines.** When the user wants a multi-phase effort — build this server out, QA-and-fix the surface, update-and-ship — *and you can spawn sub-agents*, `framework-skills/orchestrations/SKILL.md` sequences the task skills above into a gated pipeline with verification at each step. Read it to drive the run. Optional: skip it if you can't orchestrate sub-agents, and ignore it entirely if you were *spawned* as one — you've already been scoped to a single phase.
|
|
324
|
+
|
|
321
325
|
When you complete a skill's checklist, check the boxes and add a completion timestamp at the end (e.g., `Completed: 2026-03-11`).
|
|
322
326
|
|
|
323
327
|
---
|
|
324
328
|
|
|
325
329
|
## Commands
|
|
326
330
|
|
|
331
|
+
**Runtime:** Scripts use Bun's native TypeScript execution — `bun run <cmd>` is the standard invocation. `npm run <cmd>` also works (npm delegates to bun).
|
|
332
|
+
|
|
327
333
|
| Command | Purpose |
|
|
328
334
|
|:--------|:--------|
|
|
329
335
|
| `bun run build` | Compile TypeScript |
|
|
330
336
|
| `bun run rebuild` | Clean + build |
|
|
331
337
|
| `bun run clean` | Remove build artifacts |
|
|
332
338
|
| `bun run devcheck` | Lint + format + typecheck + security + changelog sync |
|
|
333
|
-
| `bun run audit:fix` |
|
|
334
|
-
| `bun run audit:refresh` | Delete `bun.lock
|
|
339
|
+
| `bun run audit:fix` | `bun audit fix` — upgrade vulnerable packages to the lowest safe version within existing ranges (`--dry-run` previews, `--latest` rewrites ranges). First response when `devcheck` flags a transitive advisory; then `bun update <name>`, then `bun dedupe` |
|
|
340
|
+
| `bun run audit:refresh` | Delete `bun.lock` and reinstall. Last resort after `audit:fix`, `bun update <name>`, and `bun dedupe` — re-resolves every ranged dep (the framework pin included) and rewrites the lockfile as `lockfileVersion: 2` |
|
|
335
341
|
| `bun run tree` | Generate directory structure doc |
|
|
336
|
-
| `bun run format` | Auto-fix formatting |
|
|
337
|
-
| `bun run
|
|
338
|
-
| `bun run lint:
|
|
342
|
+
| `bun run format` | Auto-fix formatting (safe fixes only) |
|
|
343
|
+
| `bun run format:unsafe` | Also apply Biome's unsafe autofixes — review the diff; they can change behavior |
|
|
344
|
+
| `bun run lint:mcp` | Run the MCP definition linter standalone (rule catalog: `api-linter` skill) |
|
|
345
|
+
| `bun run lint:packaging` | Packaging surface checks — `server.json`/`manifest.json` env-var parity (run by devcheck) |
|
|
339
346
|
| `bun run list-skills` | Print an index of available skills from `framework-skills/` |
|
|
340
347
|
| `bun run changelog:build` | Regenerate `CHANGELOG.md` from `changelog/*.md` |
|
|
341
348
|
| `bun run changelog:check` | Verify `CHANGELOG.md` is in sync with `changelog/` (used by devcheck) |
|
|
342
349
|
| `bun run bundle` | Build, pack, and clean a `.mcpb` for one-click Claude Desktop install |
|
|
343
|
-
| `bun run test` | Run tests |
|
|
350
|
+
| `bun run test` | Run tests (Vitest — use `bun run test`, not `bun test`) |
|
|
344
351
|
| `bun run mirror:init` | Bootstrap the local mirror (download company_tickers + companyfacts.zip). Out-of-band; resumable. |
|
|
345
352
|
| `bun run mirror:refresh` | Incrementally refresh the local mirror from the SEC bulk files. |
|
|
346
353
|
| `bun run mirror:verify` | Print mirror sync status + run sample reads. |
|
|
@@ -354,9 +361,11 @@ When you complete a skill's checklist, check the boxes and add a completion time
|
|
|
354
361
|
|
|
355
362
|
## Bundling
|
|
356
363
|
|
|
357
|
-
`bun run bundle` produces a `.mcpb` extension bundle for one-click install in Claude Desktop. The pack step is followed by `scripts/clean-mcpb.ts`, which prunes dev dependencies (`mcpb clean`) and strips dependency-shipped agent docs (`
|
|
364
|
+
`bun run bundle` produces a `.mcpb` extension bundle for one-click install in Claude Desktop. The pack step is followed by `scripts/clean-mcpb.ts`, which prunes dev dependencies (`mcpb clean`) and strips two classes of `node_modules/**` content that root-anchored `.mcpbignore` patterns cannot reach: dependency-shipped agent docs (`framework-skills/`, `skills/`, `.claude/`, `.agents/`, `SKILL.md`) and platform-specific native bindings, which would otherwise lock the bundle to the platform it was packed on. The bundle therefore ships without the DuckDB native — the framework lazy-loads `@duckdb/node-api`, so canvas tools report an actionable install hint and every other tool works normally (the data tools answer inline without registering a dataframe). MCPB is stdio-only — HTTP deployments are unaffected. Delete `manifest.json` and `.mcpbignore` to opt out; `lint:packaging` skips cleanly.
|
|
365
|
+
|
|
366
|
+
**Adding an env var requires both files:** `server.json` (registry discovery, `environmentVariables[]`) and `manifest.json` (bundle install UX, `mcp_config.env` + `user_config`). `lint:packaging` (run by `devcheck`) verifies the env var names match, that every `user_config` option is wired into `mcp_config.env` as `"X": "${user_config.X}"` (the host substitutes nothing else — `"${X}"` reaches the server as that literal string), and that an optional string option carries `"default": ""`.
|
|
358
367
|
|
|
359
|
-
**
|
|
368
|
+
**README install badges** (Claude Desktop `.mcpb`, Cursor, VS Code) and the `base64` / `encodeURIComponent` config-generation commands are ship-time concerns — run the `polish-docs-meta` skill, which carries the badge format, layout, and generation snippets in `framework-skills/polish-docs-meta/references/readme.md`.
|
|
360
369
|
|
|
361
370
|
---
|
|
362
371
|
|
|
@@ -367,7 +376,7 @@ Directory-based. Source of truth is `changelog/<major.minor>.x/<version>.md` —
|
|
|
367
376
|
**To add a release entry:**
|
|
368
377
|
|
|
369
378
|
1. Author `changelog/<major.minor>.x/<version>.md` using `changelog/template.md` as a reference.
|
|
370
|
-
2. Add YAML frontmatter: `summary` (≤350 chars, no markdown), optional `breaking: true` flags breaking changes (`· ⚠️ Breaking` badge), optional `security: true` flags security
|
|
379
|
+
2. Add YAML frontmatter: `summary` (≤350 chars, no markdown), optional `breaking: true` flags breaking changes (`· ⚠️ Breaking` badge), optional `security: true` flags a security fix in this server's own source code (`· 🛡️ Security` badge, pairs with a `## Security` body section) — never a routine dependency or transitive CVE bump, which goes under `## Dependencies`.
|
|
371
380
|
3. Set the H1 heading to `# <version> — YYYY-MM-DD`.
|
|
372
381
|
4. Run `bun run changelog:build` to regenerate `CHANGELOG.md`.
|
|
373
382
|
|
|
@@ -415,13 +424,13 @@ import { getEdgarApiService } from '@/services/edgar/edgar-api-service.js';
|
|
|
415
424
|
- [ ] Optional nested objects: handler guards for empty inner values from form-based clients (`if (input.obj?.field && ...)`, not just `if (input.obj)`). When regex/length constraints matter, use `z.union([z.literal(''), z.string().regex(...).describe(...)])` — literal variants are exempt from `describe-on-fields`.
|
|
416
425
|
- [ ] JSDoc `@fileoverview` + `@module` on every file
|
|
417
426
|
- [ ] `ctx.log` for logging, `ctx.state` for storage
|
|
418
|
-
- [ ] Handlers throw on failure — typed `errors[]` contract + `ctx.fail(reason,
|
|
427
|
+
- [ ] Handlers throw on failure — typed `errors[]` contract + `ctx.fail(reason, …)` when failure modes are known; factories or plain `Error` for ad-hoc throws. No try/catch.
|
|
419
428
|
- [ ] Tool error contracts include `recovery` strings (≥5 words)
|
|
420
429
|
- [ ] `format()` renders all data the LLM needs — Claude Code reads `structuredContent`, Claude Desktop reads `content[]`; both must carry the same data
|
|
421
430
|
- [ ] EDGAR upstream sparsity: schemas reflect real nullability; `format()` preserves uncertainty (don't fabricate facts from missing XBRL fields); tests cover at least one sparse payload
|
|
422
431
|
- [ ] Registered in `createApp()` arrays (directly or via barrel exports)
|
|
423
432
|
- [ ] Tests use `createMockContext({ errors: tool.errors })` from `@cyanheads/mcp-ts-core/testing` for tools with declared contracts
|
|
424
|
-
- [ ] `.codex-plugin/plugin.json` populated — `name`, `version`, `description`, `repository`, `license` from `package.json`; `interface.displayName` =
|
|
425
|
-
- [ ] `.codex-plugin/mcp.json` updated — server name key
|
|
426
|
-
- [ ] `.claude-plugin/plugin.json` populated — `name`, `version`, `description`, `repository`, `license` from `package.json`; inline `mcpServers` entry
|
|
433
|
+
- [ ] `.codex-plugin/plugin.json` populated — `name`, `version`, `description`, `repository`, `license` from `package.json`; `interface.displayName` = the unscoped repo name (never the npm scope — `lint:packaging` enforces this); `interface.shortDescription` from `package.json` description
|
|
434
|
+
- [ ] `.codex-plugin/mcp.json` updated — server name key is the unscoped repo name; every user-supplied variable (API key, contact email, instance URL) is listed in `env_vars` so Codex forwards it from the user's environment. Never write `"KEY": ""` into `env` — an empty value replaces the user's exported key and is read as unset
|
|
435
|
+
- [ ] `.claude-plugin/plugin.json` populated — `name`, `version`, `description`, `author`, `repository`, `license`, `keywords` from `package.json`; inline `mcpServers` entry keyed by the unscoped repo name. Every user-supplied variable is declared under `userConfig` (`type`, `title`, `description`; `sensitive: true` for keys and tokens; `required: true` or `default: ""`) and referenced from `env` as `"KEY": "${user_config.<option>}"` — mirror the `user_config` block in `manifest.json`. Never write `"KEY": ""` into `env`
|
|
427
436
|
- [ ] `bun run devcheck` passes
|
package/CLAUDE.md
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
# Agent Protocol
|
|
2
2
|
|
|
3
3
|
**Server:** secedgar-mcp-server
|
|
4
|
-
**Version:** 0.15.
|
|
5
|
-
**Framework:** [@cyanheads/mcp-ts-core](https://www.npmjs.com/package/@cyanheads/mcp-ts-core) `^0.13.
|
|
4
|
+
**Version:** 0.15.10
|
|
5
|
+
**Framework:** [@cyanheads/mcp-ts-core](https://www.npmjs.com/package/@cyanheads/mcp-ts-core) `^0.13.11`
|
|
6
6
|
**Engines:** Bun ≥1.4.0, Node ≥24.0.0
|
|
7
|
+
**MCP SDK:** `@modelcontextprotocol/server` ^2.2.0
|
|
8
|
+
**Zod:** ^4.6.5
|
|
7
9
|
|
|
8
10
|
Query SEC EDGAR filings, XBRL financials, and company data through MCP. Read-only, no API keys required. Full design: `docs/sec-edgar-mcp-design.md`.
|
|
9
11
|
|
|
@@ -18,6 +20,7 @@ Query SEC EDGAR filings, XBRL financials, and company data through MCP. Read-onl
|
|
|
18
20
|
- **Use `ctx.state`** for tenant-scoped storage. Never access persistence directly.
|
|
19
21
|
- **Ask for missing input** with `return ctx.requestInput(...)`, reading the answers back through `ctx.inputs` when the handler is re-entered. Never await a caller mid-handler.
|
|
20
22
|
- **Secrets in env vars only** — never hardcoded.
|
|
23
|
+
- **Cut noise.** Add only what earns its place: no speculative generality, no guards for states the framework already prevents (Zod-validated params, classified errors), no abstraction until a third caller proves it, no option nothing sets.
|
|
21
24
|
- **Close the loop on issues.** When implementing work tracked by a GitHub issue, comment on the issue with what landed and close it. Do both — a comment without a close leaves stale issues open; a close without a comment leaves no record of what shipped. The comment is for future readers — state the concrete changes, not the conversation that produced them.
|
|
22
25
|
|
|
23
26
|
---
|
|
@@ -117,7 +120,7 @@ Tailor suggestions to what's actually missing or stale — don't recite the full
|
|
|
117
120
|
| `EDGAR_RATE_LIMIT_COOLDOWN_SECONDS` | No | `600` | Seconds to stop sending to SEC after a 429. Calls are refused locally for this long, then the first one goes out alone as a probe. SEC lifts its block only after ten quiet minutes, so a shorter value just probes into it. |
|
|
118
121
|
| `EDGAR_TICKER_CACHE_TTL` | No | `3600` | Seconds to cache the ticker index (company_tickers.json + company_tickers_mf.json). A failed fund-file load is retried after 60 s (or the rate-limit cool-down) instead of standing for the TTL. |
|
|
119
122
|
| `EDGAR_DATASET_TTL_SECONDS` | No | `86400` | Per-table TTL for canvas-registered dataframes. Sliding window touched on every dataframe op. |
|
|
120
|
-
| `EDGAR_DATAFRAME_DROP_ENABLED` | No | `false` | Set to `true` to expose `secedgar_dataframe_drop`. TTL handles cleanup otherwise. Off, the tool is registered through `disabledTool()`: absent from `tools/list` and uncallable, but rendered on the HTTP landing page in a `disabled` group carrying the reason and `EDGAR_DATAFRAME_DROP_ENABLED=true`, so an operator can see the capability exists. The list `createApp()` receives — and `buildServerManifest()`'s `definitionCounts.tools` — is 17 either way; `/.well-known/mcp.json`
|
|
123
|
+
| `EDGAR_DATAFRAME_DROP_ENABLED` | No | `false` | Set to `true` to expose `secedgar_dataframe_drop`. TTL handles cleanup otherwise. Off, the tool is registered through `disabledTool()`: absent from `tools/list` and uncallable, but rendered on the HTTP landing page in a `disabled` group carrying the reason and `EDGAR_DATAFRAME_DROP_ENABLED=true`, so an operator can see the capability exists. The list `createApp()` receives — and `buildServerManifest()`'s `definitionCounts.tools` — is 17 either way; `/.well-known/mcp.json` carries no per-tool definitions at all, only capability flags, so nothing changes there (#103). |
|
|
121
124
|
| `EDGAR_MIRROR_ENABLED` | No | `false` | Enable the local SQLite mirror of company_tickers + XBRL company-facts. Node/Bun only (skipped on Workers). Bootstrap once with `bun run mirror:init`. |
|
|
122
125
|
| `EDGAR_MIRROR_PATH` | No | `./data/edgar-mirror` | Directory holding the mirror SQLite databases (tickers + companyfacts). |
|
|
123
126
|
| `EDGAR_MIRROR_REFRESH_CRON` | No | — | In-process nightly refresh cron (HTTP transport only). Recommended `0 9 * * *`. Omit to refresh out-of-band via `bun run mirror:refresh`. |
|
|
@@ -156,13 +159,14 @@ Handlers receive a unified `ctx` object. Key properties:
|
|
|
156
159
|
| Property | Description |
|
|
157
160
|
|:---------|:------------|
|
|
158
161
|
| `ctx.log` | Request-scoped logger — `.debug()`, `.info()`, `.notice()`, `.warning()`, `.error()`. Auto-correlates requestId, traceId, tenantId. Dual-sink: Pino **and** `notifications/message` to the client, so treat it as client-visible. |
|
|
159
|
-
| `ctx.state` | Tenant-scoped KV — `.get(key)`, `.set(key, value, { ttl? })`, `.delete(key)`, `.getMany(keys)`, `.list(prefix, { cursor, limit })`. Accepts any serializable value. |
|
|
162
|
+
| `ctx.state` | Tenant-scoped KV — `.get(key)`, `.set(key, value, { ttl? })`, `.delete(key)`, `.getMany(keys)`, `.list(prefix, { cursor, limit })`. Accepts any JSON-serializable value; reads return its JSON form (a `Date` comes back as an ISO string). |
|
|
160
163
|
| `ctx.requestInput` | Suspend and ask the caller for more input — `return ctx.requestInput({ inputRequests: { key: inputRequired.elicit({ message, requestedSchema }) } })`. Never returns; the handler is re-entered with the answers. Always present. |
|
|
161
|
-
| `ctx.inputs` |
|
|
164
|
+
| `ctx.inputs` | The request's responses — `.accepted(key, schema)`, `.view(key)`, `.state()`, `.dropped` — limited to what the client declared (`elicitation` and its form/url modes, `sampling`, `roots`). Client-supplied: a consent gate trusts only a `ctx.state` record it stored when it asked, bound to the operation, caller, and target (see the `api-context` skill). |
|
|
165
|
+
| `ctx.clientCapabilities` | What the client declared for this request, `undefined` when no view exists. Decides whether to ask for optional context (e.g. roots); never a reason to skip a consent prompt. |
|
|
162
166
|
| `ctx.enrich` | Success-path agent context (empty-result notices, query echo, pagination totals) — `ctx.enrich(...)` or `.notice()` / `.total()` / `.echo()` / `.truncated()`. Reaches `structuredContent` and `content[]`; lands only when the definition declares an `enrichment` block (no-op otherwise). |
|
|
163
167
|
| `ctx.content` | Non-text content blocks — `.image(data, mimeType)`, `.audio(data, mimeType)`, or `ctx.content(block)` for a raw block. Prepended to `content[]` after `format()`; never enters `structuredContent`. |
|
|
164
168
|
| `ctx.signal` | `AbortSignal` for cancellation. |
|
|
165
|
-
| `ctx.requestId` |
|
|
169
|
+
| `ctx.requestId` | Request ID — the one every log record of the call carries and its error envelope returns as `data.requestId`. |
|
|
166
170
|
| `ctx.tenantId` | Tenant ID from JWT; `'default'` for stdio or HTTP with auth off. |
|
|
167
171
|
|
|
168
172
|
---
|
|
@@ -171,7 +175,7 @@ Handlers receive a unified `ctx` object. Key properties:
|
|
|
171
175
|
|
|
172
176
|
Handlers throw — the framework catches, classifies, and formats.
|
|
173
177
|
|
|
174
|
-
**Recommended: typed error contract.** Declare `errors: [{ reason, code, when, recovery, retryable?, severity?, thrownBy? }]` on `tool()`. The handler then receives `ctx.fail(reason, msg?, data?)` typed against the reason union, and `data.reason` is auto-populated for observability. The `recovery` field is required (≥5 words, lint-validated).
|
|
178
|
+
**Recommended: typed error contract.** Declare `errors: [{ reason, code, when, recovery, retryable?, severity?, thrownBy? }]` on `tool()`. The handler then receives `ctx.fail(reason, msg?, data?)` typed against the reason union, and `data.reason` is auto-populated for observability. The `recovery` field is required (≥5 words, lint-validated). The framework puts it on the wire whenever a failure carrying that `reason` arrives without a hint — a bare `ctx.fail('reason')` or a service throw with `data: { reason }` — as `data.recovery.hint`, mirrored into `content[]` text unless the message already contains it verbatim; override with an explicit `{ recovery: { hint } }` when runtime context matters. The fill happens at the handler boundary, so a test asserting the hint on a direct `definition.handler(...)` throw either keeps the throw site's `...ctx.recoveryFor('reason')` or asserts through `runToolContract`. Every error envelope also carries `data.requestId`, the id the server's log records for that call carry. Mark an entry raised below the handler — a module-level helper, a service, the canvas bridge — with `thrownBy: 'service'` so `error-contract-unthrown` skips it; lint-only metadata, nothing at runtime reads it. Baseline codes (`InternalError`, `ServiceUnavailable`, `Timeout`, `ValidationError`, `SerializationError`, `RequestCancelled`) bubble freely without declaration. **All EDGAR tools that have known failure modes use this pattern.**
|
|
175
179
|
|
|
176
180
|
```ts
|
|
177
181
|
errors: [
|
|
@@ -180,13 +184,11 @@ errors: [
|
|
|
180
184
|
recovery: 'Use a ticker symbol or 10-digit CIK number for an exact match.' },
|
|
181
185
|
],
|
|
182
186
|
async handler(input, ctx) {
|
|
183
|
-
if (!match) throw ctx.fail('company_not_found', `Company '${input.company}' not found
|
|
184
|
-
...ctx.recoveryFor('company_not_found'),
|
|
185
|
-
});
|
|
187
|
+
if (!match) throw ctx.fail('company_not_found', `Company '${input.company}' not found.`);
|
|
186
188
|
}
|
|
187
189
|
```
|
|
188
190
|
|
|
189
|
-
**Service-layer pattern (no `ctx`).** Throw an `McpError` with `data: { reason, recovery: { hint } }`, and mark the tool's matching `errors[]` entry `thrownBy: 'service'`. The auto-classifier preserves `data` on the wire so clients see the same `error.data.reason` they'd see from `ctx.fail`. The tool error text closes with a term line rendering only what `data` carries — `(reason <reason>)` for a reason alone, `· retryable` / `· not retryable` appended when `data.retryable` is a boolean — so tests assert that `content[0].text` contains the diagnostic rather than pinning it exactly. A contract entry's `retryable` reaches `data` only through `ctx.fail`; a service throw sets `data.retryable` itself (#122).
|
|
191
|
+
**Service-layer pattern (no `ctx`).** Throw an `McpError` with `data: { reason, recovery: { hint } }`, and mark the tool's matching `errors[]` entry `thrownBy: 'service'`. The auto-classifier preserves `data` on the wire so clients see the same `error.data.reason` they'd see from `ctx.fail`. The tool error text closes with a term line rendering only what `data` carries — `(reason <reason>)` for a reason alone, `· retryable` / `· not retryable` appended when `data.retryable` is a boolean, then `· request <id>` — so tests assert that `content[0].text` contains the diagnostic rather than pinning it exactly. A contract entry's `retryable` reaches `data` only through `ctx.fail`; a service throw sets `data.retryable` itself (#122).
|
|
190
192
|
|
|
191
193
|
**Fallback for ad-hoc throws** (no contract entry fits, prototype code): use error factories or plain `Error`.
|
|
192
194
|
|
|
@@ -318,29 +320,34 @@ Available skills:
|
|
|
318
320
|
| `api-utils` | Formatting, parsing, security, pagination, scheduling, telemetry helpers |
|
|
319
321
|
| `api-workers` | Cloudflare Workers runtime |
|
|
320
322
|
|
|
323
|
+
**Chaining skills into pipelines.** When the user wants a multi-phase effort — build this server out, QA-and-fix the surface, update-and-ship — *and you can spawn sub-agents*, `framework-skills/orchestrations/SKILL.md` sequences the task skills above into a gated pipeline with verification at each step. Read it to drive the run. Optional: skip it if you can't orchestrate sub-agents, and ignore it entirely if you were *spawned* as one — you've already been scoped to a single phase.
|
|
324
|
+
|
|
321
325
|
When you complete a skill's checklist, check the boxes and add a completion timestamp at the end (e.g., `Completed: 2026-03-11`).
|
|
322
326
|
|
|
323
327
|
---
|
|
324
328
|
|
|
325
329
|
## Commands
|
|
326
330
|
|
|
331
|
+
**Runtime:** Scripts use Bun's native TypeScript execution — `bun run <cmd>` is the standard invocation. `npm run <cmd>` also works (npm delegates to bun).
|
|
332
|
+
|
|
327
333
|
| Command | Purpose |
|
|
328
334
|
|:--------|:--------|
|
|
329
335
|
| `bun run build` | Compile TypeScript |
|
|
330
336
|
| `bun run rebuild` | Clean + build |
|
|
331
337
|
| `bun run clean` | Remove build artifacts |
|
|
332
338
|
| `bun run devcheck` | Lint + format + typecheck + security + changelog sync |
|
|
333
|
-
| `bun run audit:fix` |
|
|
334
|
-
| `bun run audit:refresh` | Delete `bun.lock
|
|
339
|
+
| `bun run audit:fix` | `bun audit fix` — upgrade vulnerable packages to the lowest safe version within existing ranges (`--dry-run` previews, `--latest` rewrites ranges). First response when `devcheck` flags a transitive advisory; then `bun update <name>`, then `bun dedupe` |
|
|
340
|
+
| `bun run audit:refresh` | Delete `bun.lock` and reinstall. Last resort after `audit:fix`, `bun update <name>`, and `bun dedupe` — re-resolves every ranged dep (the framework pin included) and rewrites the lockfile as `lockfileVersion: 2` |
|
|
335
341
|
| `bun run tree` | Generate directory structure doc |
|
|
336
|
-
| `bun run format` | Auto-fix formatting |
|
|
337
|
-
| `bun run
|
|
338
|
-
| `bun run lint:
|
|
342
|
+
| `bun run format` | Auto-fix formatting (safe fixes only) |
|
|
343
|
+
| `bun run format:unsafe` | Also apply Biome's unsafe autofixes — review the diff; they can change behavior |
|
|
344
|
+
| `bun run lint:mcp` | Run the MCP definition linter standalone (rule catalog: `api-linter` skill) |
|
|
345
|
+
| `bun run lint:packaging` | Packaging surface checks — `server.json`/`manifest.json` env-var parity (run by devcheck) |
|
|
339
346
|
| `bun run list-skills` | Print an index of available skills from `framework-skills/` |
|
|
340
347
|
| `bun run changelog:build` | Regenerate `CHANGELOG.md` from `changelog/*.md` |
|
|
341
348
|
| `bun run changelog:check` | Verify `CHANGELOG.md` is in sync with `changelog/` (used by devcheck) |
|
|
342
349
|
| `bun run bundle` | Build, pack, and clean a `.mcpb` for one-click Claude Desktop install |
|
|
343
|
-
| `bun run test` | Run tests |
|
|
350
|
+
| `bun run test` | Run tests (Vitest — use `bun run test`, not `bun test`) |
|
|
344
351
|
| `bun run mirror:init` | Bootstrap the local mirror (download company_tickers + companyfacts.zip). Out-of-band; resumable. |
|
|
345
352
|
| `bun run mirror:refresh` | Incrementally refresh the local mirror from the SEC bulk files. |
|
|
346
353
|
| `bun run mirror:verify` | Print mirror sync status + run sample reads. |
|
|
@@ -354,9 +361,11 @@ When you complete a skill's checklist, check the boxes and add a completion time
|
|
|
354
361
|
|
|
355
362
|
## Bundling
|
|
356
363
|
|
|
357
|
-
`bun run bundle` produces a `.mcpb` extension bundle for one-click install in Claude Desktop. The pack step is followed by `scripts/clean-mcpb.ts`, which prunes dev dependencies (`mcpb clean`) and strips dependency-shipped agent docs (`
|
|
364
|
+
`bun run bundle` produces a `.mcpb` extension bundle for one-click install in Claude Desktop. The pack step is followed by `scripts/clean-mcpb.ts`, which prunes dev dependencies (`mcpb clean`) and strips two classes of `node_modules/**` content that root-anchored `.mcpbignore` patterns cannot reach: dependency-shipped agent docs (`framework-skills/`, `skills/`, `.claude/`, `.agents/`, `SKILL.md`) and platform-specific native bindings, which would otherwise lock the bundle to the platform it was packed on. The bundle therefore ships without the DuckDB native — the framework lazy-loads `@duckdb/node-api`, so canvas tools report an actionable install hint and every other tool works normally (the data tools answer inline without registering a dataframe). MCPB is stdio-only — HTTP deployments are unaffected. Delete `manifest.json` and `.mcpbignore` to opt out; `lint:packaging` skips cleanly.
|
|
365
|
+
|
|
366
|
+
**Adding an env var requires both files:** `server.json` (registry discovery, `environmentVariables[]`) and `manifest.json` (bundle install UX, `mcp_config.env` + `user_config`). `lint:packaging` (run by `devcheck`) verifies the env var names match, that every `user_config` option is wired into `mcp_config.env` as `"X": "${user_config.X}"` (the host substitutes nothing else — `"${X}"` reaches the server as that literal string), and that an optional string option carries `"default": ""`.
|
|
358
367
|
|
|
359
|
-
**
|
|
368
|
+
**README install badges** (Claude Desktop `.mcpb`, Cursor, VS Code) and the `base64` / `encodeURIComponent` config-generation commands are ship-time concerns — run the `polish-docs-meta` skill, which carries the badge format, layout, and generation snippets in `framework-skills/polish-docs-meta/references/readme.md`.
|
|
360
369
|
|
|
361
370
|
---
|
|
362
371
|
|
|
@@ -367,7 +376,7 @@ Directory-based. Source of truth is `changelog/<major.minor>.x/<version>.md` —
|
|
|
367
376
|
**To add a release entry:**
|
|
368
377
|
|
|
369
378
|
1. Author `changelog/<major.minor>.x/<version>.md` using `changelog/template.md` as a reference.
|
|
370
|
-
2. Add YAML frontmatter: `summary` (≤350 chars, no markdown), optional `breaking: true` flags breaking changes (`· ⚠️ Breaking` badge), optional `security: true` flags security
|
|
379
|
+
2. Add YAML frontmatter: `summary` (≤350 chars, no markdown), optional `breaking: true` flags breaking changes (`· ⚠️ Breaking` badge), optional `security: true` flags a security fix in this server's own source code (`· 🛡️ Security` badge, pairs with a `## Security` body section) — never a routine dependency or transitive CVE bump, which goes under `## Dependencies`.
|
|
371
380
|
3. Set the H1 heading to `# <version> — YYYY-MM-DD`.
|
|
372
381
|
4. Run `bun run changelog:build` to regenerate `CHANGELOG.md`.
|
|
373
382
|
|
|
@@ -415,13 +424,13 @@ import { getEdgarApiService } from '@/services/edgar/edgar-api-service.js';
|
|
|
415
424
|
- [ ] Optional nested objects: handler guards for empty inner values from form-based clients (`if (input.obj?.field && ...)`, not just `if (input.obj)`). When regex/length constraints matter, use `z.union([z.literal(''), z.string().regex(...).describe(...)])` — literal variants are exempt from `describe-on-fields`.
|
|
416
425
|
- [ ] JSDoc `@fileoverview` + `@module` on every file
|
|
417
426
|
- [ ] `ctx.log` for logging, `ctx.state` for storage
|
|
418
|
-
- [ ] Handlers throw on failure — typed `errors[]` contract + `ctx.fail(reason,
|
|
427
|
+
- [ ] Handlers throw on failure — typed `errors[]` contract + `ctx.fail(reason, …)` when failure modes are known; factories or plain `Error` for ad-hoc throws. No try/catch.
|
|
419
428
|
- [ ] Tool error contracts include `recovery` strings (≥5 words)
|
|
420
429
|
- [ ] `format()` renders all data the LLM needs — Claude Code reads `structuredContent`, Claude Desktop reads `content[]`; both must carry the same data
|
|
421
430
|
- [ ] EDGAR upstream sparsity: schemas reflect real nullability; `format()` preserves uncertainty (don't fabricate facts from missing XBRL fields); tests cover at least one sparse payload
|
|
422
431
|
- [ ] Registered in `createApp()` arrays (directly or via barrel exports)
|
|
423
432
|
- [ ] Tests use `createMockContext({ errors: tool.errors })` from `@cyanheads/mcp-ts-core/testing` for tools with declared contracts
|
|
424
|
-
- [ ] `.codex-plugin/plugin.json` populated — `name`, `version`, `description`, `repository`, `license` from `package.json`; `interface.displayName` =
|
|
425
|
-
- [ ] `.codex-plugin/mcp.json` updated — server name key
|
|
426
|
-
- [ ] `.claude-plugin/plugin.json` populated — `name`, `version`, `description`, `repository`, `license` from `package.json`; inline `mcpServers` entry
|
|
433
|
+
- [ ] `.codex-plugin/plugin.json` populated — `name`, `version`, `description`, `repository`, `license` from `package.json`; `interface.displayName` = the unscoped repo name (never the npm scope — `lint:packaging` enforces this); `interface.shortDescription` from `package.json` description
|
|
434
|
+
- [ ] `.codex-plugin/mcp.json` updated — server name key is the unscoped repo name; every user-supplied variable (API key, contact email, instance URL) is listed in `env_vars` so Codex forwards it from the user's environment. Never write `"KEY": ""` into `env` — an empty value replaces the user's exported key and is read as unset
|
|
435
|
+
- [ ] `.claude-plugin/plugin.json` populated — `name`, `version`, `description`, `author`, `repository`, `license`, `keywords` from `package.json`; inline `mcpServers` entry keyed by the unscoped repo name. Every user-supplied variable is declared under `userConfig` (`type`, `title`, `description`; `sensitive: true` for keys and tokens; `required: true` or `default: ""`) and referenced from `env` as `"KEY": "${user_config.<option>}"` — mirror the `user_config` block in `manifest.json`. Never write `"KEY": ""` into `env`
|
|
427
436
|
- [ ] `bun run devcheck` passes
|
package/Dockerfile
CHANGED
|
@@ -4,15 +4,17 @@
|
|
|
4
4
|
# This stage installs all dependencies (including dev), builds the TypeScript
|
|
5
5
|
# source code into JavaScript, and prepares the production assets.
|
|
6
6
|
#
|
|
7
|
-
# Pinned to BUILDPLATFORM
|
|
8
|
-
#
|
|
9
|
-
#
|
|
10
|
-
#
|
|
11
|
-
# assertion
|
|
12
|
-
#
|
|
13
|
-
#
|
|
7
|
+
# Pinned to $BUILDPLATFORM rather than the target platform: `bun run build` emits
|
|
8
|
+
# JavaScript, and only `dist/` crosses into the production stage. Built for the
|
|
9
|
+
# target instead, the non-native leg of a `--platform linux/amd64,linux/arm64`
|
|
10
|
+
# build runs under QEMU, where bun >= 1.4 aborts with a JavaScriptCore allocator
|
|
11
|
+
# assertion and fails the multi-arch push.
|
|
12
|
+
#
|
|
13
|
+
# The constraint this assumes: the build stage produces platform-independent
|
|
14
|
+
# output. A stage that compiles a native addon needs the target-arch toolchain
|
|
15
|
+
# and cannot cross-compile this way — drop the flag there.
|
|
14
16
|
# ==============================================================================
|
|
15
|
-
FROM --platform=$BUILDPLATFORM oven/bun:1.4.
|
|
17
|
+
FROM --platform=$BUILDPLATFORM oven/bun:1.4.2 AS build
|
|
16
18
|
|
|
17
19
|
WORKDIR /usr/src/app
|
|
18
20
|
|
|
@@ -35,52 +37,105 @@ COPY . .
|
|
|
35
37
|
RUN bun run build
|
|
36
38
|
|
|
37
39
|
|
|
40
|
+
# ==============================================================================
|
|
41
|
+
# Production Dependencies Stage
|
|
42
|
+
#
|
|
43
|
+
# Installs the production dependency tree for the target platform. Every step
|
|
44
|
+
# here can run JavaScript — bunfig.toml's security scanner runs as a Bun
|
|
45
|
+
# program, and so do the OTel and musl-prune scripts — so the stage runs on
|
|
46
|
+
# $BUILDPLATFORM and cross-installs with `--os`/`--cpu`, which pick each
|
|
47
|
+
# platform-specific optional dependency (native bindings such as DuckDB's) for
|
|
48
|
+
# the target. Only `node_modules` leaves this stage.
|
|
49
|
+
#
|
|
50
|
+
# A clean image rather than `FROM build`: the build stage's node_modules holds
|
|
51
|
+
# devDependencies.
|
|
52
|
+
# ==============================================================================
|
|
53
|
+
FROM --platform=$BUILDPLATFORM oven/bun:1.4.2 AS deps
|
|
54
|
+
|
|
55
|
+
WORKDIR /usr/src/app
|
|
56
|
+
|
|
57
|
+
# Copy dependency manifests. `bunfig.toml` rides along so every install below
|
|
58
|
+
# passes its release-age gate and security scanner, as a local install does.
|
|
59
|
+
COPY package.json bun.lock bunfig.toml ./
|
|
60
|
+
|
|
61
|
+
# The scanner bunfig.toml names is a devDependency, and Bun installs a missing
|
|
62
|
+
# scanner through the same production-filtered install, which omits it and
|
|
63
|
+
# aborts. Seed it from the build stage's full install instead. Remove this line,
|
|
64
|
+
# and the `rm` at the end of this stage, if bunfig.toml stops naming a scanner.
|
|
65
|
+
COPY --from=build /usr/src/app/node_modules/@socketsecurity/bun-security-scanner ./node_modules/@socketsecurity/bun-security-scanner
|
|
66
|
+
|
|
67
|
+
# Docker names the target architecture `amd64`/`arm64`; Bun's `--cpu` takes
|
|
68
|
+
# `x64`/`arm64`. Mapped once here, read by both installs below. `oven/bun`
|
|
69
|
+
# publishes only these two architectures, so any other target fails here.
|
|
70
|
+
ARG TARGETOS
|
|
71
|
+
ARG TARGETARCH
|
|
72
|
+
RUN case "$TARGETARCH" in \
|
|
73
|
+
amd64) echo x64 ;; \
|
|
74
|
+
arm64) echo arm64 ;; \
|
|
75
|
+
*) echo "Unsupported TARGETARCH '$TARGETARCH': expected amd64 or arm64" >&2; exit 1 ;; \
|
|
76
|
+
esac > .bun-cpu
|
|
77
|
+
|
|
78
|
+
# Install only production dependencies, ignoring lifecycle scripts — including
|
|
79
|
+
# better-sqlite3's native postinstall, which the image never needs (see the
|
|
80
|
+
# build stage). `--omit=peer` drops the framework's optional peer tiers (test
|
|
81
|
+
# runner, service SDKs, parsers) that Bun would otherwise auto-install. Anything
|
|
82
|
+
# this server actually imports belongs in its own `dependencies`, so nothing
|
|
83
|
+
# needed at runtime is lost.
|
|
84
|
+
RUN --mount=type=cache,target=/root/.bun/install/cache \
|
|
85
|
+
bun install --production --omit=peer --frozen-lockfile --ignore-scripts \
|
|
86
|
+
--os="$TARGETOS" --cpu="$(cat .bun-cpu)"
|
|
87
|
+
|
|
88
|
+
# Conditionally install OpenTelemetry optional peer dependencies (Tier 3).
|
|
89
|
+
# Installed by default. Omit them for a leaner image at build time
|
|
90
|
+
# with: docker build --build-arg OTEL_ENABLED=false
|
|
91
|
+
# The script reads the list and each range from the installed framework's
|
|
92
|
+
# `peerDependencies` and passes the target flags on to its `bun install`.
|
|
93
|
+
COPY scripts/install-otel.ts ./scripts/
|
|
94
|
+
ARG OTEL_ENABLED=true
|
|
95
|
+
RUN --mount=type=cache,target=/root/.bun/install/cache \
|
|
96
|
+
if [ "$OTEL_ENABLED" = "true" ]; then \
|
|
97
|
+
bun scripts/install-otel.ts --os="$TARGETOS" --cpu="$(cat .bun-cpu)"; \
|
|
98
|
+
fi
|
|
99
|
+
|
|
100
|
+
# `--os`/`--cpu` have no libc counterpart, so a native dependency published in
|
|
101
|
+
# glibc and musl variants (DuckDB's bindings, for one) installs both. The
|
|
102
|
+
# runtime image is Debian (glibc) and never loads the musl copy; the script
|
|
103
|
+
# deletes every package whose own `libc` admits only musl. It follows every
|
|
104
|
+
# install, since a later `bun install` restores what it removes.
|
|
105
|
+
COPY scripts/prune-musl-packages.ts ./scripts/
|
|
106
|
+
RUN bun scripts/prune-musl-packages.ts
|
|
107
|
+
|
|
108
|
+
# The seeded scanner served only the installs above; keep it out of the image.
|
|
109
|
+
RUN rm -rf node_modules/@socketsecurity/bun-security-scanner
|
|
110
|
+
|
|
111
|
+
|
|
38
112
|
# ==============================================================================
|
|
39
113
|
# Production Stage
|
|
40
114
|
#
|
|
41
115
|
# This stage creates a minimal, optimized, and secure image for running the
|
|
42
116
|
# application. It uses a slim base image and only includes production
|
|
43
|
-
# dependencies and build artifacts.
|
|
117
|
+
# dependencies and build artifacts. Its only Bun invocations are HEALTHCHECK
|
|
118
|
+
# and CMD, which run on the real target at container start.
|
|
44
119
|
# ==============================================================================
|
|
45
|
-
FROM oven/bun:1.4.
|
|
120
|
+
FROM oven/bun:1.4.2-slim AS production
|
|
46
121
|
|
|
47
122
|
WORKDIR /usr/src/app
|
|
48
123
|
|
|
49
|
-
# Set the environment to production for performance
|
|
50
|
-
# production dependencies are installed.
|
|
124
|
+
# Set the environment to production for performance.
|
|
51
125
|
ENV NODE_ENV=production
|
|
52
126
|
|
|
53
127
|
# OCI image metadata (https://github.com/opencontainers/image-spec/blob/main/annotations.md)
|
|
128
|
+
ARG APP_VERSION
|
|
54
129
|
LABEL org.opencontainers.image.title="@cyanheads/secedgar-mcp-server"
|
|
55
130
|
LABEL org.opencontainers.image.description="Query SEC EDGAR filings, XBRL financials, and company data through MCP. STDIO & Streamable HTTP."
|
|
56
131
|
LABEL org.opencontainers.image.source="https://github.com/cyanheads/secedgar-mcp-server"
|
|
57
132
|
LABEL org.opencontainers.image.licenses="Apache-2.0"
|
|
133
|
+
LABEL org.opencontainers.image.version="${APP_VERSION}"
|
|
58
134
|
|
|
59
|
-
#
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
# compiled native artifacts (installed with --ignore-scripts); the SQLite mirror
|
|
64
|
-
# uses bun:sqlite at runtime under Bun, so nothing needs building here.
|
|
65
|
-
COPY --from=build /usr/src/app/node_modules ./node_modules
|
|
66
|
-
|
|
67
|
-
# Conditionally install OpenTelemetry optional peer dependencies (Tier 3).
|
|
68
|
-
# These are not bundled by default to keep the base image lean. Enable at build time
|
|
69
|
-
# with: docker build --build-arg OTEL_ENABLED=true
|
|
70
|
-
ARG OTEL_ENABLED=true
|
|
71
|
-
RUN --mount=type=cache,target=/root/.bun/install/cache \
|
|
72
|
-
if [ "$OTEL_ENABLED" = "true" ]; then \
|
|
73
|
-
bun add --omit=dev --omit=peer --ignore-scripts @hono/otel \
|
|
74
|
-
@opentelemetry/instrumentation-http \
|
|
75
|
-
@opentelemetry/exporter-metrics-otlp-http \
|
|
76
|
-
@opentelemetry/exporter-trace-otlp-http \
|
|
77
|
-
@opentelemetry/instrumentation-pino \
|
|
78
|
-
@opentelemetry/resources \
|
|
79
|
-
@opentelemetry/sdk-metrics \
|
|
80
|
-
@opentelemetry/sdk-node \
|
|
81
|
-
@opentelemetry/sdk-trace-node \
|
|
82
|
-
@opentelemetry/semantic-conventions; \
|
|
83
|
-
fi
|
|
135
|
+
# The manifest comes from the build context: the deps stage's copy was rewritten
|
|
136
|
+
# by the OTel install, and the runtime reads only its name, version, and type.
|
|
137
|
+
COPY package.json ./
|
|
138
|
+
COPY --from=deps /usr/src/app/node_modules ./node_modules
|
|
84
139
|
|
|
85
140
|
# Copy the compiled application code from the build stage
|
|
86
141
|
COPY --from=build /usr/src/app/dist ./dist
|
|
@@ -122,10 +177,12 @@ ENV MCP_TRANSPORT_TYPE="http"
|
|
|
122
177
|
ENV MCP_SESSION_MODE="stateless"
|
|
123
178
|
ENV MCP_LOG_LEVEL="info"
|
|
124
179
|
ENV LOGS_DIR="/var/log/secedgar-mcp-server"
|
|
125
|
-
ENV MCP_FORCE_CONSOLE_LOGGING="true"
|
|
126
180
|
|
|
127
181
|
# Expose the port the server listens on
|
|
128
182
|
EXPOSE ${MCP_HTTP_PORT}
|
|
129
183
|
|
|
184
|
+
# Health check using a bun-native fetch (slim image ships no curl/wget)
|
|
185
|
+
HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 CMD bun -e "fetch('http://localhost:'+(process.env.MCP_HTTP_PORT??'3010')+'/healthz').then((r)=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))"
|
|
186
|
+
|
|
130
187
|
# The command to start the server
|
|
131
188
|
CMD ["bun", "run", "dist/index.js"]
|
package/README.md
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
|
|
8
8
|
<div align="center">
|
|
9
9
|
|
|
10
|
-
[](https://www.npmjs.com/package/@cyanheads/secedgar-mcp-server) [](./LICENSE) [](https://github.com/users/cyanheads/packages/container/package/secedgar-mcp-server) [](https://www.npmjs.com/package/@cyanheads/secedgar-mcp-server) [](./LICENSE) [](https://github.com/users/cyanheads/packages/container/package/secedgar-mcp-server) [](https://modelcontextprotocol.io/) [](https://www.typescriptlang.org/) [](https://bun.sh/)
|
|
11
11
|
|
|
12
12
|
</div>
|
|
13
13
|
|
|
@@ -91,7 +91,6 @@ SEC EDGAR filings, XBRL financials, and ownership data. No API key needed, only
|
|
|
91
91
|
- `accession_number` in dash or 18-digit form, optional `cik` to speed the lookup, `document` for an exhibit, `include_xbrl` for XBRL artifacts; `content_limit` 1,000–200,000 characters per page (default 50,000)
|
|
92
92
|
- Page with `offset` / `next_offset` until `content_truncated` is false, or jump with `section` (substring match on detected headings); the first page of a truncated document carries an `outline` of up to 50 headings with offsets
|
|
93
93
|
- `documents` splits the filing into primary, exhibits, and auxiliary; entries marked `binary` (scans, PDFs, archives) fail as `binary_document`, and a `section` miss returns `section_not_found` with the outline
|
|
94
|
-
- `form`, `filing_date`, and `period_ending` come from the company's submissions feed for a recent filing and from the filing's own SEC header for an older one
|
|
95
94
|
|
|
96
95
|
---
|
|
97
96
|
|
|
@@ -159,10 +158,8 @@ SEC EDGAR filings, XBRL financials, and ownership data. No API key needed, only
|
|
|
159
158
|
|
|
160
159
|
### `secedgar_fetch_frames` <sub>tool</sub>
|
|
161
160
|
|
|
162
|
-
- `concept` as a friendly name or raw tag, `period` as `CY2023`, `CY2024Q2`, or `CY2023Q4I`, `unit` (default `USD`), `sort` `desc` / `asc`; `limit` 1–100 (default 25) with `offset` / `next_offset` down the ranking
|
|
163
|
-
- One call queries one tag: `unqueried_tags` lists same-meaning variants to fetch separately
|
|
164
|
-
- `value_distribution.max_to_p95_ratio` flags scale-factor outliers, `period_end_range` shows fiscal-year mixing, and `caveats` names the fiscal-Q4 gap in quarterly frames, the proxy-statement rows in annual `NetIncomeLoss` frames, and the 10-Q trailing-twelve-month rows an annual frame can hold while its year is still open
|
|
165
|
-
- SEC publishes frames for us-gaap and dei tags only: `taxonomy` `us-gaap` (default) or `dei` picks the namespace for a raw tag (`EntityCommonStockSharesOutstanding` is dei), a friendly name keeps its own mapped taxonomy (`shares_outstanding` reads dei), and an explicit `dei` reads a friendly name's tags from dei, as in `secedgar_get_financials`; IFRS filers are read per company with `taxonomy` `ifrs-full`
|
|
161
|
+
- `concept` as a friendly name or raw tag, `period` as `CY2023`, `CY2024Q2`, or `CY2023Q4I`, `taxonomy` `us-gaap` (default) or `dei` (SEC publishes frames for no other namespace; IFRS filers are read per company), `unit` (default `USD`), `sort` `desc` / `asc`; `limit` 1–100 (default 25) with `offset` / `next_offset` down the ranking
|
|
162
|
+
- One call queries one tag: `unqueried_tags` lists same-meaning variants to fetch separately and `related_tags` alternate-definition tags; `value_distribution.max_to_p95_ratio` flags scale-factor outliers, `period_end_range` shows fiscal-year mixing, and `caveats` flags fiscal-Q4 gaps in quarterly frames and annual frames a proxy statement or a 10-Q can hold
|
|
166
163
|
- A `concept` that is neither a friendly name nor an UpperCamelCase tag fails as `unknown_concept` before the frames request, with the same formula or closest-name hint as `secedgar_get_financials`; a well-formed tag with no frame is `no_data`
|
|
167
164
|
|
|
168
165
|
---
|
|
@@ -172,7 +169,6 @@ SEC EDGAR filings, XBRL financials, and ownership data. No API key needed, only
|
|
|
172
169
|
- 2–10 `companies` × 1–8 `concepts`; `taxonomy` `us-gaap` (default) or `ifrs-full`; `period_type` `annual` (default) or `quarterly`; `periods` 1–12 (default 4), trimmed further when the inline matrix gets too large
|
|
173
170
|
- `cells` align each value on a calendar `period` and keep its `frame`, `period_end`, and source `tag`; `failed_companies` (reason `not_found`, `ambiguous`, or `no_company_facts`) and `gaps` (no value in any period) report what's missing, and `caveats` flag differing period ends, unit mismatches, and, once per concept, the companies whose values all predate the inline window, each with its newest period
|
|
174
171
|
- A concept that is neither a friendly name nor an UpperCamelCase tag is listed once in `unknown_concepts` with its hint, never as a gap per company; the call fails as `unknown_concept` only when every concept is one
|
|
175
|
-
- Inputs naming the same concept (`revenue` and `Revenue`, or one raw tag spelled twice) are compared once under the first spelling, with a caveat naming the merged inputs; a friendly name and a raw tag it maps to (`revenue` and `Revenues`) stay separate
|
|
176
172
|
|
|
177
173
|
---
|
|
178
174
|
|
|
@@ -375,6 +371,10 @@ cp .env.example .env
|
|
|
375
371
|
| `MCP_HTTP_PORT` | HTTP server port. | `3010` |
|
|
376
372
|
| `MCP_AUTH_MODE` | Authentication: `none`, `jwt`, or `oauth`. | `none` |
|
|
377
373
|
| `MCP_LOG_LEVEL` | Log level (`debug`, `info`, `warning`, `error`, etc.). | `info` |
|
|
374
|
+
| `LOG_TOOL_FAILURE_PAYLOADS` | Log each failed tool call's arguments and result, redacted by key name and capped at `LOG_TOOL_FAILURE_PAYLOAD_MAX_BYTES` (default `16384`). A secret inside a free-form value is not redacted. | `false` |
|
|
375
|
+
| `OTEL_ENABLED` | Enable [OpenTelemetry instrumentation](https://github.com/cyanheads/mcp-ts-core/tree/main/docs/telemetry) (spans, metrics, completion logs). | `false` |
|
|
376
|
+
| `OTEL_EXPORTER_OTLP_ENDPOINT` | OTLP base URL; traces go to `/v1/traces` and metrics to `/v1/metrics`. `OTEL_EXPORTER_OTLP_TRACES_ENDPOINT` / `OTEL_EXPORTER_OTLP_METRICS_ENDPOINT` override either one. | — |
|
|
377
|
+
| `OTEL_EXPORTER_OTLP_LOGS_ENDPOINT` | Opt-in OTLP log export (e.g. `http://localhost:4318/v1/logs`); the base endpoint never enables it. | — |
|
|
378
378
|
|
|
379
379
|
See [`.env.example`](./.env.example) for the full list of optional overrides.
|
|
380
380
|
|