@cyanheads/congressgov-mcp-server 0.8.1 → 0.9.1

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 (84) hide show
  1. package/AGENTS.md +44 -17
  2. package/CLAUDE.md +44 -17
  3. package/Dockerfile +4 -0
  4. package/README.md +35 -124
  5. package/changelog/0.9.x/0.9.0.md +25 -0
  6. package/changelog/0.9.x/0.9.1.md +31 -0
  7. package/dist/config/server-config.d.ts +0 -7
  8. package/dist/config/server-config.d.ts.map +1 -1
  9. package/dist/config/server-config.js +1 -9
  10. package/dist/config/server-config.js.map +1 -1
  11. package/dist/index.js +1 -0
  12. package/dist/index.js.map +1 -1
  13. package/dist/mcp-server/tools/definitions/bill-lookup.tool.d.ts +9 -2
  14. package/dist/mcp-server/tools/definitions/bill-lookup.tool.d.ts.map +1 -1
  15. package/dist/mcp-server/tools/definitions/bill-summaries.tool.d.ts +9 -2
  16. package/dist/mcp-server/tools/definitions/bill-summaries.tool.d.ts.map +1 -1
  17. package/dist/mcp-server/tools/definitions/committee-lookup.tool.d.ts +9 -2
  18. package/dist/mcp-server/tools/definitions/committee-lookup.tool.d.ts.map +1 -1
  19. package/dist/mcp-server/tools/definitions/committee-reports.tool.d.ts +9 -2
  20. package/dist/mcp-server/tools/definitions/committee-reports.tool.d.ts.map +1 -1
  21. package/dist/mcp-server/tools/definitions/crs-reports.tool.d.ts +9 -2
  22. package/dist/mcp-server/tools/definitions/crs-reports.tool.d.ts.map +1 -1
  23. package/dist/mcp-server/tools/definitions/daily-record.tool.d.ts +9 -2
  24. package/dist/mcp-server/tools/definitions/daily-record.tool.d.ts.map +1 -1
  25. package/dist/mcp-server/tools/definitions/enacted-laws.tool.d.ts +9 -2
  26. package/dist/mcp-server/tools/definitions/enacted-laws.tool.d.ts.map +1 -1
  27. package/dist/mcp-server/tools/definitions/member-lookup.tool.d.ts +9 -2
  28. package/dist/mcp-server/tools/definitions/member-lookup.tool.d.ts.map +1 -1
  29. package/dist/mcp-server/tools/definitions/roll-votes.tool.d.ts +9 -2
  30. package/dist/mcp-server/tools/definitions/roll-votes.tool.d.ts.map +1 -1
  31. package/dist/mcp-server/tools/definitions/roll-votes.tool.js +3 -2
  32. package/dist/mcp-server/tools/definitions/roll-votes.tool.js.map +1 -1
  33. package/dist/mcp-server/tools/definitions/senate-nominations.tool.d.ts +49 -4
  34. package/dist/mcp-server/tools/definitions/senate-nominations.tool.d.ts.map +1 -1
  35. package/dist/mcp-server/tools/definitions/senate-nominations.tool.js +102 -5
  36. package/dist/mcp-server/tools/definitions/senate-nominations.tool.js.map +1 -1
  37. package/dist/mcp-server/tools/format-helpers.d.ts.map +1 -1
  38. package/dist/mcp-server/tools/format-helpers.js +37 -8
  39. package/dist/mcp-server/tools/format-helpers.js.map +1 -1
  40. package/dist/mcp-server/tools/tool-helpers.d.ts +11 -4
  41. package/dist/mcp-server/tools/tool-helpers.d.ts.map +1 -1
  42. package/dist/mcp-server/tools/tool-helpers.js +12 -4
  43. package/dist/mcp-server/tools/tool-helpers.js.map +1 -1
  44. package/dist/services/call-budget.d.ts +77 -0
  45. package/dist/services/call-budget.d.ts.map +1 -0
  46. package/dist/services/call-budget.js +118 -0
  47. package/dist/services/call-budget.js.map +1 -0
  48. package/dist/services/congress-api/congress-api-service.d.ts +4 -0
  49. package/dist/services/congress-api/congress-api-service.d.ts.map +1 -1
  50. package/dist/services/congress-api/congress-api-service.js +60 -32
  51. package/dist/services/congress-api/congress-api-service.js.map +1 -1
  52. package/dist/services/congress-documents/congress-documents-service.d.ts +46 -15
  53. package/dist/services/congress-documents/congress-documents-service.d.ts.map +1 -1
  54. package/dist/services/congress-documents/congress-documents-service.js +95 -24
  55. package/dist/services/congress-documents/congress-documents-service.js.map +1 -1
  56. package/dist/services/congress-documents/document-body-cache.d.ts +66 -0
  57. package/dist/services/congress-documents/document-body-cache.d.ts.map +1 -0
  58. package/dist/services/congress-documents/document-body-cache.js +122 -0
  59. package/dist/services/congress-documents/document-body-cache.js.map +1 -0
  60. package/dist/services/congress-mirror/ingest.d.ts +13 -7
  61. package/dist/services/congress-mirror/ingest.d.ts.map +1 -1
  62. package/dist/services/congress-mirror/ingest.js +33 -10
  63. package/dist/services/congress-mirror/ingest.js.map +1 -1
  64. package/dist/services/congress-mirror/schema.d.ts.map +1 -1
  65. package/dist/services/congress-mirror/schema.js +15 -1
  66. package/dist/services/congress-mirror/schema.js.map +1 -1
  67. package/dist/services/senate-lis/parse.d.ts +23 -0
  68. package/dist/services/senate-lis/parse.d.ts.map +1 -1
  69. package/dist/services/senate-lis/parse.js +49 -5
  70. package/dist/services/senate-lis/parse.js.map +1 -1
  71. package/dist/services/senate-lis/senate-vote-service.d.ts.map +1 -1
  72. package/dist/services/senate-lis/senate-vote-service.js +32 -25
  73. package/dist/services/senate-lis/senate-vote-service.js.map +1 -1
  74. package/dist/utils/congress-calendar.d.ts +15 -0
  75. package/dist/utils/congress-calendar.d.ts.map +1 -0
  76. package/dist/utils/congress-calendar.js +33 -0
  77. package/dist/utils/congress-calendar.js.map +1 -0
  78. package/package.json +7 -6
  79. package/scripts/_mirror-context.ts +15 -9
  80. package/scripts/congress-mirror-init.ts +1 -1
  81. package/scripts/congress-mirror-refresh.ts +1 -1
  82. package/scripts/congress-mirror-verify.ts +5 -4
  83. package/server.json +3 -3
  84. package/skills/congressgov-guide/SKILL.md +50 -0
