@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.
- package/AGENTS.md +29 -16
- package/CLAUDE.md +29 -16
- package/Dockerfile +1 -1
- package/README.md +14 -9
- package/changelog/0.1.x/0.1.15.md +24 -0
- package/changelog/0.1.x/0.1.16.md +21 -0
- package/dist/mcp-server/resources/definitions/nvd-cve.resource.d.ts +17 -1
- package/dist/mcp-server/resources/definitions/nvd-cve.resource.d.ts.map +1 -1
- package/dist/mcp-server/resources/definitions/nvd-cve.resource.js +22 -7
- package/dist/mcp-server/resources/definitions/nvd-cve.resource.js.map +1 -1
- package/dist/mcp-server/tools/definitions/nvd-audit-cpe.tool.d.ts +2 -0
- package/dist/mcp-server/tools/definitions/nvd-audit-cpe.tool.d.ts.map +1 -1
- package/dist/mcp-server/tools/definitions/nvd-audit-cpe.tool.js +28 -31
- package/dist/mcp-server/tools/definitions/nvd-audit-cpe.tool.js.map +1 -1
- package/dist/mcp-server/tools/definitions/nvd-get-cve-history.tool.d.ts +8 -0
- package/dist/mcp-server/tools/definitions/nvd-get-cve-history.tool.d.ts.map +1 -1
- package/dist/mcp-server/tools/definitions/nvd-get-cve-history.tool.js +34 -6
- package/dist/mcp-server/tools/definitions/nvd-get-cve-history.tool.js.map +1 -1
- package/dist/mcp-server/tools/definitions/nvd-get-cve.tool.d.ts +7 -0
- package/dist/mcp-server/tools/definitions/nvd-get-cve.tool.d.ts.map +1 -1
- package/dist/mcp-server/tools/definitions/nvd-get-cve.tool.js +46 -23
- package/dist/mcp-server/tools/definitions/nvd-get-cve.tool.js.map +1 -1
- package/dist/mcp-server/tools/definitions/nvd-search-cpes.tool.d.ts +2 -0
- package/dist/mcp-server/tools/definitions/nvd-search-cpes.tool.d.ts.map +1 -1
- package/dist/mcp-server/tools/definitions/nvd-search-cpes.tool.js +26 -7
- package/dist/mcp-server/tools/definitions/nvd-search-cpes.tool.js.map +1 -1
- package/dist/mcp-server/tools/definitions/nvd-search-cves.tool.d.ts +1 -0
- package/dist/mcp-server/tools/definitions/nvd-search-cves.tool.d.ts.map +1 -1
- package/dist/mcp-server/tools/definitions/nvd-search-cves.tool.js +16 -2
- package/dist/mcp-server/tools/definitions/nvd-search-cves.tool.js.map +1 -1
- package/dist/mcp-server/tools/formatting/cpe-match.d.ts +46 -0
- package/dist/mcp-server/tools/formatting/cpe-match.d.ts.map +1 -0
- package/dist/mcp-server/tools/formatting/cpe-match.js +43 -0
- package/dist/mcp-server/tools/formatting/cpe-match.js.map +1 -0
- package/dist/services/nvd-cpe/nvd-cpe-service.d.ts +3 -0
- package/dist/services/nvd-cpe/nvd-cpe-service.d.ts.map +1 -1
- package/dist/services/nvd-cpe/nvd-cpe-service.js +3 -2
- package/dist/services/nvd-cpe/nvd-cpe-service.js.map +1 -1
- package/dist/services/nvd-cve/nvd-cve-service.d.ts +35 -1
- package/dist/services/nvd-cve/nvd-cve-service.d.ts.map +1 -1
- package/dist/services/nvd-cve/nvd-cve-service.js +116 -27
- package/dist/services/nvd-cve/nvd-cve-service.js.map +1 -1
- package/dist/services/nvd-cve/types.d.ts +12 -2
- package/dist/services/nvd-cve/types.d.ts.map +1 -1
- package/manifest.json +1 -1
- package/package.json +4 -4
- 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.
|
|
5
|
-
**Framework:** [@cyanheads/mcp-ts-core](https://www.npmjs.com/package/@cyanheads/mcp-ts-core) `^0.
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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.
|
|
5
|
-
**Framework:** [@cyanheads/mcp-ts-core](https://www.npmjs.com/package/@cyanheads/mcp-ts-core) `^0.
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
[](./CHANGELOG.md) [](./LICENSE) [](https://github.com/users/cyanheads/packages/container/package/nist-nvd-mcp-server) [](https://modelcontextprotocol.io/) [](https://www.npmjs.com/package/@cyanheads/nist-nvd-mcp-server) [](https://www.typescriptlang.org/) [](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
|
|
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: `
|
|
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
|
|
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
|
-
-
|
|
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
|
|
98
|
-
-
|
|
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
|
-
- `
|
|
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,
|
|
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;
|
|
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 {
|
|
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
|
|
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,
|
|
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;
|
|
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
|
-
/**
|
|
32
|
-
const
|
|
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
|
|
290
|
-
|
|
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
|
|
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,
|
|
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 >
|
|
357
|
-
lines.push(` - … ${cve.references.length -
|
|
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
|
}
|