@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.
Files changed (54) hide show
  1. package/AGENTS.md +33 -24
  2. package/CLAUDE.md +33 -24
  3. package/Dockerfile +95 -38
  4. package/README.md +7 -7
  5. package/changelog/0.15.x/0.15.10.md +39 -0
  6. package/dist/mcp-server/tools/definitions/company-search.tool.d.ts.map +1 -1
  7. package/dist/mcp-server/tools/definitions/company-search.tool.js +1 -4
  8. package/dist/mcp-server/tools/definitions/company-search.tool.js.map +1 -1
  9. package/dist/mcp-server/tools/definitions/compare-companies.tool.d.ts.map +1 -1
  10. package/dist/mcp-server/tools/definitions/compare-companies.tool.js +1 -2
  11. package/dist/mcp-server/tools/definitions/compare-companies.tool.js.map +1 -1
  12. package/dist/mcp-server/tools/definitions/dataframe-describe.tool.d.ts.map +1 -1
  13. package/dist/mcp-server/tools/definitions/dataframe-describe.tool.js +1 -3
  14. package/dist/mcp-server/tools/definitions/dataframe-describe.tool.js.map +1 -1
  15. package/dist/mcp-server/tools/definitions/dataframe-drop.tool.d.ts.map +1 -1
  16. package/dist/mcp-server/tools/definitions/dataframe-drop.tool.js +1 -3
  17. package/dist/mcp-server/tools/definitions/dataframe-drop.tool.js.map +1 -1
  18. package/dist/mcp-server/tools/definitions/dataframe-query.tool.d.ts.map +1 -1
  19. package/dist/mcp-server/tools/definitions/dataframe-query.tool.js +1 -3
  20. package/dist/mcp-server/tools/definitions/dataframe-query.tool.js.map +1 -1
  21. package/dist/mcp-server/tools/definitions/find-holders.tool.d.ts.map +1 -1
  22. package/dist/mcp-server/tools/definitions/find-holders.tool.js +2 -5
  23. package/dist/mcp-server/tools/definitions/find-holders.tool.js.map +1 -1
  24. package/dist/mcp-server/tools/definitions/get-beneficial-owners.tool.d.ts.map +1 -1
  25. package/dist/mcp-server/tools/definitions/get-beneficial-owners.tool.js +2 -8
  26. package/dist/mcp-server/tools/definitions/get-beneficial-owners.tool.js.map +1 -1
  27. package/dist/mcp-server/tools/definitions/get-filing.tool.d.ts.map +1 -1
  28. package/dist/mcp-server/tools/definitions/get-filing.tool.js +0 -2
  29. package/dist/mcp-server/tools/definitions/get-filing.tool.js.map +1 -1
  30. package/dist/mcp-server/tools/definitions/get-financials.tool.d.ts.map +1 -1
  31. package/dist/mcp-server/tools/definitions/get-financials.tool.js +3 -11
  32. package/dist/mcp-server/tools/definitions/get-financials.tool.js.map +1 -1
  33. package/dist/mcp-server/tools/definitions/get-fund-holdings.tool.d.ts.map +1 -1
  34. package/dist/mcp-server/tools/definitions/get-fund-holdings.tool.js +7 -17
  35. package/dist/mcp-server/tools/definitions/get-fund-holdings.tool.js.map +1 -1
  36. package/dist/mcp-server/tools/definitions/get-insider-transactions.tool.d.ts.map +1 -1
  37. package/dist/mcp-server/tools/definitions/get-insider-transactions.tool.js +2 -6
  38. package/dist/mcp-server/tools/definitions/get-insider-transactions.tool.js.map +1 -1
  39. package/dist/mcp-server/tools/definitions/get-institutional-holdings.tool.d.ts.map +1 -1
  40. package/dist/mcp-server/tools/definitions/get-institutional-holdings.tool.js +8 -14
  41. package/dist/mcp-server/tools/definitions/get-institutional-holdings.tool.js.map +1 -1
  42. package/dist/mcp-server/tools/definitions/get-material-events.tool.d.ts.map +1 -1
  43. package/dist/mcp-server/tools/definitions/get-material-events.tool.js +3 -11
  44. package/dist/mcp-server/tools/definitions/get-material-events.tool.js.map +1 -1
  45. package/dist/mcp-server/tools/definitions/get-snapshot.tool.d.ts.map +1 -1
  46. package/dist/mcp-server/tools/definitions/get-snapshot.tool.js +3 -11
  47. package/dist/mcp-server/tools/definitions/get-snapshot.tool.js.map +1 -1
  48. package/dist/mcp-server/tools/definitions/search-filings.tool.d.ts.map +1 -1
  49. package/dist/mcp-server/tools/definitions/search-filings.tool.js +3 -3
  50. package/dist/mcp-server/tools/definitions/search-filings.tool.js.map +1 -1
  51. package/dist/services/canvas-bridge/canvas-bridge.js +2 -2
  52. package/dist/services/canvas-bridge/canvas-bridge.js.map +1 -1
  53. package/package.json +10 -9
  54. 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.9