package/AGENTS.md CHANGED
@@ -1,10 +1,10 @@
1
1
  # Agent Protocol
2
2
 
3
3
  **Server:** congressgov-mcp-server
4
- **Version:** 0.8.1
5
- **Framework:** [@cyanheads/mcp-ts-core](https://www.npmjs.com/package/@cyanheads/mcp-ts-core) `^0.13.16`
4
+ **Version:** 0.9.1
5
+ **Framework:** [@cyanheads/mcp-ts-core](https://www.npmjs.com/package/@cyanheads/mcp-ts-core) `^0.13.18`
6
6
  **Engines:** Bun ≥1.4.0, Node ≥24.0.0
7
- **MCP SDK:** `@modelcontextprotocol/server` ^2.3.1
7
+ **MCP SDK:** `@modelcontextprotocol/server` ^2.4.0
8
8
  **Zod:** ^4.6.7
9
9
 
10
10
  > **Read the framework docs first:** `node_modules/@cyanheads/mcp-ts-core/CLAUDE.md` contains the full API reference — builders, Context, error codes, exports, patterns. This file covers server-specific conventions only.
@@ -74,7 +74,7 @@ Tailor suggestions to what's actually missing or stale — don't recite the full
74
74
  | `congressgov_member_lookup` | Discover members by state/district/congress, retrieve legislative portfolios |
75
75
  | `congressgov_committee_lookup` | Browse committees and retrieve legislation, reports, nominations |
76
76
  | `congressgov_roll_votes` | Retrieve House and Senate roll call votes and member voting positions |
77
- | `congressgov_senate_nominations` | Browse presidential nominations, track Senate confirmation pipeline |
77
+ | `congressgov_senate_nominations` | Browse presidential nominations, track Senate confirmation pipeline; an action citing its own Senate record vote (`actions` rows, `latestAction` on `list`/`get`) carries `rollVote`, the `congressgov_roll_votes` arguments for it, from the 101st Congress on |
78
78
  | `congressgov_bill_summaries` | Browse recent CRS bill summaries — the "what's happening" feed, bounded by the shared summary page budget and per-row text window |
79
79
  | `congressgov_crs_reports` | Browse and retrieve nonpartisan CRS policy analysis reports |
80
80
  | `congressgov_committee_reports` | Browse and retrieve committee reports accompanying legislation + bounded report text via `content` |
@@ -83,7 +83,7 @@ Tailor suggestions to what's actually missing or stale — don't recite the full
83
83
 
84
84
  **Summary bounding** (`src/mcp-server/tools/summary-window.ts`, constants in `tool-helpers.ts`). Both summary surfaces enforce two limits. `SUMMARY_PAGE_CHARACTERS` stops a page once its serialized rows reach the budget — the first row always returns, `pagination.nextOffset` counts the rows actually returned, and an enrichment notice discloses the stop. `DEFAULT_SUMMARY_TEXT_CHARACTERS` bounds one row's upstream `text`; a windowed row gains `textTotalCharacters` / `textTruncated` / `textNextOffset`, and its window end is retracted off any HTML tag or character reference it would otherwise split (unless the construct is wider than the whole window, where the requested limit wins). A row inside both limits is returned untouched, so an unbounded page stays byte-identical. `congressgov_bill_lookup` `summaries` overrides the row window with `characterOffset`/`characterLimit` and selects one version with `versionCode` — Congress.gov publishes no version filter, so selection reads the bill's whole summary list and matches client-side.
85
85
 
86
- ### Resources (5)
86
+ ### Resources (6)
87
87
 
88
88
  | URI Template | Description |
89
89
  |:-------------|:------------|
@@ -92,6 +92,7 @@ Tailor suggestions to what's actually missing or stale — don't recite the full
92
92
  | `congress://member/{bioguideId}` | Member profile |
93
93
  | `congress://bill/{congress}/{billType}/{billNumber}` | Bill detail |
94
94
  | `congress://committee/{committeeCode}` | Committee detail |
95
+ | `skill://congressgov-guide/SKILL.md` | Published legislative research guide |
95
96
 
96
97
  ### Prompts (2)
97
98
 
@@ -110,11 +111,13 @@ src/
110
111
  config/
111
112
  server-config.ts # CONGRESS_API_KEY, base URL (Zod schema)
112
113
  services/
114
+ call-budget.ts # One 50 s upstream budget per tool call / resource read, shared by the three HTTP services
113
115
  congress-api/
114
116
  congress-api-service.ts # API client — auth, pagination, rate limiting
115
117
  types.ts # API response types
116
118
  congress-documents/
117
- congress-documents-service.ts # www.congress.gov document fetch — host allowlist, byte ceiling, character window
119
+ congress-documents-service.ts # www.congress.gov document fetch — host allowlist, byte ceiling, call budget, character window
120
+ document-body-cache.ts # Process-local LRU of decompressed bodies — one fetch per document per TTL
118
121
  document-formats.ts # Format label → URL resolution over upstream format lists
119
122
  extract-text.ts # GPO `<pre>` / XML body → deterministic plain text
120
123
  extract-text-stream.ts # Incremental twin of extract-text.ts — one character window, constant memory
@@ -127,7 +130,7 @@ src/
127
130
  types.ts # Mirror row, search filters, search page types
128
131
  senate-lis/
129
132
  senate-vote-service.ts # Senate LIS XML client — fetch, retry, not-found
130
- parse.ts # pure XML → domain parsers (fast-xml-parser)
133
+ parse.ts # pure XML → domain parsers (fast-xml-parser); Senate vote coverage, session for a date
131
134
  types.ts # Senate vote domain types
132
135
  mcp-server/
133
136
  tools/definitions/
@@ -153,6 +156,7 @@ src/
153
156
  legislative-research.prompt.ts # congressgov_legislative_research
154
157
  utils/
155
158
  character-references.ts # Single-pass HTML/XML character-reference decoding
159
+ congress-calendar.ts # ordinal(), congressFirstYear(), currentCongressNumber() — congress numbering arithmetic
156
160
  scripts/
157
161
  _mirror-context.ts # Shared bootstrap for the mirror lifecycle CLI scripts
158
162
  congress-mirror-init.ts # mirror:init — full out-of-band mirror build
@@ -166,6 +170,12 @@ scripts/
166
170
 
167
171
  Four services. `CongressApiService` backs nine tools and the House branch of `congressgov_roll_votes`; `SenateVoteService` backs only the Senate branch of `congressgov_roll_votes` (the Congress.gov API exposes no Senate vote namespace); `CongressMirrorService` backs `congressgov_search_bills` only; `CongressDocumentsService` backs the `content` operation on `congressgov_bill_lookup`, `congressgov_committee_reports`, and `congressgov_daily_record`.
168
172
 
173
+ **Call budget** (`call-budget.ts`, `withCallBudget`) — every upstream request a tool call or resource read makes through `CongressApiService`, `SenateVoteService`, or `CongressDocumentsService` spends one 50 s budget (`CALL_BUDGET_MS`) counted from the call's start (`ctx.timestamp`), so chained and parallel requests (a roster paged four times, a `content` lookup and then its document) end together before the 60 s default MCP client timeout:
174
+ - Each request's retry ladder gets what is left as `withRetry` `deadlineMs`; each attempt's timeout (10 s API, 12 s Senate, 30 s document) is clamped to the remainder and its I/O carries the budget's signal
175
+ - A backoff the remainder cannot hold fails at once as the expiry; a `Retry-After` wait it cannot hold fails at once with the upstream's own error and `retryAfter`; a request the spent budget cannot cover is never sent
176
+ - Running out is `Timeout`, reason `upstream_timeout`, with `budgetMs` and `retryAttempts`. A caller abort stays `RequestCancelled`, and a 429 is still never retried
177
+ - Services read the budget from the handler's own `ctx` — pass it to every service call, never a context derived from it (that carries a later timestamp). A request with no `ctx` — the mirror ingester and the `mirror:*` scripts — runs the plain per-request ladder, unbudgeted
178
+
169
179
  **`CongressApiService`** — wraps the Congress.gov REST API v3:
170
180
  - API key via `X-Api-Key` header (never logged; kept out of the URL so it can't leak in upstream error messages)
171
181
  - Pagination: `offset` + `limit` query params, max 250 per request
@@ -178,18 +188,22 @@ Four services. `CongressApiService` backs nine tools and the House branch of `co
178
188
  - The whole session menu is one file; each vote's roster ships inline — pagination is client-side
179
189
  - The host returns HTTP 200 with an HTML page for unknown congress/session/vote, so "not found" is detected from the body, not the status code
180
190
  - Party totals are derived from the roster (the feed publishes none)
191
+ - senate.gov's firewall answers automated clients with HTTP 403, and a retry meets the same refusal: a 403 fails on the first attempt as `upstream_error` with `retryable: false` and a hint that House votes are unaffected, while a 5xx or timeout is retried within the call budget
181
192
 
182
193
  **`CongressDocumentsService`** — reads the document bodies behind Congress.gov's format URLs (`www.congress.gov`):
183
194
  - A second host, so a sibling service rather than a method on `CongressApiService` — no API key, no JSON, and enrolled omnibus bills that run to ten megabytes
184
195
  - URLs are resolved from upstream format metadata and checked against a `www.congress.gov` allowlist; a caller never supplies one
185
- - The fetch is bounded: a 25 MB ceiling (refused on `Content-Length`, else mid-stream), a 30s deadline, and a content-type allowlist
186
- - The body is extracted as it streams and only the requested window is retained, so a read's live set is flat in the size of the document (transient allocation is not — peak RSS runs above a buffered read); the ceiling bounds how long a response may run, not how much of one fits in a buffer
196
+ - The fetch is bounded: a 25 MB ceiling (refused on `Content-Length`, else mid-stream), a content-type allowlist, and the call budget (above) — the api.congress.gov format lookup the handler makes first spends from it, and the document attempts and backoff share what is left
197
+ - The body is extracted as it streams and only the requested window is extracted into memory; the ceiling bounds how long a response may run, not how much of one fits in a buffer (transient allocation still scales with the body — peak RSS runs above a buffered read)
198
+ - A document that runs past its window keeps its decompressed body in `DocumentBodyCache` — process-local, one cache for every tenant (each body is public and fetched without credentials), 64 MB LRU, 16 MB per body (a larger one is captured until it passes that and then dropped), 10-minute TTL from insert — and later windows replay those bytes through the same extractor in 128 KiB slices with a macrotask between them: one fetch per document per TTL, and a replay never spends the budget. A window that reaches the end of its document caches nothing, a cancelled read caches nothing, and concurrent cold reads each fetch (no single-flight)
187
199
  - The structured response is bounded by an exact character window — `structuredContent.content.text` and its offsets index the extracted plain text and are never snapped to section breaks, so feeding `nextOffset` back walks a document with no overlap and no gap; `content[]` wraps that unchanged window in a dynamically safe Markdown fence for presentation
188
200
  - `extract-text.ts` unwraps GPO's `<pre>` print output verbatim (whitespace is the document's structure), while no-`<pre>` XML gains one space only at an otherwise-unseparated sibling close→open element boundary; both readings decode character references through `src/utils/character-references.ts` — the one decoder every HTML/XML → text path in the server shares, single-pass so an escaped reference stays escaped — and `extract-text-stream.ts` is their incremental twin, pinned byte-for-byte against the whole-string extractor
189
201
 
190
202
  **`CongressMirrorService`** — local SQLite FTS5 mirror of bill title + CRS summary text, backing `congressgov_search_bills` only:
191
203
  - Opt-in via `CONGRESS_MIRROR_ENABLED` (off by default); built out-of-band via the `mirror:init`/`mirror:refresh` scripts, never on server startup
192
204
  - No live-API fallback — ingests through the existing `CongressApiService`, and a mirror that hasn't finished its initial build returns an empty result with a notice, not an error
205
+ - Every sync re-walks each congress's whole summaries list, requested from January 1 of the congress's first year with no end date (without `fromDateTime`, Congress.gov returns only the last day's summaries); the `updateDate` checkpoint is persisted only once the whole walk finishes; store spec `version: 2` migrates an older store by clearing its checkpoint, so the next sync backfills
206
+ - The `mirror:*` scripts initialize the framework logger: log lines go to stderr as JSON, stdout carries only the script's own output
193
207
  - `createApp({ teardown })` stops the refresh schedule, cancels and awaits an active refresh, then closes the SQLite mirror. With `MCP_SESSION_MODE` unset, HTTP defaults to stateless; an explicit `MCP_SESSION_MODE` value still overrides it. No handler uses `ctx.requestInput`.
194
208
 
195
209
  **Usage in tools:**
@@ -198,7 +212,7 @@ import { getCongressApi } from '@/services/congress-api/congress-api-service.js'
198
212
 
199
213
  async handler(input, ctx) {
200
214
  const api = getCongressApi();
201
- const bills = await api.listBills({ congress: input.congress, limit: input.limit });
215
+ const bills = await api.listBills({ congress: input.congress, limit: input.limit }, ctx);
202
216
  ctx.log.info('Bills fetched', { congress: input.congress, count: bills.bills.length });
203
217
  return bills;
204
218
  }
@@ -215,11 +229,13 @@ All tools share these patterns. The service layer handles them uniformly:
215
229
  | Condition | Error |
216
230
  |:----------|:------|
217
231
  | Rate limit hit | `rateLimited(...)` with `rate_limited`, credential/shared-quota guidance and upstream `Retry-After` when supplied |
218
- | Deadline or HTTP timeout | `Timeout` (-32004), `upstream_timeout`; HTTP responses retain status |
219
- | API unavailable (5xx) | `serviceUnavailable('Congress.gov API returned HTTP {status}.')` |
232
+ | Attempt timeout, call budget spent, or HTTP timeout response | `Timeout` (-32004), `upstream_timeout`; a spent budget names it (`{upstream} did not deliver {request} within this call's 50-second budget for upstream requests (N attempts).`) and carries `budgetMs`/`retryAttempts`; HTTP responses retain status |
233
+ | API unavailable (5xx) | `ServiceUnavailable` (-32000), `upstream_error`: `Congress.gov returned an unexpected error (HTTP {status}).`; a 501 carries `retryable: false` |
220
234
  | Entity not found (404) | `notFound('{entity} not found', { ...identifiers })` |
235
+ | Upstream rejects the request (400, 401, 402, 403, 422) | `invalidParams('Congress.gov rejected the request as malformed (HTTP {status}).')`, `invalid_request` |
236
+ | Rejected API key (401/403 whose body carries an api.data.gov `API_KEY_*` code) | `configurationError(...)` (-32008), `api_key_rejected`, `retryable: false`, gateway code in `data.upstreamCode` and the message; the hint tells the operator to replace or unset `CONGRESS_API_KEY` and never echoes the key. Classified on the body code, since a malformed path segment is also a plain 403 (#34) |
221
237
  | Invalid params | `validationError('...', { field })` |
222
- | Network error | `serviceUnavailable('Unable to reach the Congress.gov API.')` |
238
+ | Network error | `ServiceUnavailable` (-32000), `upstream_error`: `Congress.gov returned an unexpected error.` |
223
239
 
224
240
  The `content` operation adds `documentErrorContracts` (tool-helpers) on top: `document_unavailable`, `format_unavailable`, `document_fetch_failed`, `document_too_large`, and `offset_past_end` — the last of which `congressgov_bill_lookup` also raises from its `summaries` branch when `characterOffset` is past the end of every returned row's text. `CongressDocumentsService` raises each with a matching `data.reason`; the framework fills the declared `recovery` hint when the throw carries none, service throws included. Explicit recovery overrides win. A direct `definition.handler(...)` test sees the throw unchanged; retain `ctx.recoveryFor` where such a test asserts the hint, or assert the filled hint through `runToolContract`.
225
241
 
@@ -256,9 +272,18 @@ import { getServerConfig } from '@/config/server-config.js';
256
272
 
257
273
  ## Skills
258
274
 
259
- Skills are modular instructions in `framework-skills/` at the project root. Read them directly when a task matches — e.g., `framework-skills/add-tool/SKILL.md` when adding a tool. `bun run list-skills` prints the full registry. The directory is deliberately not `skills/`: Claude Code and Codex auto-load a plugin's root `skills/`, so a server that ships `.claude-plugin/` or `.codex-plugin/` would hand these development skills to every agent that installs it. Keep `skills/` free for skills meant for those agents.
275
+ Two skill trees live at the project root, written for different readers:
276
+
277
+ | Tree | For | Written by |
278
+ |:-----|:----|:-----------|
279
+ | `framework-skills/` | The agent developing this server — framework skills (`metadata.audience: external`) plus any development-time skills of the server's own | `init` and the `maintenance` sync (framework skills); you (your own) |
280
+ | `skills/` | The agents that install the server and call its tools — tool chains, identifier sources, gates | `congressgov-guide/SKILL.md`, maintained with the `add-skill` skill |
281
+
282
+ Development skills are modular instructions. Read them directly when a task matches — e.g., `framework-skills/add-tool/SKILL.md` when adding a tool. `bun run list-skills` prints the full registry. Their directory is deliberately not `skills/`: Claude Code and Codex auto-load a plugin's root `skills/`, so a server that ships `.claude-plugin/` or `.codex-plugin/` would hand them to every agent that installs it.
283
+
284
+ `skills/congressgov-guide/SKILL.md` reaches plugin installs and the `.mcpb` bundle as-is and ships in the npm package and Docker image. `createApp({ skills: 'skills' })` serves it through the MCP Skills extension and the `skill://congressgov-guide/SKILL.md` resource. `lint:packaging` validates the tree.
260
285
 
261
- **Agent skill directory:** Copy skills into the directory your agent discovers (Claude Code: `.claude/skills/`, others: equivalent). This makes skills available as context without needing to reference `framework-skills/` paths manually. After framework updates, run the `maintenance` skill — it re-syncs the agent directory automatically (Phase B).
286
+ **Agent skill directory:** Copy `framework-skills/` into the directory your agent discovers (Claude Code: `.claude/skills/`, others: equivalent). Skills then load as context without referencing `framework-skills/` paths. After framework updates, run the `maintenance` skill — Phase B re-syncs the agent directory.
262
287
 
263
288
  Available skills:
264
289
 
@@ -270,6 +295,7 @@ Available skills:
270
295
  | `add-app-tool` | Scaffold an MCP App tool + paired UI resource |
271
296
  | `add-resource` | Scaffold a new resource definition |
272
297
  | `add-prompt` | Scaffold a new prompt definition |
298
+ | `add-skill` | Author a skill the server publishes in root `skills/` for the agents that use it — when one earns its place, structure, validation, delivery |
273
299
  | `add-service` | Scaffold a new service integration |
274
300
  | `add-test` | Scaffold test file for a tool, resource, or service |
275
301
  | `field-test` | Exercise tools/resources/prompts with real inputs, verify behavior, report issues |
@@ -317,7 +343,7 @@ When you complete a skill's checklist, check the boxes and add a completion time
317
343
  | `bun run format` | Auto-fix formatting (safe fixes only) |
318
344
  | `bun run format:unsafe` | Also apply Biome's unsafe autofixes — review the diff; they can change behavior |
319
345
  | `bun run lint:mcp` | Validate MCP tool/resource/prompt definitions |
320
- | `bun run lint:packaging` | Validate env var alignment between `manifest.json` and `server.json` |
346
+ | `bun run lint:packaging` | Validate bundle exclusions, plugin manifests, published skills, env var alignment, and version parity |
321
347
  | `bun run bundle` | Build and pack as `.mcpb` for one-click Claude Desktop install |
322
348
  | `bun run changelog:build` | Regenerate `CHANGELOG.md` from `changelog/*.md` |
323
349
  | `bun run changelog:check` | Verify `CHANGELOG.md` is in sync (used by devcheck) |
@@ -334,7 +360,7 @@ When you complete a skill's checklist, check the boxes and add a completion time
334
360
 
335
361
  ## Bundling
336
362
 
337
- `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. MCPB is stdio-only — HTTP deployments are unaffected. The bundle file ships as `dist/congressgov-mcp-server.mcpb`.
363
+ `bun run bundle` produces a `.mcpb` extension bundle for one-click install in Claude Desktop. `.mcpbignore` excludes root development paths with anchored patterns; root `skills/` ships. `scripts/clean-mcpb.ts` prunes dev dependencies and strips dependency agent docs, platform-specific native bindings, node-gyp build output that records host paths, and framework build-only files. Compiled native addons stay. Run `lint:packaging` after the completed bundle. MCPB is stdio-only; HTTP deployments are unaffected. The bundle file ships as `dist/congressgov-mcp-server.mcpb`.
338
364
 
339
365
  **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, every `user_config` option is wired into `mcp_config.env` as `"X": "${user_config.X}"`, and an optional string option carries `"default": ""`. The host substitutes only the `user_config` reference; `"${X}"` reaches the server literally.
340
366
 
@@ -400,6 +426,7 @@ The `release-and-publish` run handles npm, MCP Registry, bundle, GitHub Release,
400
426
  - [ ] If wrapping external API: tests include at least one sparse payload case with omitted upstream fields
401
427
  - [ ] Registered in `createApp()` arrays (directly or via barrel exports)
402
428
  - [ ] Tests use `createMockContext()` from `@cyanheads/mcp-ts-core/testing`
429
+ - [ ] Root `plugin.json` metadata matches `package.json`; `mcp.json` uses the unscoped server key, `npx -y <full package name>`, and the fixed env from `.codex-plugin/mcp.json`. User variables remain in the overlays; `.codex-plugin/plugin.json` retains `mcpServers: './.codex-plugin/mcp.json'`
403
430
  - [ ] `.codex-plugin/plugin.json` populated — `name` and `interface.displayName` = unscoped repo name; `version`, `description`, `repository`, `license` and `interface.shortDescription` from `package.json`
404
431
  - [ ] `.codex-plugin/mcp.json` updated — server name key is the unscoped repo name; every user-supplied variable is listed in `env_vars`. Never write `"KEY": ""` into `env` — it overrides the user's exported key
405
432
  - [ ] `.claude-plugin/plugin.json` populated — unscoped `name`, `version`, `description`, `author`, `repository`, `license`, `keywords`; inline `mcpServers` entry keyed by the unscoped repo name. User-supplied variables use `userConfig` and `"KEY": "${user_config.<option>}"` env references, matching `manifest.json`
package/CLAUDE.md CHANGED
@@ -1,10 +1,10 @@
1
1
  # Agent Protocol
2
2
 
3
3
  **Server:** congressgov-mcp-server
4
- **Version:** 0.8.1
5
- **Framework:** [@cyanheads/mcp-ts-core](https://www.npmjs.com/package/@cyanheads/mcp-ts-core) `^0.13.16`
4
+ **Version:** 0.9.1
5
+ **Framework:** [@cyanheads/mcp-ts-core](https://www.npmjs.com/package/@cyanheads/mcp-ts-core) `^0.13.18`
6
6
  **Engines:** Bun ≥1.4.0, Node ≥24.0.0
7
- **MCP SDK:** `@modelcontextprotocol/server` ^2.3.1
7
+ **MCP SDK:** `@modelcontextprotocol/server` ^2.4.0
8
8
  **Zod:** ^4.6.7
9
9
 
10
10
  > **Read the framework docs first:** `node_modules/@cyanheads/mcp-ts-core/CLAUDE.md` contains the full API reference — builders, Context, error codes, exports, patterns. This file covers server-specific conventions only.
@@ -74,7 +74,7 @@ Tailor suggestions to what's actually missing or stale — don't recite the full
74
74
  | `congressgov_member_lookup` | Discover members by state/district/congress, retrieve legislative portfolios |
75
75
  | `congressgov_committee_lookup` | Browse committees and retrieve legislation, reports, nominations |
76
76
  | `congressgov_roll_votes` | Retrieve House and Senate roll call votes and member voting positions |
77
- | `congressgov_senate_nominations` | Browse presidential nominations, track Senate confirmation pipeline |
77
+ | `congressgov_senate_nominations` | Browse presidential nominations, track Senate confirmation pipeline; an action citing its own Senate record vote (`actions` rows, `latestAction` on `list`/`get`) carries `rollVote`, the `congressgov_roll_votes` arguments for it, from the 101st Congress on |
78
78
  | `congressgov_bill_summaries` | Browse recent CRS bill summaries — the "what's happening" feed, bounded by the shared summary page budget and per-row text window |
79
79
  | `congressgov_crs_reports` | Browse and retrieve nonpartisan CRS policy analysis reports |
80
80
  | `congressgov_committee_reports` | Browse and retrieve committee reports accompanying legislation + bounded report text via `content` |
@@ -83,7 +83,7 @@ Tailor suggestions to what's actually missing or stale — don't recite the full
83
83
 
84
84
  **Summary bounding** (`src/mcp-server/tools/summary-window.ts`, constants in `tool-helpers.ts`). Both summary surfaces enforce two limits. `SUMMARY_PAGE_CHARACTERS` stops a page once its serialized rows reach the budget — the first row always returns, `pagination.nextOffset` counts the rows actually returned, and an enrichment notice discloses the stop. `DEFAULT_SUMMARY_TEXT_CHARACTERS` bounds one row's upstream `text`; a windowed row gains `textTotalCharacters` / `textTruncated` / `textNextOffset`, and its window end is retracted off any HTML tag or character reference it would otherwise split (unless the construct is wider than the whole window, where the requested limit wins). A row inside both limits is returned untouched, so an unbounded page stays byte-identical. `congressgov_bill_lookup` `summaries` overrides the row window with `characterOffset`/`characterLimit` and selects one version with `versionCode` — Congress.gov publishes no version filter, so selection reads the bill's whole summary list and matches client-side.
85
85
 
86
- ### Resources (5)
86
+ ### Resources (6)
87
87
 
88
88
  | URI Template | Description |
89
89
  |:-------------|:------------|
@@ -92,6 +92,7 @@ Tailor suggestions to what's actually missing or stale — don't recite the full
92
92
  | `congress://member/{bioguideId}` | Member profile |
93
93
  | `congress://bill/{congress}/{billType}/{billNumber}` | Bill detail |
94
94
  | `congress://committee/{committeeCode}` | Committee detail |
95
+ | `skill://congressgov-guide/SKILL.md` | Published legislative research guide |
95
96
 
96
97
  ### Prompts (2)
97
98
 
@@ -110,11 +111,13 @@ src/
110
111
  config/
111
112
  server-config.ts # CONGRESS_API_KEY, base URL (Zod schema)
112
113
  services/
114
+ call-budget.ts # One 50 s upstream budget per tool call / resource read, shared by the three HTTP services
113
115
  congress-api/
114
116
  congress-api-service.ts # API client — auth, pagination, rate limiting
115
117
  types.ts # API response types
116
118
  congress-documents/
117
- congress-documents-service.ts # www.congress.gov document fetch — host allowlist, byte ceiling, character window
119
+ congress-documents-service.ts # www.congress.gov document fetch — host allowlist, byte ceiling, call budget, character window
120
+ document-body-cache.ts # Process-local LRU of decompressed bodies — one fetch per document per TTL
118
121
  document-formats.ts # Format label → URL resolution over upstream format lists
119
122
  extract-text.ts # GPO `<pre>` / XML body → deterministic plain text
120
123
  extract-text-stream.ts # Incremental twin of extract-text.ts — one character window, constant memory
@@ -127,7 +130,7 @@ src/
127
130
  types.ts # Mirror row, search filters, search page types
128
131
  senate-lis/
129
132
  senate-vote-service.ts # Senate LIS XML client — fetch, retry, not-found
130
- parse.ts # pure XML → domain parsers (fast-xml-parser)
133
+ parse.ts # pure XML → domain parsers (fast-xml-parser); Senate vote coverage, session for a date
131
134
  types.ts # Senate vote domain types
132
135
  mcp-server/
133
136
  tools/definitions/
@@ -153,6 +156,7 @@ src/
153
156
  legislative-research.prompt.ts # congressgov_legislative_research
154
157
  utils/
155
158
  character-references.ts # Single-pass HTML/XML character-reference decoding
159
+ congress-calendar.ts # ordinal(), congressFirstYear(), currentCongressNumber() — congress numbering arithmetic
156
160
  scripts/
157
161
  _mirror-context.ts # Shared bootstrap for the mirror lifecycle CLI scripts
158
162
  congress-mirror-init.ts # mirror:init — full out-of-band mirror build
@@ -166,6 +170,12 @@ scripts/
166
170
 
167
171
  Four services. `CongressApiService` backs nine tools and the House branch of `congressgov_roll_votes`; `SenateVoteService` backs only the Senate branch of `congressgov_roll_votes` (the Congress.gov API exposes no Senate vote namespace); `CongressMirrorService` backs `congressgov_search_bills` only; `CongressDocumentsService` backs the `content` operation on `congressgov_bill_lookup`, `congressgov_committee_reports`, and `congressgov_daily_record`.
168
172
 
173
+ **Call budget** (`call-budget.ts`, `withCallBudget`) — every upstream request a tool call or resource read makes through `CongressApiService`, `SenateVoteService`, or `CongressDocumentsService` spends one 50 s budget (`CALL_BUDGET_MS`) counted from the call's start (`ctx.timestamp`), so chained and parallel requests (a roster paged four times, a `content` lookup and then its document) end together before the 60 s default MCP client timeout:
174
+ - Each request's retry ladder gets what is left as `withRetry` `deadlineMs`; each attempt's timeout (10 s API, 12 s Senate, 30 s document) is clamped to the remainder and its I/O carries the budget's signal
175
+ - A backoff the remainder cannot hold fails at once as the expiry; a `Retry-After` wait it cannot hold fails at once with the upstream's own error and `retryAfter`; a request the spent budget cannot cover is never sent
176
+ - Running out is `Timeout`, reason `upstream_timeout`, with `budgetMs` and `retryAttempts`. A caller abort stays `RequestCancelled`, and a 429 is still never retried
177
+ - Services read the budget from the handler's own `ctx` — pass it to every service call, never a context derived from it (that carries a later timestamp). A request with no `ctx` — the mirror ingester and the `mirror:*` scripts — runs the plain per-request ladder, unbudgeted
178
+
169
179
  **`CongressApiService`** — wraps the Congress.gov REST API v3:
170
180
  - API key via `X-Api-Key` header (never logged; kept out of the URL so it can't leak in upstream error messages)
171
181
  - Pagination: `offset` + `limit` query params, max 250 per request
@@ -178,18 +188,22 @@ Four services. `CongressApiService` backs nine tools and the House branch of `co
178
188
  - The whole session menu is one file; each vote's roster ships inline — pagination is client-side
179
189
  - The host returns HTTP 200 with an HTML page for unknown congress/session/vote, so "not found" is detected from the body, not the status code
180
190
  - Party totals are derived from the roster (the feed publishes none)
191
+ - senate.gov's firewall answers automated clients with HTTP 403, and a retry meets the same refusal: a 403 fails on the first attempt as `upstream_error` with `retryable: false` and a hint that House votes are unaffected, while a 5xx or timeout is retried within the call budget
181
192
 
182
193
  **`CongressDocumentsService`** — reads the document bodies behind Congress.gov's format URLs (`www.congress.gov`):
183
194
  - A second host, so a sibling service rather than a method on `CongressApiService` — no API key, no JSON, and enrolled omnibus bills that run to ten megabytes
184
195
  - URLs are resolved from upstream format metadata and checked against a `www.congress.gov` allowlist; a caller never supplies one
185
- - The fetch is bounded: a 25 MB ceiling (refused on `Content-Length`, else mid-stream), a 30s deadline, and a content-type allowlist
186
- - The body is extracted as it streams and only the requested window is retained, so a read's live set is flat in the size of the document (transient allocation is not — peak RSS runs above a buffered read); the ceiling bounds how long a response may run, not how much of one fits in a buffer
196
+ - The fetch is bounded: a 25 MB ceiling (refused on `Content-Length`, else mid-stream), a content-type allowlist, and the call budget (above) — the api.congress.gov format lookup the handler makes first spends from it, and the document attempts and backoff share what is left
197
+ - The body is extracted as it streams and only the requested window is extracted into memory; the ceiling bounds how long a response may run, not how much of one fits in a buffer (transient allocation still scales with the body — peak RSS runs above a buffered read)
198
+ - A document that runs past its window keeps its decompressed body in `DocumentBodyCache` — process-local, one cache for every tenant (each body is public and fetched without credentials), 64 MB LRU, 16 MB per body (a larger one is captured until it passes that and then dropped), 10-minute TTL from insert — and later windows replay those bytes through the same extractor in 128 KiB slices with a macrotask between them: one fetch per document per TTL, and a replay never spends the budget. A window that reaches the end of its document caches nothing, a cancelled read caches nothing, and concurrent cold reads each fetch (no single-flight)
187
199
  - The structured response is bounded by an exact character window — `structuredContent.content.text` and its offsets index the extracted plain text and are never snapped to section breaks, so feeding `nextOffset` back walks a document with no overlap and no gap; `content[]` wraps that unchanged window in a dynamically safe Markdown fence for presentation
188
200
  - `extract-text.ts` unwraps GPO's `<pre>` print output verbatim (whitespace is the document's structure), while no-`<pre>` XML gains one space only at an otherwise-unseparated sibling close→open element boundary; both readings decode character references through `src/utils/character-references.ts` — the one decoder every HTML/XML → text path in the server shares, single-pass so an escaped reference stays escaped — and `extract-text-stream.ts` is their incremental twin, pinned byte-for-byte against the whole-string extractor
189
201
 
190
202
  **`CongressMirrorService`** — local SQLite FTS5 mirror of bill title + CRS summary text, backing `congressgov_search_bills` only:
191
203
  - Opt-in via `CONGRESS_MIRROR_ENABLED` (off by default); built out-of-band via the `mirror:init`/`mirror:refresh` scripts, never on server startup
192
204
  - No live-API fallback — ingests through the existing `CongressApiService`, and a mirror that hasn't finished its initial build returns an empty result with a notice, not an error
205
+ - Every sync re-walks each congress's whole summaries list, requested from January 1 of the congress's first year with no end date (without `fromDateTime`, Congress.gov returns only the last day's summaries); the `updateDate` checkpoint is persisted only once the whole walk finishes; store spec `version: 2` migrates an older store by clearing its checkpoint, so the next sync backfills
206
+ - The `mirror:*` scripts initialize the framework logger: log lines go to stderr as JSON, stdout carries only the script's own output
193
207
  - `createApp({ teardown })` stops the refresh schedule, cancels and awaits an active refresh, then closes the SQLite mirror. With `MCP_SESSION_MODE` unset, HTTP defaults to stateless; an explicit `MCP_SESSION_MODE` value still overrides it. No handler uses `ctx.requestInput`.
194
208
 
195
209
  **Usage in tools:**
@@ -198,7 +212,7 @@ import { getCongressApi } from '@/services/congress-api/congress-api-service.js'
198
212
 
199
213
  async handler(input, ctx) {
200
214
  const api = getCongressApi();
201
- const bills = await api.listBills({ congress: input.congress, limit: input.limit });
215
+ const bills = await api.listBills({ congress: input.congress, limit: input.limit }, ctx);
202
216
  ctx.log.info('Bills fetched', { congress: input.congress, count: bills.bills.length });
203
217
  return bills;
204
218
  }
@@ -215,11 +229,13 @@ All tools share these patterns. The service layer handles them uniformly:
215
229
  | Condition | Error |
216
230
  |:----------|:------|
217
231
  | Rate limit hit | `rateLimited(...)` with `rate_limited`, credential/shared-quota guidance and upstream `Retry-After` when supplied |
218
- | Deadline or HTTP timeout | `Timeout` (-32004), `upstream_timeout`; HTTP responses retain status |
219
- | API unavailable (5xx) | `serviceUnavailable('Congress.gov API returned HTTP {status}.')` |
232
+ | Attempt timeout, call budget spent, or HTTP timeout response | `Timeout` (-32004), `upstream_timeout`; a spent budget names it (`{upstream} did not deliver {request} within this call's 50-second budget for upstream requests (N attempts).`) and carries `budgetMs`/`retryAttempts`; HTTP responses retain status |
233
+ | API unavailable (5xx) | `ServiceUnavailable` (-32000), `upstream_error`: `Congress.gov returned an unexpected error (HTTP {status}).`; a 501 carries `retryable: false` |
220
234
  | Entity not found (404) | `notFound('{entity} not found', { ...identifiers })` |
235
+ | Upstream rejects the request (400, 401, 402, 403, 422) | `invalidParams('Congress.gov rejected the request as malformed (HTTP {status}).')`, `invalid_request` |
236
+ | Rejected API key (401/403 whose body carries an api.data.gov `API_KEY_*` code) | `configurationError(...)` (-32008), `api_key_rejected`, `retryable: false`, gateway code in `data.upstreamCode` and the message; the hint tells the operator to replace or unset `CONGRESS_API_KEY` and never echoes the key. Classified on the body code, since a malformed path segment is also a plain 403 (#34) |
221
237
  | Invalid params | `validationError('...', { field })` |
222
- | Network error | `serviceUnavailable('Unable to reach the Congress.gov API.')` |
238
+ | Network error | `ServiceUnavailable` (-32000), `upstream_error`: `Congress.gov returned an unexpected error.` |
223
239
 
224
240
  The `content` operation adds `documentErrorContracts` (tool-helpers) on top: `document_unavailable`, `format_unavailable`, `document_fetch_failed`, `document_too_large`, and `offset_past_end` — the last of which `congressgov_bill_lookup` also raises from its `summaries` branch when `characterOffset` is past the end of every returned row's text. `CongressDocumentsService` raises each with a matching `data.reason`; the framework fills the declared `recovery` hint when the throw carries none, service throws included. Explicit recovery overrides win. A direct `definition.handler(...)` test sees the throw unchanged; retain `ctx.recoveryFor` where such a test asserts the hint, or assert the filled hint through `runToolContract`.
225
241
 
@@ -256,9 +272,18 @@ import { getServerConfig } from '@/config/server-config.js';
256
272
 
257
273
  ## Skills
258
274
 
259
- Skills are modular instructions in `framework-skills/` at the project root. Read them directly when a task matches — e.g., `framework-skills/add-tool/SKILL.md` when adding a tool. `bun run list-skills` prints the full registry. The directory is deliberately not `skills/`: Claude Code and Codex auto-load a plugin's root `skills/`, so a server that ships `.claude-plugin/` or `.codex-plugin/` would hand these development skills to every agent that installs it. Keep `skills/` free for skills meant for those agents.
275
+ Two skill trees live at the project root, written for different readers:
276
+
277
+ | Tree | For | Written by |
278
+ |:-----|:----|:-----------|
279
+ | `framework-skills/` | The agent developing this server — framework skills (`metadata.audience: external`) plus any development-time skills of the server's own | `init` and the `maintenance` sync (framework skills); you (your own) |
280
+ | `skills/` | The agents that install the server and call its tools — tool chains, identifier sources, gates | `congressgov-guide/SKILL.md`, maintained with the `add-skill` skill |
281
+
282
+ Development skills are modular instructions. Read them directly when a task matches — e.g., `framework-skills/add-tool/SKILL.md` when adding a tool. `bun run list-skills` prints the full registry. Their directory is deliberately not `skills/`: Claude Code and Codex auto-load a plugin's root `skills/`, so a server that ships `.claude-plugin/` or `.codex-plugin/` would hand them to every agent that installs it.
283
+
284
+ `skills/congressgov-guide/SKILL.md` reaches plugin installs and the `.mcpb` bundle as-is and ships in the npm package and Docker image. `createApp({ skills: 'skills' })` serves it through the MCP Skills extension and the `skill://congressgov-guide/SKILL.md` resource. `lint:packaging` validates the tree.
260
285
 
261
- **Agent skill directory:** Copy skills into the directory your agent discovers (Claude Code: `.claude/skills/`, others: equivalent). This makes skills available as context without needing to reference `framework-skills/` paths manually. After framework updates, run the `maintenance` skill — it re-syncs the agent directory automatically (Phase B).
286
+ **Agent skill directory:** Copy `framework-skills/` into the directory your agent discovers (Claude Code: `.claude/skills/`, others: equivalent). Skills then load as context without referencing `framework-skills/` paths. After framework updates, run the `maintenance` skill — Phase B re-syncs the agent directory.
262
287
 
263
288
  Available skills:
264
289
 
@@ -270,6 +295,7 @@ Available skills:
270
295
  | `add-app-tool` | Scaffold an MCP App tool + paired UI resource |
271
296
  | `add-resource` | Scaffold a new resource definition |
272
297
  | `add-prompt` | Scaffold a new prompt definition |
298
+ | `add-skill` | Author a skill the server publishes in root `skills/` for the agents that use it — when one earns its place, structure, validation, delivery |
273
299
  | `add-service` | Scaffold a new service integration |
274
300
  | `add-test` | Scaffold test file for a tool, resource, or service |
275
301
  | `field-test` | Exercise tools/resources/prompts with real inputs, verify behavior, report issues |
@@ -317,7 +343,7 @@ When you complete a skill's checklist, check the boxes and add a completion time
317
343
  | `bun run format` | Auto-fix formatting (safe fixes only) |
318
344
  | `bun run format:unsafe` | Also apply Biome's unsafe autofixes — review the diff; they can change behavior |
319
345
  | `bun run lint:mcp` | Validate MCP tool/resource/prompt definitions |
320
- | `bun run lint:packaging` | Validate env var alignment between `manifest.json` and `server.json` |
346
+ | `bun run lint:packaging` | Validate bundle exclusions, plugin manifests, published skills, env var alignment, and version parity |
321
347
  | `bun run bundle` | Build and pack as `.mcpb` for one-click Claude Desktop install |
322
348
  | `bun run changelog:build` | Regenerate `CHANGELOG.md` from `changelog/*.md` |
323
349
  | `bun run changelog:check` | Verify `CHANGELOG.md` is in sync (used by devcheck) |
@@ -334,7 +360,7 @@ When you complete a skill's checklist, check the boxes and add a completion time
334
360
 
335
361
  ## Bundling
336
362
 
337
- `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. MCPB is stdio-only — HTTP deployments are unaffected. The bundle file ships as `dist/congressgov-mcp-server.mcpb`.
363
+ `bun run bundle` produces a `.mcpb` extension bundle for one-click install in Claude Desktop. `.mcpbignore` excludes root development paths with anchored patterns; root `skills/` ships. `scripts/clean-mcpb.ts` prunes dev dependencies and strips dependency agent docs, platform-specific native bindings, node-gyp build output that records host paths, and framework build-only files. Compiled native addons stay. Run `lint:packaging` after the completed bundle. MCPB is stdio-only; HTTP deployments are unaffected. The bundle file ships as `dist/congressgov-mcp-server.mcpb`.
338
364
 
339
365
  **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, every `user_config` option is wired into `mcp_config.env` as `"X": "${user_config.X}"`, and an optional string option carries `"default": ""`. The host substitutes only the `user_config` reference; `"${X}"` reaches the server literally.
340
366
 
@@ -400,6 +426,7 @@ The `release-and-publish` run handles npm, MCP Registry, bundle, GitHub Release,
400
426
  - [ ] If wrapping external API: tests include at least one sparse payload case with omitted upstream fields
401
427
  - [ ] Registered in `createApp()` arrays (directly or via barrel exports)
402
428
  - [ ] Tests use `createMockContext()` from `@cyanheads/mcp-ts-core/testing`
429
+ - [ ] Root `plugin.json` metadata matches `package.json`; `mcp.json` uses the unscoped server key, `npx -y <full package name>`, and the fixed env from `.codex-plugin/mcp.json`. User variables remain in the overlays; `.codex-plugin/plugin.json` retains `mcpServers: './.codex-plugin/mcp.json'`
403
430
  - [ ] `.codex-plugin/plugin.json` populated — `name` and `interface.displayName` = unscoped repo name; `version`, `description`, `repository`, `license` and `interface.shortDescription` from `package.json`
404
431
  - [ ] `.codex-plugin/mcp.json` updated — server name key is the unscoped repo name; every user-supplied variable is listed in `env_vars`. Never write `"KEY": ""` into `env` — it overrides the user's exported key
405
432
  - [ ] `.claude-plugin/plugin.json` populated — unscoped `name`, `version`, `description`, `author`, `repository`, `license`, `keywords`; inline `mcpServers` entry keyed by the unscoped repo name. User-supplied variables use `userConfig` and `"KEY": "${user_config.<option>}"` env references, matching `manifest.json`
package/Dockerfile CHANGED
@@ -28,6 +28,9 @@ RUN --mount=type=cache,target=/root/.bun/install/cache \
28
28
  # Copy the rest of the source code
29
29
  COPY . .
30
30
 
31
+ # Published agent skills are read from the application root at runtime.
32
+ RUN mkdir -p skills
33
+
31
34
  # Build the application
32
35
  RUN bun run build
33
36
 
@@ -134,6 +137,7 @@ COPY --from=deps /usr/src/app/node_modules ./node_modules
134
137
 
135
138
  # Copy the compiled application code from the build stage
136
139
  COPY --from=build /usr/src/app/dist ./dist
140
+ COPY --from=build /usr/src/app/skills ./skills
137
141
 
138
142
  # Copy the mirror lifecycle scripts (mirror:init / mirror:refresh / mirror:verify)
139
143
  # and the shared context shim they import, then emit a runtime tsconfig so Bun