@cyanheads/nist-nvd-mcp-server 0.1.14 → 0.1.16

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 (47) hide show
  1. package/AGENTS.md +29 -16
  2. package/CLAUDE.md +29 -16
  3. package/Dockerfile +1 -1
  4. package/README.md +14 -9
  5. package/changelog/0.1.x/0.1.15.md +24 -0
  6. package/changelog/0.1.x/0.1.16.md +21 -0
  7. package/dist/mcp-server/resources/definitions/nvd-cve.resource.d.ts +17 -1
  8. package/dist/mcp-server/resources/definitions/nvd-cve.resource.d.ts.map +1 -1
  9. package/dist/mcp-server/resources/definitions/nvd-cve.resource.js +22 -7
  10. package/dist/mcp-server/resources/definitions/nvd-cve.resource.js.map +1 -1
  11. package/dist/mcp-server/tools/definitions/nvd-audit-cpe.tool.d.ts +2 -0
  12. package/dist/mcp-server/tools/definitions/nvd-audit-cpe.tool.d.ts.map +1 -1
  13. package/dist/mcp-server/tools/definitions/nvd-audit-cpe.tool.js +28 -31
  14. package/dist/mcp-server/tools/definitions/nvd-audit-cpe.tool.js.map +1 -1
  15. package/dist/mcp-server/tools/definitions/nvd-get-cve-history.tool.d.ts +8 -0
  16. package/dist/mcp-server/tools/definitions/nvd-get-cve-history.tool.d.ts.map +1 -1
  17. package/dist/mcp-server/tools/definitions/nvd-get-cve-history.tool.js +34 -6
  18. package/dist/mcp-server/tools/definitions/nvd-get-cve-history.tool.js.map +1 -1
  19. package/dist/mcp-server/tools/definitions/nvd-get-cve.tool.d.ts +7 -0
  20. package/dist/mcp-server/tools/definitions/nvd-get-cve.tool.d.ts.map +1 -1
  21. package/dist/mcp-server/tools/definitions/nvd-get-cve.tool.js +46 -23
  22. package/dist/mcp-server/tools/definitions/nvd-get-cve.tool.js.map +1 -1
  23. package/dist/mcp-server/tools/definitions/nvd-search-cpes.tool.d.ts +2 -0
  24. package/dist/mcp-server/tools/definitions/nvd-search-cpes.tool.d.ts.map +1 -1
  25. package/dist/mcp-server/tools/definitions/nvd-search-cpes.tool.js +26 -7
  26. package/dist/mcp-server/tools/definitions/nvd-search-cpes.tool.js.map +1 -1
  27. package/dist/mcp-server/tools/definitions/nvd-search-cves.tool.d.ts +1 -0
  28. package/dist/mcp-server/tools/definitions/nvd-search-cves.tool.d.ts.map +1 -1
  29. package/dist/mcp-server/tools/definitions/nvd-search-cves.tool.js +16 -2
  30. package/dist/mcp-server/tools/definitions/nvd-search-cves.tool.js.map +1 -1
  31. package/dist/mcp-server/tools/formatting/cpe-match.d.ts +46 -0
  32. package/dist/mcp-server/tools/formatting/cpe-match.d.ts.map +1 -0
  33. package/dist/mcp-server/tools/formatting/cpe-match.js +43 -0
  34. package/dist/mcp-server/tools/formatting/cpe-match.js.map +1 -0
  35. package/dist/services/nvd-cpe/nvd-cpe-service.d.ts +3 -0
  36. package/dist/services/nvd-cpe/nvd-cpe-service.d.ts.map +1 -1
  37. package/dist/services/nvd-cpe/nvd-cpe-service.js +3 -2
  38. package/dist/services/nvd-cpe/nvd-cpe-service.js.map +1 -1
  39. package/dist/services/nvd-cve/nvd-cve-service.d.ts +35 -1
  40. package/dist/services/nvd-cve/nvd-cve-service.d.ts.map +1 -1
  41. package/dist/services/nvd-cve/nvd-cve-service.js +116 -27
  42. package/dist/services/nvd-cve/nvd-cve-service.js.map +1 -1
  43. package/dist/services/nvd-cve/types.d.ts +12 -2
  44. package/dist/services/nvd-cve/types.d.ts.map +1 -1
  45. package/manifest.json +1 -1
  46. package/package.json +4 -4
  47. package/server.json +3 -3
package/AGENTS.md CHANGED
@@ -1,8 +1,8 @@
1
1
  # Developer Protocol
2
2
 
3
3
  **Server:** nist-nvd-mcp-server
4
- **Version:** 0.1.14
5
- **Framework:** [@cyanheads/mcp-ts-core](https://www.npmjs.com/package/@cyanheads/mcp-ts-core) `^0.10.14`
4
+ **Version:** 0.1.16
5
+ **Framework:** [@cyanheads/mcp-ts-core](https://www.npmjs.com/package/@cyanheads/mcp-ts-core) `^0.11.0`
6
6
  **Engines:** Bun ≥1.3.0, Node ≥24.0.0
7
7
  **MCP SDK:** `@modelcontextprotocol/sdk` ^1.29.0
8
8
  **Zod:** ^4.4.3
@@ -35,7 +35,7 @@ Tailor suggestions to what's actually missing or stale — don't recite the full
35
35
  - **Logic throws, framework catches.** Tool/resource handlers are pure — throw on failure, no `try/catch`. Plain `Error` is fine; the framework catches, classifies, and formats. Use error factories (`notFound()`, `validationError()`, etc.) when the error code matters.
36
36
  - **Use `ctx.log`** for request-scoped logging. No `console` calls.
37
37
  - **Use `ctx.state`** for tenant-scoped storage. Never access persistence directly.
38
- - **Check `ctx.elicit` / `ctx.sample`** for presence before calling.
38
+ - **Check `ctx.elicit`** for presence before calling.
39
39
  - **Secrets in env vars only** — never hardcoded.
40
40
  - **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.
41
41
 
@@ -67,12 +67,15 @@ export const nvdSearchCpes = tool('nvd_search_cpes', {
67
67
  title: z.string().optional().describe('Human-readable title.'),
68
68
  deprecated: z.boolean().describe('Whether this CPE is deprecated.'),
69
69
  })).describe('Matching CPE dictionary entries.'),
70
- queryMeta: z.object({
71
- totalResults: z.number().describe('Total matches before limit.'),
72
- returned: z.number().describe('Entries returned.'),
73
- }).describe('Pagination metadata.'),
74
70
  }),
75
71
 