5
- **Framework:** [@cyanheads/mcp-ts-core](https://www.npmjs.com/package/@cyanheads/mcp-ts-core) `^0.13.6`
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` on mcp-ts-core 0.13.6 carries no per-tool definitions at all, so nothing changes there (#103). |
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` | Reader over a retried request's responses — `.accepted(key, schema)`, `.view(key)`, `.state()`, `.dropped`. Empty on the first round. |
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` | Unique request ID. |
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). Use `ctx.recoveryFor('reason')` to spread the contract recovery onto the wire (mirrored into `content[]` unless the message already contains it verbatim); pass an explicit `{ recovery: { hint } }` when runtime context matters. Forwarding it is lint-enforced per throw site (`error-contract-recovery-unforwarded`). 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.**
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` | Apply `bun audit fix` — first move when `devcheck` flags an advisory. |
334
- | `bun run audit:refresh` | Delete `bun.lock`, reinstall, re-audit. Use when `devcheck` flags a transitive advisory — stale lockfile can mask already-patched deps. If advisory survives, it's real. |
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 lint:mcp` | Validate MCP tool/resource definitions |
338
- | `bun run lint:packaging` | Validate env-var alignment between `manifest.json` and `server.json` |
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 (`node_modules/**` `framework-skills/`, `skills/`, `.claude/`, `.agents/`, `SKILL.md`) that root-anchored `.mcpbignore` patterns cannot reach. MCPB is stdio-only — HTTP deployments are unaffected. Delete `manifest.json` and `.mcpbignore` to opt out; `lint:packaging` skips cleanly.
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
- **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.
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 fixes (`· 🛡️ Security` badge, pairs with a `## Security` body section).
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, …, ctx.recoveryFor(reason))` when failure modes are known; factories or plain `Error` for ad-hoc throws. No try/catch.
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` = package name; `interface.shortDescription` from `package.json` description
425
- - [ ] `.codex-plugin/mcp.json` updated — server name key matches `package.json` name; env vars added for any required API keys
426
- - [ ] `.claude-plugin/plugin.json` populated — `name`, `version`, `description`, `repository`, `license` from `package.json`; inline `mcpServers` entry with server name key, env vars for any required API keys
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.9
5
- **Framework:** [@cyanheads/mcp-ts-core](https://www.npmjs.com/package/@cyanheads/mcp-ts-core) `^0.13.6`
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` on mcp-ts-core 0.13.6 carries no per-tool definitions at all, so nothing changes there (#103). |
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` | Reader over a retried request's responses — `.accepted(key, schema)`, `.view(key)`, `.state()`, `.dropped`. Empty on the first round. |
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` | Unique request ID. |
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). Use `ctx.recoveryFor('reason')` to spread the contract recovery onto the wire (mirrored into `content[]` unless the message already contains it verbatim); pass an explicit `{ recovery: { hint } }` when runtime context matters. Forwarding it is lint-enforced per throw site (`error-contract-recovery-unforwarded`). 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.**
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` | Apply `bun audit fix` — first move when `devcheck` flags an advisory. |
334
- | `bun run audit:refresh` | Delete `bun.lock`, reinstall, re-audit. Use when `devcheck` flags a transitive advisory — stale lockfile can mask already-patched deps. If advisory survives, it's real. |
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 lint:mcp` | Validate MCP tool/resource definitions |
338
- | `bun run lint:packaging` | Validate env-var alignment between `manifest.json` and `server.json` |
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 (`node_modules/**` `framework-skills/`, `skills/`, `.claude/`, `.agents/`, `SKILL.md`) that root-anchored `.mcpbignore` patterns cannot reach. MCPB is stdio-only — HTTP deployments are unaffected. Delete `manifest.json` and `.mcpbignore` to opt out; `lint:packaging` skips cleanly.
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
- **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.
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 fixes (`· 🛡️ Security` badge, pairs with a `## Security` body section).
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, …, ctx.recoveryFor(reason))` when failure modes are known; factories or plain `Error` for ad-hoc throws. No try/catch.
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` = package name; `interface.shortDescription` from `package.json` description
425
- - [ ] `.codex-plugin/mcp.json` updated — server name key matches `package.json` name; env vars added for any required API keys
426
- - [ ] `.claude-plugin/plugin.json` populated — `name`, `version`, `description`, `repository`, `license` from `package.json`; inline `mcpServers` entry with server name key, env vars for any required API keys
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, not the target platform. `bun run build` emits
8
- # platform-independent JavaScript and only `dist/` is copied forward, so this
9
- # stage has no reason to run under emulation — and under QEMU it does not run
10
- # at all: Bun's JavaScriptCore aborts with a spurious MemoryExhaustion
11
- # assertion, so a cross-arch build of this stage fails outright on an arm64
12
- # host. Building natively also removes emulation from the slowest stage of a
13
- # multi-arch build.
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.0 AS build
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.0-slim AS production
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 and to ensure only
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
- # Copy dependency manifests
60
- COPY package.json bun.lock ./
61
-
62
- # Copy node_modules from the build stage rather than reinstalling. Deps carry no
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
- [![npm](https://img.shields.io/npm/v/@cyanheads/secedgar-mcp-server?style=flat-square&logo=npm&logoColor=white)](https://www.npmjs.com/package/@cyanheads/secedgar-mcp-server) [![License](https://img.shields.io/badge/License-Apache%202.0-orange.svg?style=flat-square)](./LICENSE) [![Docker](https://img.shields.io/badge/Docker-ghcr.io-2496ED?style=flat-square&logo=docker&logoColor=white)](https://github.com/users/cyanheads/packages/container/package/secedgar-mcp-server) [![MCP SDK](https://img.shields.io/badge/MCP%20SDK-^2.0.0-green.svg?style=flat-square)](https://modelcontextprotocol.io/) [![TypeScript](https://img.shields.io/badge/TypeScript-^7.0.2-3178C6.svg?style=flat-square)](https://www.typescriptlang.org/) [![Bun](https://img.shields.io/badge/Bun-v1.4.0-blueviolet.svg?style=flat-square)](https://bun.sh/)
10
+ [![npm](https://img.shields.io/npm/v/@cyanheads/secedgar-mcp-server?style=flat-square&logo=npm&logoColor=white)](https://www.npmjs.com/package/@cyanheads/secedgar-mcp-server) [![License](https://img.shields.io/badge/License-Apache%202.0-orange.svg?style=flat-square)](./LICENSE) [![Docker](https://img.shields.io/badge/Docker-ghcr.io-2496ED?style=flat-square&logo=docker&logoColor=white)](https://github.com/users/cyanheads/packages/container/package/secedgar-mcp-server) [![MCP SDK](https://img.shields.io/badge/MCP%20SDK-^2.2.0-green.svg?style=flat-square)](https://modelcontextprotocol.io/) [![TypeScript](https://img.shields.io/badge/TypeScript-^7.0.2-3178C6.svg?style=flat-square)](https://www.typescriptlang.org/) [![Bun](https://img.shields.io/badge/Bun-v1.4.2-blueviolet.svg?style=flat-square)](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, and `related_tags` lists alternate-definition tags some filers report instead
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