AGENTS.md
+36 −16
https://raw.githubusercontent.com/cyanheads/cisa-cybersecurity-mcp-server/HEAD/AGENTS.md
− **Version:** 0.3.2− **Framework:** [@cyanheads/mcp-ts-core](https://www.npmjs.com/package/@cyanheads/mcp-ts-core) `^0.13.15`− **MCP SDK:** `@modelcontextprotocol/server` ^2.3.1− **Zod:** ^4.6.6− Four keyless CISA datasets, all read-only, served over seven tools and two resources. `docs/design.md` is the as-built specification — upstream shapes, per-tool contracts, storage tiers, and the numbered decisions log. Read it before changing a handler; every count and field name in it was verified against the live sources.− cveIds: z.array(CveIdInputSchema.describe('One CVE identifier, e.g. CVE-2025-39964.')).min(1).max(200)− .describe('CVE identifiers to check, up to 200 per call. The whole batch costs zero upstream requests.'),− results: z.array(KevRecordSchema).describe('One result per requested CVE, in the order supplied.'),− const results = input.cveIds.map((cveId) => {− return { results, foundCount, notFoundCount: results.length - foundCount };− format: (result) => [{ type: 'text', text: result.results.flatMap(renderKevRecord).join('\n') }],− All seven `CISA_*` variables are optional — every source is keyless and every tunable has a working default, so the server runs correctly with none of them set. Adding one means editing four files together: the schema above, `.env.example`, `server.json` `environmentVariables[]` (both package entries), and `manifest.json` (`mcp_config.env` + `user_config`). `lint:packaging` fails on a mismatch.− Skills are modular instructions in `framework-skills/` at the project root. Read them directly when a task matches — e.g., `framework-skills/add-tool/SKILL.md` when adding a tool. `bun run list-skills` prints the full registry. The directory is deliberately not `skills/`: Claude Code and Codex auto-load a plugin's root `skills/`, so a server that ships `.claude-plugin/` or `.codex-plugin/` would hand these development skills to every agent that installs it. Keep `skills/` free for skills meant f− **Agent skill directory:** Copy skills into the directory your agent discovers (Claude Code: `.claude/skills/`, others: equivalent). Skills then load as context without referencing `framework-skills/` paths. After framework updates, run the `maintenance` skill — Phase B re-syncs the agent directory.− | `bun run lint:packaging` | Packaging surface checks — `server.json`/`manifest.json` env-var parity (run by devcheck) |− `npm run bundle` produces a `.mcpb` extension bundle for one-click install in Claude Desktop. The pack step is followed by `scripts/clean-mcpb.ts`, which prunes dev dependencies (`mcpb clean`) and strips three classes of `node_modules/**` content that root-anchored `.mcpbignore` patterns cannot reach: dependency-shipped agent docs (`framework-skills/`, `skills/`, `.claude/`, `.agents/`, `SKILL.md`), platform-specific native bindings that would lock the bundle to the platform it was packed on (`N+ **Version:** 0.4.0+ **Framework:** [@cyanheads/mcp-ts-core](https://www.npmjs.com/package/@cyanheads/mcp-ts-core) `^0.13.18`+ **MCP SDK:** `@modelcontextprotocol/server` ^2.4.0+ **Zod:** ^4.6.7+ Four keyless CISA datasets, all read-only, served over seven tools, two dataset resource templates, and the published `cisa-cybersecurity-guide` skill resource. `docs/design.md` is the as-built specification — upstream shapes, per-tool contracts, storage tiers, and the numbered decisions log. Read it before changing a handler; its measured corpus counts and sizes are historical observations.+ | `cisa-cybersecurity-guide` skill resource | Root `skills/cisa-cybersecurity-guide/SKILL.md`, exposed through `createApp({ skills: 'skills' })`. Vulnerability triage, advisory applicability, and evidence coverage workflows. |+ - Five read tools advertise a 1,000 hard ceiling and share workload caps in `src/mcp-server/read-limits.ts`: CVE status 200, SSVC 50, KEV search 100, ICS search 50, alerts 30. `CISA_CLAMP_OVERSIZED_INPUTS=true` bounds requests with explicit batch `unprocessedIds` or page continuation; false throws non-retryable `read_limit_exceeded`. Clamping logs and increments `mcp.cisa.input.normalized`; counts cover processed CVEs only.+ - KEV continuation takes the offset from its cursor and the page size from the current request. Alert continuation binds feed and GUID within the available current or cached window. An unavailable anchor gives empty items and restart guidance, never an inferred eviction or historical completeness claim.+ - SSVC results expose `outcome`; `fetch_failed` counts toward `failedCount`, never `notFoundCount`. Partial failures cannot support a notice claiming no SSVC exists; all processed lookups failing remains retryable `enrichment_source_unavailable`.+ - Both advisory outline arms retain the complete `advisory` header, including URLs and attribution. Remediations, flags, and threats retain `groupIds`; grouped `productIds` union direct IDs with group members in document order, while direct-only arrays keep their order and duplicates. Unresolved references remain unresolved. Ingest-content version 3 refreshes unchanged stored documents; optional flags/threats support rows awaiting re-ingest.+ import { applyReadLimit } from '@/mcp-server/read-limits.js';+ cveIds: z.array(CveIdInputSchema.describe('One CVE identifier, e.g. CVE-2025-39964.')).min(1).max(1000)+ .describe('Accept up to 1,000 CVE IDs; process 200 and return the remainder unless clamping is disabled.'),+ results: z.array(KevRecordSchema).describe('One result per processed CVE, in the order supplied.'),+ unprocessedIds: z.array(CveIdInputSchema).optional().describe('Normalized IDs to send in a follow-up call.'),+ const count = applyReadLimit('cisa_check_cve_status', input.cveIds.length, ctx);+ const unprocessedIds = input.cveIds.slice(count);+ const results = input.cveIds.slice(0, count).map((cveId) => {+ return { results, foundCount, notFoundCount: results.length - foundCount, ...(unprocessedIds.length ? { unprocessedIds } : {}) };+ format: (result) => [{ type: 'text', text: [...result.results.flatMap(renderKevRecord), ...(result.unprocessedIds ? [`Unprocessed IDs: ${result.unprocessedIds.join(', ')}`] : [])].join('\n') }],+ clampOversizedInputs: z.stringbool().default(true)+ .describe('Bound oversized read inputs with explicit continuation; false rejects them.'),+ clampOversizedInputs: 'CISA_CLAMP_OVERSIZED_INPUTS',+ All eight `CISA_*` variables are optional — every source is keyless and every tunable has a working default, including `CISA_CLAMP_OVERSIZED_INPUTS=true`. The server runs correctly with none set. Adding one means updating the schema above, `.env.example`, `server.json` `environmentVariables[]` (both package entries), `manifest.json` (`mcp_config.env` + `user_config`), and both legacy plugin environment surfaces (`.claude-plugin/plugin.json` and `.codex-plugin/mcp.json`). Keep the names and defau+ Two skill trees live at the project root, written for different readers:+ | Tree | For | Written by |+ |:-----|:----|:-----------|+ | `framework-skills/` | The agent developing this server — framework skills (`metadata.audience: external`) plus any development-time skills of the server's own | `init` and the `maintenance` sync (framework skills); you (your own) |+ | `skills/` | The agents that install the server and call its tools — tool chains, identifier sources, gates | You, deliberately, with the `add-skill` skill. Absent by default; usually none is needed |+ Development skills are modular instructions. Read them directly when a task matches — e.g., `framework-skills/add-tool/SKILL.md` when adding a tool. `bun run list-skills` prints the full registry. Their directory is deliberately not `skills/`: Claude Code and Codex auto-load a plugin's root `skills/`, so a server that ships `.claude-plugin/` or `.codex-plugin/` would hand them to every agent that installs it.+ A published skill in root `skills/` reaches plugin installs and the `.mcpb` bundle as-is, ships in the npm package and the Docker image, and reaches MCP clients when the server opts in with `createApp({ skills: 'skills' })`. `lint:packaging` validates the tree.+ **Agent skill directory:** Copy `framework-skills/` into the directory your agent discovers (Claude Code: `.claude/skills/`, others: equivalent). Skills then load as context without referencing `framework-skills/` paths. After framework updates, run the `maintenance` skill — Phase B re-syncs the agent directory.+ | `add-skill` | Author a skill the server publishes in root `skills/` for the agents that use it — when one earns its place, structure, validation, delivery |+ | `bun run lint:packaging` | Packaging surface checks — env-var parity, root-anchored bundle exclusions, plugin manifests, and published skills (run by devcheck) |+ `npm run bundle` produces a `.mcpb` extension bundle for one-click install in Claude Desktop. The pack step is followed by `scripts/clean-mcpb.ts`, which prunes dev dependencies (`mcpb clean`) and strips four classes of `node_modules/**` content that root-anchored `.mcpbignore` patterns cannot reach: dependency-shipped agent docs (`framework-skills/`, `skills/`, `.claude/`, `.agents/`, `SKILL.md`), platform-specific native bindings that would lock the bundle to the platform it was packed on (`NA+ - [ ] `plugin.json` + `mcp.json` (Agent Plugins 1.0.0 — read by Codex, Cursor, GitHub Copilot, VS Code, Kiro) populated — `plugin.json` `name` = the unscoped repo name (lowercase `a-z`, `0-9`, `-`, `.` only); `version`, `description`, `author`, `homepage`, `repository`, `license`, `keywords` from `package.json`; no other top-level field, `extensions` included. `mcp.json` holds one `"type": "stdio"` entry keyed by the unscoped name, `npx -y <full package name>`, and the same fixed `env` as `.code