72
+ // Pagination totals are agent-facing context, not domain payload — declare them as
73
+ // `enrichment` and populate via ctx.enrich so they reach structuredContent AND content[].
74
+ enrichment: {
75
+ totalCount: z.number().describe('Total matches before the limit was applied.'),
76
+ returned: z.number().describe('Entries returned.'),
77
+ },
78
+
76
79
  errors: [
77
80
  {
78
81
  reason: 'missing_search_input',
@@ -91,7 +94,9 @@ export const nvdSearchCpes = tool('nvd_search_cpes', {
91
94
  ctx.log.info('Searching CPE dictionary', { keyword: input.keyword, limit: input.limit });
92
95
  const service = getNvdCpeService();
93
96
  const result = await service.searchCpes({ keyword: input.keyword, cpeMatchString: input.cpeMatchString, limit: input.limit }, ctx);
94
- return { cpes: result.cpes, queryMeta: { totalResults: result.totalResults, returned: result.returned } };
97
+ ctx.enrich({ returned: result.returned });
98
+ ctx.enrich.total(result.totalResults);
99
+ return { cpes: result.cpes };
95
100
  },
96
101
 
97
102
  format: (result) => [{ type: 'text', text: result.cpes.map(c => `**${c.title ?? c.cpeName}**: \`${c.cpeName}\``).join('\n') }],
@@ -162,10 +167,14 @@ Handlers receive a unified `ctx` object. Key properties:
162
167
  | Property | Description |
163
168
  |:---------|:------------|
164
169
  | `ctx.log` | Request-scoped logger — `.debug()`, `.info()`, `.notice()`, `.warning()`, `.error()`. Auto-correlates requestId, traceId, tenantId. |
165
- | `ctx.state` | Tenant-scoped KV — `.get(key)`, `.set(key, value, { ttl? })`, `.delete(key)`, `.list(prefix, { cursor, limit })`. Accepts any serializable value. |
170
+ | `ctx.state` | Tenant-scoped KV — `.get(key)`, `.set(key, value, { ttl? })`, `.delete(key)`, `.getMany(keys)`, `.list(prefix, { cursor, limit })`. Accepts any serializable value. |
171
+ | `ctx.elicit` | Ask user for structured input — form call `(message, schema)` or `.url(message, url)` for an external link. **Check for presence first:** `if (ctx.elicit) { ... }` |
172
+ | `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). |
173
+ | `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`. |
166
174
  | `ctx.signal` | `AbortSignal` for cancellation. Forwarded to NVD HTTP requests. |
175
+ | `ctx.progress` | Task progress (present when `task: true`) — `.setTotal(n)`, `.increment()`, `.update(message)`. |
167
176
  | `ctx.requestId` | Unique request ID. |
168
- | `ctx.tenantId` | Tenant ID from JWT or `'default'` for stdio. |
177
+ | `ctx.tenantId` | Tenant ID from JWT; `'default'` for stdio or HTTP with auth off. |
169
178
 
170
179
  ---
171
180
 
@@ -279,6 +288,7 @@ Available skills:
279
288
  | `git-wrapup` | Land working-tree changes as a versioned commit + annotated tag — version bump, changelog, verify, tag. Local only. |
280
289
  | `release-and-publish` | Push + npm + MCP Registry + GH Release + Docker. Picks up from `git-wrapup` |
281
290
  | `maintenance` | Investigate changelogs, adopt upstream changes, sync skills to agent dirs |
291
+ | `orchestrations` | Chain task skills into a gated multi-phase pipeline — build-out, QA-fix, update-ship — when you can spawn sub-agents |
282
292
  | `report-issue-framework` | File a bug or feature request against `@cyanheads/mcp-ts-core` via `gh` CLI |
283
293
  | `report-issue-local` | File a bug or feature request against this server's own repo via `gh` CLI |
284
294
  | `techniques` | Catalog of response/data-shaping techniques — overflow handling, payload shaping, retrieval patterns |
@@ -288,13 +298,12 @@ Available skills:
288
298
  | `api-context` | Context interface, logger, state, progress |
289
299
  | `api-errors` | McpError, JsonRpcErrorCode, error patterns |
290
300
  | `api-linter` | Definition linter rule catalog — invoked by `bun run lint:mcp` and `devcheck` |
301
+ | `api-mirror` | MirrorService: persistent self-refreshing local mirror (embedded SQLite + FTS5) of a bulk upstream dataset — Tier 3 opt-in |
291
302
  | `api-services` | LLM, Speech, Graph services |
292
303
  | `api-testing` | createMockContext, test patterns |
293
304
  | `api-utils` | Formatting, parsing, security, pagination, scheduling, telemetry helpers |
294
305
  | `api-telemetry` | OTel catalog: spans, metrics, completion logs, env config, cardinality rules |
295
- | `api-mirror` | MirrorService: persistent local SQLite mirror of a bulk upstream dataset — defineMirror, sqliteMirrorStore, FTS5 |
296
306
  | `api-workers` | Cloudflare Workers runtime |
297
- | `orchestrations` | Chain task skills into a gated multi-phase pipeline (build-out, QA-fix, update-ship) when sub-agents are available |
298
307
 
299
308
  When you complete a skill's checklist, check the boxes and add a completion timestamp at the end (e.g., `Completed: 2026-03-11`).
300
309
 
@@ -310,16 +319,20 @@ When you complete a skill's checklist, check the boxes and add a completion time
310
319
  | `bun run rebuild` | Clean + build |
311
320
  | `bun run clean` | Remove build artifacts |
312
321
  | `bun run devcheck` | Lint + format + typecheck + security + changelog sync |
313
- | `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. |
322
+ | `bun run audit:refresh` | Delete `bun.lock`, reinstall, and re-run `bun audit`. Use when `devcheck` flags a transitive advisory — Bun's `update` is sticky on transitive resolutions, so the advisory may be a stale-lockfile false positive. If it survives the refresh, it's real. |
323
+ | `bun run lint:mcp` | Run the MCP definition linter standalone (rule catalog: `api-linter` skill) |
324
+ | `bun run lint:packaging` | Packaging surface checks — `server.json`/`manifest.json` env-var parity (run by devcheck) |
325
+ | `bun run list-skills` | Print the skill registry |
314
326
  | `bun run tree` | Generate directory structure doc |
315
- | `bun run format` | Auto-fix formatting |
316
- | `bun run test` | Run tests |
327
+ | `bun run format` | Auto-fix formatting (safe fixes only) |
328
+ | `bun run format:unsafe` | Also apply Biome's unsafe autofixes — review the diff; they can change behavior |
329
+ | `bun run test` | Run tests (Vitest — use `bun run test`, not `bun test`) |
317
330
  | `bun run start:stdio` | Production mode (stdio) |
318
331
  | `bun run start:http` | Production mode (HTTP) |
319
332
  | `bun run changelog:build` | Regenerate `CHANGELOG.md` from `changelog/*.md` |
320
333
  | `bun run changelog:check` | Verify `CHANGELOG.md` is in sync (used by devcheck) |
321
334
  | `bun run release:github` | Create GitHub Release — constructs title from tag, conditionally attaches `.mcpb` bundle |
322
- | `bun run bundle` | Build and pack as `.mcpb` for one-click Claude Desktop install |
335
+ | `bun run bundle` | Build, pack, and clean a `.mcpb` for one-click Claude Desktop install |
323
336
 
324
337
  ---
325
338
 
package/CLAUDE.md CHANGED
@@ -1,8 +1,8 @@
1
1
  # Developer Protocol
2
2
 
3
3
  **Server:** nist-nvd-mcp-server
4
- **Version:** 0.1.14
5
- **Framework:** [@cyanheads/mcp-ts-core](https://www.npmjs.com/package/@cyanheads/mcp-ts-core) `^0.10.14`
4
+ **Version:** 0.1.16
5
+ **Framework:** [@cyanheads/mcp-ts-core](https://www.npmjs.com/package/@cyanheads/mcp-ts-core) `^0.11.0`
6
6
  **Engines:** Bun ≥1.3.0, Node ≥24.0.0
7
7
  **MCP SDK:** `@modelcontextprotocol/sdk` ^1.29.0
8
8
  **Zod:** ^4.4.3
@@ -35,7 +35,7 @@ Tailor suggestions to what's actually missing or stale — don't recite the full
35
35
  - **Logic throws, framework catches.** Tool/resource handlers are pure — throw on failure, no `try/catch`. Plain `Error` is fine; the framework catches, classifies, and formats. Use error factories (`notFound()`, `validationError()`, etc.) when the error code matters.
36
36
  - **Use `ctx.log`** for request-scoped logging. No `console` calls.
37
37
  - **Use `ctx.state`** for tenant-scoped storage. Never access persistence directly.
38
- - **Check `ctx.elicit` / `ctx.sample`** for presence before calling.
38
+ - **Check `ctx.elicit`** for presence before calling.
39
39
  - **Secrets in env vars only** — never hardcoded.
40
40
  - **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.
41
41
 
@@ -67,12 +67,15 @@ export const nvdSearchCpes = tool('nvd_search_cpes', {
67
67
  title: z.string().optional().describe('Human-readable title.'),
68
68
  deprecated: z.boolean().describe('Whether this CPE is deprecated.'),
69
69
  })).describe('Matching CPE dictionary entries.'),
70
- queryMeta: z.object({
71
- totalResults: z.number().describe('Total matches before limit.'),
72
- returned: z.number().describe('Entries returned.'),
73
- }).describe('Pagination metadata.'),
74
70
  }),
75
71
 
72
+ // Pagination totals are agent-facing context, not domain payload — declare them as
73
+ // `enrichment` and populate via ctx.enrich so they reach structuredContent AND content[].
74
+ enrichment: {
75
+ totalCount: z.number().describe('Total matches before the limit was applied.'),
76
+ returned: z.number().describe('Entries returned.'),
77
+ },
78
+
76
79
  errors: [
77
80
  {
78
81
  reason: 'missing_search_input',
@@ -91,7 +94,9 @@ export const nvdSearchCpes = tool('nvd_search_cpes', {
91
94
  ctx.log.info('Searching CPE dictionary', { keyword: input.keyword, limit: input.limit });
92
95
  const service = getNvdCpeService();
93
96
  const result = await service.searchCpes({ keyword: input.keyword, cpeMatchString: input.cpeMatchString, limit: input.limit }, ctx);
94
- return { cpes: result.cpes, queryMeta: { totalResults: result.totalResults, returned: result.returned } };
97
+ ctx.enrich({ returned: result.returned });
98
+ ctx.enrich.total(result.totalResults);
99
+ return { cpes: result.cpes };
95
100
  },
96
101
 
97
102
  format: (result) => [{ type: 'text', text: result.cpes.map(c => `**${c.title ?? c.cpeName}**: \`${c.cpeName}\``).join('\n') }],
@@ -162,10 +167,14 @@ Handlers receive a unified `ctx` object. Key properties:
162
167
  | Property | Description |
163
168
  |:---------|:------------|
164
169
  | `ctx.log` | Request-scoped logger — `.debug()`, `.info()`, `.notice()`, `.warning()`, `.error()`. Auto-correlates requestId, traceId, tenantId. |
165
- | `ctx.state` | Tenant-scoped KV — `.get(key)`, `.set(key, value, { ttl? })`, `.delete(key)`, `.list(prefix, { cursor, limit })`. Accepts any serializable value. |
170
+ | `ctx.state` | Tenant-scoped KV — `.get(key)`, `.set(key, value, { ttl? })`, `.delete(key)`, `.getMany(keys)`, `.list(prefix, { cursor, limit })`. Accepts any serializable value. |
171
+ | `ctx.elicit` | Ask user for structured input — form call `(message, schema)` or `.url(message, url)` for an external link. **Check for presence first:** `if (ctx.elicit) { ... }` |
172
+ | `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). |
173
+ | `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`. |
166
174
  | `ctx.signal` | `AbortSignal` for cancellation. Forwarded to NVD HTTP requests. |
175
+ | `ctx.progress` | Task progress (present when `task: true`) — `.setTotal(n)`, `.increment()`, `.update(message)`. |
167
176
  | `ctx.requestId` | Unique request ID. |
168
- | `ctx.tenantId` | Tenant ID from JWT or `'default'` for stdio. |
177
+ | `ctx.tenantId` | Tenant ID from JWT; `'default'` for stdio or HTTP with auth off. |
169
178
 
170
179
  ---
171
180
 
@@ -279,6 +288,7 @@ Available skills:
279
288
  | `git-wrapup` | Land working-tree changes as a versioned commit + annotated tag — version bump, changelog, verify, tag. Local only. |
280
289
  | `release-and-publish` | Push + npm + MCP Registry + GH Release + Docker. Picks up from `git-wrapup` |
281
290
  | `maintenance` | Investigate changelogs, adopt upstream changes, sync skills to agent dirs |
291
+ | `orchestrations` | Chain task skills into a gated multi-phase pipeline — build-out, QA-fix, update-ship — when you can spawn sub-agents |
282
292
  | `report-issue-framework` | File a bug or feature request against `@cyanheads/mcp-ts-core` via `gh` CLI |
283
293
  | `report-issue-local` | File a bug or feature request against this server's own repo via `gh` CLI |
284
294
  | `techniques` | Catalog of response/data-shaping techniques — overflow handling, payload shaping, retrieval patterns |
@@ -288,13 +298,12 @@ Available skills:
288
298
  | `api-context` | Context interface, logger, state, progress |
289
299
  | `api-errors` | McpError, JsonRpcErrorCode, error patterns |
290
300
  | `api-linter` | Definition linter rule catalog — invoked by `bun run lint:mcp` and `devcheck` |
301
+ | `api-mirror` | MirrorService: persistent self-refreshing local mirror (embedded SQLite + FTS5) of a bulk upstream dataset — Tier 3 opt-in |
291
302
  | `api-services` | LLM, Speech, Graph services |
292
303
  | `api-testing` | createMockContext, test patterns |
293
304
  | `api-utils` | Formatting, parsing, security, pagination, scheduling, telemetry helpers |
294
305
  | `api-telemetry` | OTel catalog: spans, metrics, completion logs, env config, cardinality rules |
295
- | `api-mirror` | MirrorService: persistent local SQLite mirror of a bulk upstream dataset — defineMirror, sqliteMirrorStore, FTS5 |
296
306
  | `api-workers` | Cloudflare Workers runtime |
297
- | `orchestrations` | Chain task skills into a gated multi-phase pipeline (build-out, QA-fix, update-ship) when sub-agents are available |
298
307
 
299
308
  When you complete a skill's checklist, check the boxes and add a completion timestamp at the end (e.g., `Completed: 2026-03-11`).
300
309
 
@@ -310,16 +319,20 @@ When you complete a skill's checklist, check the boxes and add a completion time
310
319
  | `bun run rebuild` | Clean + build |
311
320
  | `bun run clean` | Remove build artifacts |
312
321
  | `bun run devcheck` | Lint + format + typecheck + security + changelog sync |
313
- | `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. |
322
+ | `bun run audit:refresh` | Delete `bun.lock`, reinstall, and re-run `bun audit`. Use when `devcheck` flags a transitive advisory — Bun's `update` is sticky on transitive resolutions, so the advisory may be a stale-lockfile false positive. If it survives the refresh, it's real. |
323
+ | `bun run lint:mcp` | Run the MCP definition linter standalone (rule catalog: `api-linter` skill) |
324
+ | `bun run lint:packaging` | Packaging surface checks — `server.json`/`manifest.json` env-var parity (run by devcheck) |
325
+ | `bun run list-skills` | Print the skill registry |
314
326
  | `bun run tree` | Generate directory structure doc |
315
- | `bun run format` | Auto-fix formatting |
316
- | `bun run test` | Run tests |
327
+ | `bun run format` | Auto-fix formatting (safe fixes only) |
328
+ | `bun run format:unsafe` | Also apply Biome's unsafe autofixes — review the diff; they can change behavior |
329
+ | `bun run test` | Run tests (Vitest — use `bun run test`, not `bun test`) |
317
330
  | `bun run start:stdio` | Production mode (stdio) |
318
331
  | `bun run start:http` | Production mode (HTTP) |
319
332
  | `bun run changelog:build` | Regenerate `CHANGELOG.md` from `changelog/*.md` |
320
333
  | `bun run changelog:check` | Verify `CHANGELOG.md` is in sync (used by devcheck) |
321
334
  | `bun run release:github` | Create GitHub Release — constructs title from tag, conditionally attaches `.mcpb` bundle |
322
- | `bun run bundle` | Build and pack as `.mcpb` for one-click Claude Desktop install |
335
+ | `bun run bundle` | Build, pack, and clean a `.mcpb` for one-click Claude Desktop install |
323
336
 
324
337
  ---
325
338
 
package/Dockerfile CHANGED
@@ -60,7 +60,7 @@ RUN --mount=type=cache,target=/root/.bun/install/cache \
60
60
  ARG OTEL_ENABLED=true
61
61
  RUN --mount=type=cache,target=/root/.bun/install/cache \
62
62
  if [ "$OTEL_ENABLED" = "true" ]; then \
63
- bun add @hono/otel \
63
+ bun add --omit=dev --ignore-scripts @hono/otel \
64
64
  @opentelemetry/instrumentation-http \
65
65
  @opentelemetry/exporter-metrics-otlp-http \
66
66
  @opentelemetry/exporter-trace-otlp-http \
package/README.md CHANGED
@@ -7,7 +7,7 @@
7
7
 
8
8
  <div align="center">
9
9
 
10
- [![Version](https://img.shields.io/badge/Version-0.1.14-blue.svg?style=flat-square)](./CHANGELOG.md) [![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/nist-nvd-mcp-server) [![MCP SDK](https://img.shields.io/badge/MCP%20SDK-^1.29.0-green.svg?style=flat-square)](https://modelcontextprotocol.io/) [![npm](https://img.shields.io/npm/v/@cyanheads/nist-nvd-mcp-server?style=flat-square&logo=npm&logoColor=white)](https://www.npmjs.com/package/@cyanheads/nist-nvd-mcp-server) [![TypeScript](https://img.shields.io/badge/TypeScript-^6.0.3-3178C6.svg?style=flat-square)](https://www.typescriptlang.org/) [![Bun](https://img.shields.io/badge/Bun-v1.3.14-blueviolet.svg?style=flat-square)](https://bun.sh/)
10
+ [![Version](https://img.shields.io/badge/Version-0.1.16-blue.svg?style=flat-square)](./CHANGELOG.md) [![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/nist-nvd-mcp-server) [![MCP SDK](https://img.shields.io/badge/MCP%20SDK-^1.29.0-green.svg?style=flat-square)](https://modelcontextprotocol.io/) [![npm](https://img.shields.io/npm/v/@cyanheads/nist-nvd-mcp-server?style=flat-square&logo=npm&logoColor=white)](https://www.npmjs.com/package/@cyanheads/nist-nvd-mcp-server) [![TypeScript](https://img.shields.io/badge/TypeScript-^6.0.3-3178C6.svg?style=flat-square)](https://www.typescriptlang.org/) [![Bun](https://img.shields.io/badge/Bun-v1.3.14-blueviolet.svg?style=flat-square)](https://bun.sh/)
11
11
 
12
12
  </div>
13
13
 
@@ -49,8 +49,9 @@ The primary discovery tool for vulnerability surveillance and triage workflows.
49
49
  - CISA KEV filter — limit results to known-exploited vulnerabilities
50
50
  - Convenience date shorthands: `pubDays` and `lastModDays` for "last N days" queries
51
51
  - Explicit ISO 8601 date range parameters (`pubStartDate`/`pubEndDate`, etc.) with 120-day max span
52
- - Auto-clamps convenience date params that exceed 120 days and reports clamped values in `queryMeta`
52
+ - Auto-clamps convenience date params that exceed 120 days and reports clamped values in the response enrichment
53
53
  - Pagination via `limit` (up to 2000) and `offset`
54
+ - Every row carries a truncated description alongside the ID, so results are distinguishable without a follow-up fetch
54
55
  - Results are always brief; call `nvd_get_cve` for full detail
55
56
 
56
57
  ---
@@ -61,9 +62,10 @@ Fetch one or more CVEs by ID with full detail or brief summaries.
61
62
 
62
63
  - Batch up to 100 CVE IDs per call
63
64
  - Full mode: all CVSS scores across v2.0, v3.0, v3.1, and v4.0; CWE weaknesses; CPE configurations; CISA KEV fields; references
64
- - Brief mode (`brief: true`): ID, status, top severity, KEV name — recommended for batches larger than 10
65
+ - Brief mode (`brief: true`): ID, status, top severity, KEV name, truncated description — recommended for batches larger than 10
65
66
  - `includeReferences: false` to strip the references array and reduce response size
66
- - Per-ID parity check: `queryMeta.missingIds` lists any requested IDs NVD didn't return
67
+ - Per-ID parity check: the `missingIds` enrichment field lists any requested IDs NVD didn't return
68
+ - Rendered text carries the affected-product criteria and references the record holds, capped with a `… N more` trailer; `allLanguages: true` renders every localized description, not just English
67
69
 
68
70
  ---
69
71
 
@@ -73,7 +75,7 @@ Look up product identifiers before auditing.
73
75
 
74
76
  - Keyword search (e.g., `"apache http server"`, `"openssl"`) or partial CPEv2.3 pattern
75
77
  - Returns full CPE name, human-readable title, deprecation status, and superseding CPEs
76
- - Pagination up to 10,000 entries — narrow the keyword when `totalResults > returned`
78
+ - Pagination via `limit` (up to 10,000 per page) and `offset` — a vendor-level keyword can match tens of thousands of entries, so page with `offset` rather than trying to narrow further
77
79
  - Use this before `nvd_audit_cpe` — CPE names are arcane strings; guessing audits the wrong product
78
80
 
79
81
  ---
@@ -86,7 +88,8 @@ Full CVE audit for a specific product version.
86
88
  - Version range via `versionStart`/`versionEnd` with inclusive/exclusive type control
87
89
  - Client-side severity filter (`severityMin`) to strip low-signal entries
88
90
  - Returns full CVE records (ID, CVSS scores, CWE, CPE configurations, KEV fields, references)
89
- - Echoes the CPE identifier used in `queryMeta` so callers can verify the correct product was queried
91
+ - Pagination via `limit` (up to 2000) and `offset` — page at a modest `limit` instead of raising it, since each result is a full record
92
+ - Echoes the CPE identifier used in the response enrichment so callers can verify the correct product was queried
90
93
 
91
94
  ---
92
95
 
@@ -94,8 +97,9 @@ Full CVE audit for a specific product version.
94
97
 
95
98
  Track a CVE's lifecycle over time.
96
99
 
97
- - Returns change events in reverse-chronological order: CVSS revisions, status transitions, reference additions, CPE configuration updates
98
- - Paginated via `limit` and `offset`
100
+ - Returns change events: CVSS revisions, status transitions, reference additions, CPE configuration updates
101
+ - `order` picks which end to read from — `newest` (default) returns the most recent events first, `oldest` returns NVD's native oldest-first order
102
+ - Paginated via `limit` and `offset`, where `offset` counts from the end `order` anchors to
99
103
  - Note: the NVD history endpoint is significantly slower without an API key — set `NVD_API_KEY` and raise `NVD_REQUEST_TIMEOUT_MS` for reliable operation
100
104
 
101
105
  ## Resource
@@ -127,9 +131,10 @@ NVD-specific:
127
131
 
128
132
  Agent-friendly output:
129
133
 
130
- - `queryMeta` on every response — total results, returned count, page offset, and any date-clamping events so agents can reason about what was actually queried
134
+ - An `enrichment` block on every response, carried on both `structuredContent` and the rendered text — total results, returned count, page offset, the filters actually applied, and any date-clamping events, so agents can reason about what was really queried
131
135
  - `missingIds` in batch CVE lookups — per-ID parity check instead of a silent partial result
132
136
  - CPE echo in audit responses — `cpeName` or `virtualMatchString` reflected back so callers can verify the correct product was audited
137
+ - Empty-result notices that name the cause — an unmatched query, a severity threshold that emptied the page, and an offset past the end of the result set are told apart rather than all reading as "nothing found"
133
138
 
134
139
  ## Getting started
135
140
 
@@ -0,0 +1,24 @@
1
+ ---
2
+ summary: "nvd_get_cve_history gains an order input (newest/oldest) and no longer loses pages carrying Affected/SSVC change values; invalid_cve_id_format and cve_not_found now carry their declared recovery hints; nvd_search_cves' severity filter is documented as exact-band, not a floor."
3
+ breaking: false
4
+ security: false
5
+ ---
6
+
7
+ # 0.1.15 — 2026-07-27
8
+
9
+ ## Added
10
+
11
+ - **`order` input on `nvd_get_cve_history`** (`oldest` | `newest`, default `newest`) — pages change history from either end. `oldest` maps `offset` straight to NVD's native `startIndex` in one request; `newest` reverses it, adding a second tail-anchored request only when the history is longer than `limit`. The effective order is echoed in the `enrichment` block. ([#29](https://github.com/cyanheads/nist-nvd-mcp-server/issues/29))
12
+
13
+ ## Fixed
14
+
15
+ - **`nvd_get_cve_history` failed output validation on pages carrying `Affected` or `SSVC` changes** — NVD emits an array for `Affected` details and an object for `SSVC` details, which the `oldValue`/`newValue` string schema rejected, losing the entire page. Non-string values now serialize to JSON strings during normalization. ([#27](https://github.com/cyanheads/nist-nvd-mcp-server/issues/27))
16
+ - **`nvd_get_cve_history`'s `changes` output was documented as reverse-chronological but returned NVD's native oldest-first order** — the description now states the real ordering, resolved by the new `order` input above. ([#29](https://github.com/cyanheads/nist-nvd-mcp-server/issues/29))
17
+ - **`invalid_cve_id_format` and `cve_not_found` shipped no recovery hint** — both were thrown from `NvdCveService` via bare `validationError()`/`notFound()` rather than `ctx.fail()`, so the recovery text declared on `nvd_get_cve`, `nvd_get_cve_history`, and the `nvd://cve/{cveId}` resource never reached the caller. Throw sites now resolve `ctx.recoveryFor(reason)` so each carries the calling definition's own hint. ([#33](https://github.com/cyanheads/nist-nvd-mcp-server/issues/33))
18
+ - **`nvd_search_cves`' `severity` input described itself as "or above"** — NVD's severity parameters are exact-match, not a floor, so a `severity: "HIGH"` query silently dropped every `CRITICAL` result. The `severity` input and `filtersApplied.severity` enrichment now state the filter selects exactly one band and that covering several bands takes one call per band. ([#28](https://github.com/cyanheads/nist-nvd-mcp-server/issues/28))
19
+
20
+ ## Dependencies
21
+
22
+ - `@cyanheads/mcp-ts-core` ^0.10.14 → ^0.11.0
23
+ - `@biomejs/biome` ^2.5.3 → ^2.5.5
24
+ - `tsc-alias` ^1.9.0 → ^1.9.1
@@ -0,0 +1,21 @@
1
+ ---
2
+ summary: "nvd_audit_cpe and nvd_search_cpes gain offset paging; nvd_get_cve's format() renders CPE match criteria and every language a record carries instead of a bare count and English-only text; search results carry a truncated description; docs/design.md and README describe the enrichment block instead of the removed queryMeta envelope."
3
+ breaking: false
4
+ security: false
5
+ ---
6
+
7
+ # 0.1.16 — 2026-07-27
8
+
9
+ ## Added
10
+
11
+ - **`offset` input on `nvd_audit_cpe` and `nvd_search_cpes`** — zero-based, threaded to NVD's `startIndex` and echoed in the `enrichment` block alongside `totalCount` and `returned`, matching the pattern `nvd_search_cves` already used. `nvd_search_cpes`'s truncation notice now names the offset that reaches the remainder instead of telling the caller to narrow an already vendor-level keyword; both tools distinguish an offset past the end of the result set from their other empty-result cases. ([#31](https://github.com/cyanheads/nist-nvd-mcp-server/issues/31))
12
+ - **Truncated description on brief CVE rows** — `BriefCveRecord` gains an optional `description` (first 200 characters of the English text, exported as `BRIEF_DESCRIPTION_CHARS`), populated on both `nvd_search_cves` results and `nvd_get_cve`'s `brief: true` mode. Lets a caller distinguish search hits without a follow-up full-detail fetch. ([#32](https://github.com/cyanheads/nist-nvd-mcp-server/issues/32))
13
+
14
+ ## Changed
15
+
16
+ - **`nvd_get_cve`'s `format()` now matches `structuredContent`** — CPE match criteria render through the shared `formatCpeMatch()`/`flattenCpeMatches()` helpers (moved to `src/mcp-server/tools/formatting/cpe-match.ts`, also used by `nvd_audit_cpe`) instead of a bare node-group count, capped at 5 with a trailer pointing at `nvd_audit_cpe` for the remainder; every language a record carries renders under `allLanguages` instead of only English; references cap raised from 5 to 15. ([#30](https://github.com/cyanheads/nist-nvd-mcp-server/issues/30))
17
+ - **`docs/design.md`, `README.md`, and the tool template in `CLAUDE.md`/`AGENTS.md` describe the `enrichment` block** — all three still described the `queryMeta` output envelope, which stopped being emitted several releases ago. Rewritten against each tool's actual `enrichment` fields, including ones added since the drift started (`filtersApplied`, `severityMin`, `filteredCount`, `offset`). ([#26](https://github.com/cyanheads/nist-nvd-mcp-server/issues/26))
18
+
19
+ ## Fixed
20
+
21
+ - **`nvd://cve/{cveId}` resource carried a dead empty-result branch** — a single-ID miss always throws `cve_not_found` inside `fetchById`, so the resource's own `result.cves.length === 0` check could never execute. Removed; tests retargeted to exercise the real throw path over a mocked HTTP client.
@@ -3,7 +3,23 @@
3
3
  * @module src/mcp-server/resources/definitions/nvd-cve
4
4
  */
5
5
  import { z } from '@cyanheads/mcp-ts-core';
6
+ import { JsonRpcErrorCode } from '@cyanheads/mcp-ts-core/errors';
6
7
  export declare const nvdCveResource: import("@cyanheads/mcp-ts-core").ResourceDefinition<z.ZodObject<{
7
8
  cveId: z.ZodString;
8
- }, z.core.$strip>, undefined, undefined>;
9
+ }, z.core.$strip>, undefined, readonly [{
10
+ readonly reason: "invalid_cve_id_format";
11
+ readonly code: JsonRpcErrorCode.ValidationError;
12
+ readonly when: "The cveId segment of the URI fails format validation.";
13
+ readonly recovery: "Use a URI of the form nvd://cve/CVE-YYYY-NNNNN, for example nvd://cve/CVE-2021-44228.";
14
+ }, {
15
+ /**
16
+ * Thrown by the service, not this handler: a single-ID fetch that NVD answers with no
17
+ * records raises `cve_not_found` there. The entry stays declared here so the service's
18
+ * `ctx.recoveryFor('cve_not_found')` resolves this recovery text onto the wire.
19
+ */
20
+ readonly reason: "cve_not_found";
21
+ readonly code: JsonRpcErrorCode.NotFound;
22
+ readonly when: "The CVE ID is well-formed but NVD holds no record for it.";
23
+ readonly recovery: "Verify the CVE ID is correct, or use nvd_search_cves to find it by keyword or date range.";
24
+ }]>;
9
25
  //# sourceMappingURL=nvd-cve.resource.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"nvd-cve.resource.d.ts","sourceRoot":"","sources":["../../../../src/mcp-server/resources/definitions/nvd-cve.resource.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAY,CAAC,EAAE,MAAM,wBAAwB,CAAC;AAMrD,eAAO,MAAM,cAAc;;wCAqCzB,CAAC"}
1
+ {"version":3,"file":"nvd-cve.resource.d.ts","sourceRoot":"","sources":["../../../../src/mcp-server/resources/definitions/nvd-cve.resource.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAY,CAAC,EAAE,MAAM,wBAAwB,CAAC;AACrD,OAAO,EAAE,gBAAgB,EAAE,MAAM,+BAA+B,CAAC;AAKjE,eAAO,MAAM,cAAc;;;;;;;;IAuBrB;;;;OAIG;;;;;GA+BP,CAAC"}
@@ -3,7 +3,7 @@
3
3
  * @module src/mcp-server/resources/definitions/nvd-cve
4
4
  */
5
5
  import { resource, z } from '@cyanheads/mcp-ts-core';
6
- import { notFound, validationError } from '@cyanheads/mcp-ts-core/errors';
6
+ import { JsonRpcErrorCode } from '@cyanheads/mcp-ts-core/errors';
7
7
  import { getNvdCveService } from '../../../services/nvd-cve/nvd-cve-service.js';
8
8
  const CVE_ID_REGEX = /^CVE-\d{4}-\d{4,}$/i;
9
9
  export const nvdCveResource = resource('nvd://cve/{cveId}', {
@@ -17,19 +17,34 @@ export const nvdCveResource = resource('nvd://cve/{cveId}', {
17
17
  .string()
18
18
  .describe('CVE identifier (e.g., "CVE-2021-44228"). Must match the format CVE-YYYY-NNNNN.'),
19
19
  }),
20
+ errors: [
21
+ {
22
+ reason: 'invalid_cve_id_format',
23
+ code: JsonRpcErrorCode.ValidationError,
24
+ when: 'The cveId segment of the URI fails format validation.',
25
+ recovery: 'Use a URI of the form nvd://cve/CVE-YYYY-NNNNN, for example nvd://cve/CVE-2021-44228.',
26
+ },
27
+ {
28
+ /**
29
+ * Thrown by the service, not this handler: a single-ID fetch that NVD answers with no
30
+ * records raises `cve_not_found` there. The entry stays declared here so the service's
31
+ * `ctx.recoveryFor('cve_not_found')` resolves this recovery text onto the wire.
32
+ */
33
+ reason: 'cve_not_found',
34
+ code: JsonRpcErrorCode.NotFound,
35
+ when: 'The CVE ID is well-formed but NVD holds no record for it.',
36
+ recovery: 'Verify the CVE ID is correct, or use nvd_search_cves to find it by keyword or date range.',
37
+ },
38
+ ],
20
39
  async handler(params, ctx) {
21
40
  const { cveId } = params;
22
41
  if (!CVE_ID_REGEX.test(cveId)) {
23
- throw validationError(`Invalid CVE ID format: "${cveId}". Expected format: CVE-YYYY-NNNNN.`, {
24
- cveId,
25
- });
42
+ throw ctx.fail('invalid_cve_id_format', `Invalid CVE ID format: "${cveId}". Expected format: CVE-YYYY-NNNNN.`, { cveId, ...ctx.recoveryFor('invalid_cve_id_format') });
26
43
  }
27
44
  ctx.log.debug('Fetching CVE resource', { cveId });
28
45
  const service = getNvdCveService();
46
+ // A single-ID miss throws `cve_not_found` inside fetchById, so this always has a record.
29
47
  const result = await service.fetchById([cveId.toUpperCase()], { includeReferences: true, allLanguages: false }, ctx);
30
- if (result.cves.length === 0) {
31
- throw notFound(`CVE ${cveId} not found in the NVD database.`, { cveId });
32
- }
33
48
  return result.cves[0];
34
49
  },
35
50
  });
@@ -1 +1 @@
1
- {"version":3,"file":"nvd-cve.resource.js","sourceRoot":"","sources":["../../../../src/mcp-server/resources/definitions/nvd-cve.resource.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAE,QAAQ,EAAE,CAAC,EAAE,MAAM,wBAAwB,CAAC;AACrD,OAAO,EAAE,QAAQ,EAAE,eAAe,EAAE,MAAM,+BAA+B,CAAC;AAC1E,OAAO,EAAE,gBAAgB,EAAE,MAAM,uCAAuC,CAAC;AAEzE,MAAM,YAAY,GAAG,qBAAqB,CAAC;AAE3C,MAAM,CAAC,MAAM,cAAc,GAAG,QAAQ,CAAC,mBAAmB,EAAE;IAC1D,IAAI,EAAE,gBAAgB;IACtB,WAAW,EACT,sEAAsE;QACtE,0FAA0F;QAC1F,sDAAsD;IACxD,QAAQ,EAAE,kBAAkB;IAE5B,MAAM,EAAE,CAAC,CAAC,MAAM,CAAC;QACf,KAAK,EAAE,CAAC;aACL,MAAM,EAAE;aACR,QAAQ,CAAC,gFAAgF,CAAC;KAC9F,CAAC;IAEF,KAAK,CAAC,OAAO,CAAC,MAAM,EAAE,GAAG;QACvB,MAAM,EAAE,KAAK,EAAE,GAAG,MAAM,CAAC;QACzB,IAAI,CAAC,YAAY,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC;YAC9B,MAAM,eAAe,CAAC,2BAA2B,KAAK,qCAAqC,EAAE;gBAC3F,KAAK;aACN,CAAC,CAAC;QACL,CAAC;QAED,GAAG,CAAC,GAAG,CAAC,KAAK,CAAC,uBAAuB,EAAE,EAAE,KAAK,EAAE,CAAC,CAAC;QAClD,MAAM,OAAO,GAAG,gBAAgB,EAAE,CAAC;QAEnC,MAAM,MAAM,GAAG,MAAM,OAAO,CAAC,SAAS,CACpC,CAAC,KAAK,CAAC,WAAW,EAAE,CAAC,EACrB,EAAE,iBAAiB,EAAE,IAAI,EAAE,YAAY,EAAE,KAAK,EAAE,EAChD,GAAG,CACJ,CAAC;QAEF,IAAI,MAAM,CAAC,IAAI,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YAC7B,MAAM,QAAQ,CAAC,OAAO,KAAK,iCAAiC,EAAE,EAAE,KAAK,EAAE,CAAC,CAAC;QAC3E,CAAC;QAED,OAAO,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IACxB,CAAC;CACF,CAAC,CAAC"}
1
+ {"version":3,"file":"nvd-cve.resource.js","sourceRoot":"","sources":["../../../../src/mcp-server/resources/definitions/nvd-cve.resource.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAE,QAAQ,EAAE,CAAC,EAAE,MAAM,wBAAwB,CAAC;AACrD,OAAO,EAAE,gBAAgB,EAAE,MAAM,+BAA+B,CAAC;AACjE,OAAO,EAAE,gBAAgB,EAAE,MAAM,uCAAuC,CAAC;AAEzE,MAAM,YAAY,GAAG,qBAAqB,CAAC;AAE3C,MAAM,CAAC,MAAM,cAAc,GAAG,QAAQ,CAAC,mBAAmB,EAAE;IAC1D,IAAI,EAAE,gBAAgB;IACtB,WAAW,EACT,sEAAsE;QACtE,0FAA0F;QAC1F,sDAAsD;IACxD,QAAQ,EAAE,kBAAkB;IAE5B,MAAM,EAAE,CAAC,CAAC,MAAM,CAAC;QACf,KAAK,EAAE,CAAC;aACL,MAAM,EAAE;aACR,QAAQ,CAAC,gFAAgF,CAAC;KAC9F,CAAC;IAEF,MAAM,EAAE;QACN;YACE,MAAM,EAAE,uBAAuB;YAC/B,IAAI,EAAE,gBAAgB,CAAC,eAAe;YACtC,IAAI,EAAE,uDAAuD;YAC7D,QAAQ,EACN,uFAAuF;SAC1F;QACD;YACE;;;;eAIG;YACH,MAAM,EAAE,eAAe;YACvB,IAAI,EAAE,gBAAgB,CAAC,QAAQ;YAC/B,IAAI,EAAE,2DAA2D;YACjE,QAAQ,EACN,2FAA2F;SAC9F;KACF;IAED,KAAK,CAAC,OAAO,CAAC,MAAM,EAAE,GAAG;QACvB,MAAM,EAAE,KAAK,EAAE,GAAG,MAAM,CAAC;QACzB,IAAI,CAAC,YAAY,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC;YAC9B,MAAM,GAAG,CAAC,IAAI,CACZ,uBAAuB,EACvB,2BAA2B,KAAK,qCAAqC,EACrE,EAAE,KAAK,EAAE,GAAG,GAAG,CAAC,WAAW,CAAC,uBAAuB,CAAC,EAAE,CACvD,CAAC;QACJ,CAAC;QAED,GAAG,CAAC,GAAG,CAAC,KAAK,CAAC,uBAAuB,EAAE,EAAE,KAAK,EAAE,CAAC,CAAC;QAClD,MAAM,OAAO,GAAG,gBAAgB,EAAE,CAAC;QAEnC,yFAAyF;QACzF,MAAM,MAAM,GAAG,MAAM,OAAO,CAAC,SAAS,CACpC,CAAC,KAAK,CAAC,WAAW,EAAE,CAAC,EACrB,EAAE,iBAAiB,EAAE,IAAI,EAAE,YAAY,EAAE,KAAK,EAAE,EAChD,GAAG,CACJ,CAAC;QAEF,OAAO,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IACxB,CAAC;CACF,CAAC,CAAC"}
@@ -25,6 +25,7 @@ export declare const nvdAuditCpe: import("@cyanheads/mcp-ts-core").ToolDefinitio
25
25
  }>>;
26
26
  allLanguages: z.ZodDefault<z.ZodBoolean>;
27
27
  limit: z.ZodDefault<z.ZodNumber>;
28
+ offset: z.ZodDefault<z.ZodNumber>;
28
29
  }, z.core.$strip>, z.ZodObject<{
29
30
  cves: z.ZodArray<z.ZodObject<{
30
31
  cveId: z.ZodString;
@@ -111,6 +112,7 @@ export declare const nvdAuditCpe: import("@cyanheads/mcp-ts-core").ToolDefinitio
111
112
  }], {
112
113
  readonly totalCount: z.ZodNumber;
113
114
  readonly returned: z.ZodNumber;
115
+ readonly offset: z.ZodNumber;
114
116
  readonly auditTarget: z.ZodString;
115
117
  readonly severityMin: z.ZodOptional<z.ZodString>;
116
118
  readonly filteredCount: z.ZodOptional<z.ZodNumber>;
@@ -1 +1 @@
1
- {"version":3,"file":"nvd-audit-cpe.tool.d.ts","sourceRoot":"","sources":["../../../../src/mcp-server/tools/definitions/nvd-audit-cpe.tool.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAQ,CAAC,EAAE,MAAM,wBAAwB,CAAC;AACjD,OAAO,EAAE,gBAAgB,EAAE,MAAM,+BAA+B,CAAC;AAsJjE,eAAO,MAAM,WAAW;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAyTtB,CAAC"}
1
+ {"version":3,"file":"nvd-audit-cpe.tool.d.ts","sourceRoot":"","sources":["../../../../src/mcp-server/tools/definitions/nvd-audit-cpe.tool.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAQ,CAAC,EAAE,MAAM,wBAAwB,CAAC;AACjD,OAAO,EAAE,gBAAgB,EAAE,MAAM,+BAA+B,CAAC;AA0HjE,eAAO,MAAM,WAAW;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EA2UtB,CAAC"}
@@ -4,6 +4,7 @@
4
4
  */
5
5
  import { tool, z } from '@cyanheads/mcp-ts-core';
6
6
  import { JsonRpcErrorCode } from '@cyanheads/mcp-ts-core/errors';
7
+ import { CPE_MATCH_CAP, flattenCpeMatches } from '../../../mcp-server/tools/formatting/cpe-match.js';
7
8
  import { getNvdCveService } from '../../../services/nvd-cve/nvd-cve-service.js';
8
9
  const CPE_V23_REGEX = /^cpe:2\.3:/i;
9
10
  const CvssScoreSchema = z.object({
@@ -28,30 +29,8 @@ const CpeMatchSchema = z.object({
28
29
  versionEndIncluding: z.string().optional().describe('Inclusive upper version bound.'),
29
30
  versionEndExcluding: z.string().optional().describe('Exclusive upper version bound.'),
30
31
  });
31
- /** Cap on CPE match criteria rendered per CVE — mirrors the references cap in this formatter. */
32
- const CPE_MATCH_CAP = 5;
33
- /**
34
- * Render one CPE match as a single line: the criteria string, its version bounds, and the
35
- * operators that govern it. `nodeOperator` combines the matches inside a node; `groupOperator`
36
- * combines sibling nodes in the group, so an `AND` there marks conditions that hold together
37
- * (e.g. a firmware match and the hardware it runs on) rather than independent alternatives.
38
- */
39
- function formatCpeMatch(match, groupOperator, nodeOperator) {
40
- const bounds = [
41
- match.versionStartIncluding && `>= ${match.versionStartIncluding}`,
42
- match.versionStartExcluding && `> ${match.versionStartExcluding}`,
43
- match.versionEndIncluding && `<= ${match.versionEndIncluding}`,
44
- match.versionEndExcluding && `< ${match.versionEndExcluding}`,
45
- ].filter(Boolean);
46
- const notes = [
47
- nodeOperator,
48
- groupOperator && `${groupOperator} with sibling nodes`,
49
- match.vulnerable ? undefined : 'not the vulnerable component',
50
- ].filter(Boolean);
51
- return (`${match.criteria}` +
52
- (bounds.length > 0 ? ` (${bounds.join(', ')})` : '') +
53
- (notes.length > 0 ? ` [${notes.join('; ')}]` : ''));
54
- }
32
+ /** References rendered per CVE — this tool returns full records for many CVEs at once. */
33
+ const REFERENCE_CAP = 5;
55
34
  const CveRecordSchema = z.object({
56
35
  cveId: z.string().describe('CVE identifier (e.g., "CVE-2021-44228").'),
57
36
  vulnStatus: z.string().describe('NVD analysis status.'),
@@ -172,6 +151,13 @@ export const nvdAuditCpe = tool('nvd_audit_cpe', {
172
151
  .max(2000)
173
152
  .default(20)
174
153
  .describe('Maximum number of CVEs to return (default 20, max 2000).'),
154
+ offset: z
155
+ .number()
156
+ .int()
157
+ .min(0)
158
+ .default(0)
159
+ .describe('Zero-based page offset for pagination. Page through totalCount with a modest limit ' +
160
+ 'rather than raising limit — this tool returns full CVE records, so a large limit is a large response.'),
175
161
  }),
176
162
  output: z.object({
177
163
  cves: z
@@ -181,6 +167,7 @@ export const nvdAuditCpe = tool('nvd_audit_cpe', {
181
167
  enrichment: {
182
168
  totalCount: z.number().describe('Total CVEs matched before pagination.'),
183
169
  returned: z.number().describe('Number of CVE records returned.'),
170
+ offset: z.number().describe('Page offset used in this query.'),
184
171
  auditTarget: z.string().describe('The CPE name or virtual match string used for this audit.'),
185
172
  severityMin: z
186
173
  .string()
@@ -197,6 +184,7 @@ export const nvdAuditCpe = tool('nvd_audit_cpe', {
197
184
  },
198
185
  enrichmentTrailer: {
199
186
  returned: { label: 'Returned' },
187
+ offset: { label: 'Offset' },
200
188
  auditTarget: { label: 'Audit Target' },
201
189
  severityMin: { label: 'Severity Filter' },
202
190
  filteredCount: { label: 'Dropped by Severity Filter' },
@@ -261,6 +249,7 @@ export const nvdAuditCpe = tool('nvd_audit_cpe', {
261
249
  cpeName: input.cpeName,
262
250
  virtualMatchString: input.virtualMatchString,
263
251
  limit: input.limit,
252
+ offset: input.offset,
264
253
  });
265
254
  const service = getNvdCveService();
266
255
  const result = await service.auditCpe({
@@ -273,10 +262,12 @@ export const nvdAuditCpe = tool('nvd_audit_cpe', {
273
262
  ...(input.severityMin && { severityMin: input.severityMin }),
274
263
  allLanguages: input.allLanguages,
275
264
  limit: input.limit,
265
+ offset: input.offset,
276
266
  }, ctx);
277
267
  const auditTarget = result.cpeName ?? result.virtualMatchString ?? 'unknown CPE';
278
268
  ctx.enrich({
279
269
  returned: result.returned,
270
+ offset: result.offset,
280
271
  auditTarget,
281
272
  ...(input.severityMin && {
282
273
  severityMin: input.severityMin,
@@ -285,9 +276,14 @@ export const nvdAuditCpe = tool('nvd_audit_cpe', {
285
276
  });
286
277
  ctx.enrich.total(result.totalResults);
287
278
  if (result.cves.length === 0) {
288
- // Distinguish "the CPE has no CVEs" from "severityMin dropped everything on the page":
289
- // filteredCount > 0 means NVD did return CVEs, they were just below the threshold.
290
- if (input.severityMin && result.filteredCount > 0) {
279
+ // Distinguish "the CPE has no CVEs" from "severityMin dropped everything on the page" from
280
+ // "the offset ran off the end": filteredCount > 0 means NVD did return CVEs, they were just
281
+ // below the threshold; an offset at or past totalCount means the product has CVEs but this
282
+ // page is empty, so telling the caller to re-check the CPE would send them the wrong way.
283
+ if (result.totalResults > 0 && input.offset >= result.totalResults) {
284
+ ctx.enrich.notice(`Offset ${input.offset} is past the end of the result set (${result.totalResults} total). Use a lower offset to page through results.`);
285
+ }
286
+ else if (input.severityMin && result.filteredCount > 0) {
291
287
  ctx.enrich.notice(`All ${result.filteredCount} CVE(s) on the fetched page scored below ${input.severityMin}. ` +
292
288
  'Lower severityMin or raise limit to widen the page NVD returns.');
293
289
  }
@@ -338,7 +334,7 @@ export const nvdAuditCpe = tool('nvd_audit_cpe', {
338
334
  lines.push(` - Required Action: ${cve.cisaKev.requiredAction}`);
339
335
  }
340
336
  if (cve.configurations && cve.configurations.length > 0) {
341
- const matches = cve.configurations.flatMap((cfg) => cfg.nodes.flatMap((node) => node.cpeMatch.map((m) => formatCpeMatch(m, cfg.operator, node.operator))));
337
+ const matches = flattenCpeMatches(cve.configurations);
342
338
  lines.push(`**Configurations:** ${cve.configurations.length} node group(s), ${matches.length} CPE match(es)`);
343
339
  for (const match of matches.slice(0, CPE_MATCH_CAP))
344
340
  lines.push(` - ${match}`);
@@ -348,13 +344,14 @@ export const nvdAuditCpe = tool('nvd_audit_cpe', {
348
344
  }
349
345
  if (cve.references && cve.references.length > 0) {
350
346
  lines.push(`**References (${cve.references.length}):**`);
351
- for (const ref of cve.references.slice(0, 5)) {
347
+ for (const ref of cve.references.slice(0, REFERENCE_CAP)) {
352
348
  const sourcePart = ref.source ? ` [${ref.source}]` : '';
353
349
  const tagPart = ref.tags?.length ? ` (${ref.tags.join(', ')})` : '';
354
350
  lines.push(` - ${ref.url}${sourcePart}${tagPart}`);
355
351
  }
356
- if (cve.references.length > 5)
357
- lines.push(` - … ${cve.references.length - 5} more`);
352
+ if (cve.references.length > REFERENCE_CAP) {
353
+ lines.push(` - … ${cve.references.length - REFERENCE_CAP} more`);
354
+ }
358
355
  }
359
356
  lines.push('');
360
357
  }