@cyanheads/college-scorecard-mcp-server 0.4.0 → 0.6.0

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 (109) hide show
  1. package/AGENTS.md +31 -12
  2. package/CLAUDE.md +31 -12
  3. package/Dockerfile +5 -3
  4. package/README.md +64 -51
  5. package/changelog/0.5.x/0.5.0.md +41 -0
  6. package/changelog/0.6.x/0.6.0.md +34 -0
  7. package/changelog/template.md +10 -3
  8. package/dist/data/cip-2020.json +522 -0
  9. package/dist/data/cip-codes.d.ts +1 -1
  10. package/dist/data/cip-codes.d.ts.map +1 -1
  11. package/dist/data/cip-codes.js +16 -1121
  12. package/dist/data/cip-codes.js.map +1 -1
  13. package/dist/data/earnings-context.d.ts.map +1 -1
  14. package/dist/data/earnings-context.js +25 -13
  15. package/dist/data/earnings-context.js.map +1 -1
  16. package/dist/data/field-catalog.d.ts.map +1 -1
  17. package/dist/data/field-catalog.js +102 -23
  18. package/dist/data/field-catalog.js.map +1 -1
  19. package/dist/data/program-provenance.d.ts +124 -0
  20. package/dist/data/program-provenance.d.ts.map +1 -0
  21. package/dist/data/program-provenance.js +111 -0
  22. package/dist/data/program-provenance.js.map +1 -0
  23. package/dist/data/repayment-context.d.ts +74 -0
  24. package/dist/data/repayment-context.d.ts.map +1 -0
  25. package/dist/data/repayment-context.js +69 -0
  26. package/dist/data/repayment-context.js.map +1 -0
  27. package/dist/index.js +4 -2
  28. package/dist/index.js.map +1 -1
  29. package/dist/mcp-server/format-helpers.js +2 -2
  30. package/dist/mcp-server/format-helpers.js.map +1 -1
  31. package/dist/mcp-server/institution-context.d.ts +61 -0
  32. package/dist/mcp-server/institution-context.d.ts.map +1 -0
  33. package/dist/mcp-server/institution-context.js +91 -0
  34. package/dist/mcp-server/institution-context.js.map +1 -0
  35. package/dist/mcp-server/program-format.d.ts +25 -0
  36. package/dist/mcp-server/program-format.d.ts.map +1 -0
  37. package/dist/mcp-server/program-format.js +71 -0
  38. package/dist/mcp-server/program-format.js.map +1 -0
  39. package/dist/mcp-server/prompts/definitions/compare.prompt.js +4 -4
  40. package/dist/mcp-server/prompts/definitions/compare.prompt.js.map +1 -1
  41. package/dist/mcp-server/repayment.d.ts +107 -0
  42. package/dist/mcp-server/repayment.d.ts.map +1 -0
  43. package/dist/mcp-server/repayment.js +146 -0
  44. package/dist/mcp-server/repayment.js.map +1 -0
  45. package/dist/mcp-server/resources/definitions/programs-page.resource.d.ts +83 -0
  46. package/dist/mcp-server/resources/definitions/programs-page.resource.d.ts.map +1 -0
  47. package/dist/mcp-server/resources/definitions/programs-page.resource.js +120 -0
  48. package/dist/mcp-server/resources/definitions/programs-page.resource.js.map +1 -0
  49. package/dist/mcp-server/resources/definitions/programs.resource.d.ts +3 -3
  50. package/dist/mcp-server/resources/definitions/programs.resource.d.ts.map +1 -1
  51. package/dist/mcp-server/resources/definitions/programs.resource.js +6 -20
  52. package/dist/mcp-server/resources/definitions/programs.resource.js.map +1 -1
  53. package/dist/mcp-server/resources/definitions/school.resource.d.ts +3 -3
  54. package/dist/mcp-server/resources/definitions/school.resource.d.ts.map +1 -1
  55. package/dist/mcp-server/resources/definitions/school.resource.js +8 -4
  56. package/dist/mcp-server/resources/definitions/school.resource.js.map +1 -1
  57. package/dist/mcp-server/tools/definitions/compare-schools.tool.d.ts +17 -3
  58. package/dist/mcp-server/tools/definitions/compare-schools.tool.d.ts.map +1 -1
  59. package/dist/mcp-server/tools/definitions/compare-schools.tool.js +176 -44
  60. package/dist/mcp-server/tools/definitions/compare-schools.tool.js.map +1 -1
  61. package/dist/mcp-server/tools/definitions/get-earnings.tool.d.ts +10 -3
  62. package/dist/mcp-server/tools/definitions/get-earnings.tool.d.ts.map +1 -1
  63. package/dist/mcp-server/tools/definitions/get-earnings.tool.js +20 -9
  64. package/dist/mcp-server/tools/definitions/get-earnings.tool.js.map +1 -1
  65. package/dist/mcp-server/tools/definitions/get-programs.tool.d.ts +100 -4
  66. package/dist/mcp-server/tools/definitions/get-programs.tool.d.ts.map +1 -1
  67. package/dist/mcp-server/tools/definitions/get-programs.tool.js +93 -48
  68. package/dist/mcp-server/tools/definitions/get-programs.tool.js.map +1 -1
  69. package/dist/mcp-server/tools/definitions/get-school.tool.d.ts +81 -4
  70. package/dist/mcp-server/tools/definitions/get-school.tool.d.ts.map +1 -1
  71. package/dist/mcp-server/tools/definitions/get-school.tool.js +157 -15
  72. package/dist/mcp-server/tools/definitions/get-school.tool.js.map +1 -1
  73. package/dist/mcp-server/tools/definitions/list-fields.tool.d.ts.map +1 -1
  74. package/dist/mcp-server/tools/definitions/list-fields.tool.js +8 -3
  75. package/dist/mcp-server/tools/definitions/list-fields.tool.js.map +1 -1
  76. package/dist/mcp-server/tools/definitions/lookup-cip.tool.d.ts +4 -0
  77. package/dist/mcp-server/tools/definitions/lookup-cip.tool.d.ts.map +1 -1
  78. package/dist/mcp-server/tools/definitions/lookup-cip.tool.js +60 -19
  79. package/dist/mcp-server/tools/definitions/lookup-cip.tool.js.map +1 -1
  80. package/dist/mcp-server/tools/definitions/search-programs.tool.d.ts +102 -4
  81. package/dist/mcp-server/tools/definitions/search-programs.tool.d.ts.map +1 -1
  82. package/dist/mcp-server/tools/definitions/search-programs.tool.js +118 -50
  83. package/dist/mcp-server/tools/definitions/search-programs.tool.js.map +1 -1
  84. package/dist/mcp-server/tools/definitions/search-schools.tool.d.ts +33 -3
  85. package/dist/mcp-server/tools/definitions/search-schools.tool.d.ts.map +1 -1
  86. package/dist/mcp-server/tools/definitions/search-schools.tool.js +75 -13
  87. package/dist/mcp-server/tools/definitions/search-schools.tool.js.map +1 -1
  88. package/dist/mcp-server/tools/definitions/value-analysis.tool.d.ts +69 -4
  89. package/dist/mcp-server/tools/definitions/value-analysis.tool.d.ts.map +1 -1
  90. package/dist/mcp-server/tools/definitions/value-analysis.tool.js +80 -30
  91. package/dist/mcp-server/tools/definitions/value-analysis.tool.js.map +1 -1
  92. package/dist/services/scorecard/input-normalization.d.ts.map +1 -1
  93. package/dist/services/scorecard/input-normalization.js +2 -1
  94. package/dist/services/scorecard/input-normalization.js.map +1 -1
  95. package/dist/services/scorecard/program-outcomes.d.ts +158 -0
  96. package/dist/services/scorecard/program-outcomes.d.ts.map +1 -0
  97. package/dist/services/scorecard/program-outcomes.js +104 -0
  98. package/dist/services/scorecard/program-outcomes.js.map +1 -0
  99. package/dist/services/scorecard/scorecard-service.d.ts +1 -1
  100. package/dist/services/scorecard/scorecard-service.d.ts.map +1 -1
  101. package/dist/services/scorecard/scorecard-service.js +47 -4
  102. package/dist/services/scorecard/scorecard-service.js.map +1 -1
  103. package/dist/services/scorecard/types.d.ts +32 -1
  104. package/dist/services/scorecard/types.d.ts.map +1 -1
  105. package/dist/services/scorecard/types.js +7 -1
  106. package/dist/services/scorecard/types.js.map +1 -1
  107. package/package.json +12 -10
  108. package/server.json +3 -3
  109. package/skills/college-scorecard-guide/SKILL.md +47 -0
package/AGENTS.md CHANGED
@@ -1,11 +1,11 @@
1
1
  # Developer Protocol
2
2
 
3
3
  **Server:** college-scorecard-mcp-server
4
- **Version:** 0.4.0
5
- **Framework:** [@cyanheads/mcp-ts-core](https://www.npmjs.com/package/@cyanheads/mcp-ts-core) `^0.13.15`
6
- **Engines:** Bun ≥1.4.0, Node ≥24.0.0
7
- **MCP SDK:** `@modelcontextprotocol/server` ^2.3.1
8
- **Zod:** ^4.6.5
4
+ **Version:** 0.6.0
5
+ **Framework:** [@cyanheads/mcp-ts-core](https://www.npmjs.com/package/@cyanheads/mcp-ts-core) `^0.13.22`
6
+ **Engines:** Bun ≥1.4.4, Node ≥24.0.0
7
+ **MCP SDK:** `@modelcontextprotocol/server` ^2.4.0
8
+ **Zod:** ^4.6.7
9
9
 
10
10
  > **Read the framework docs first:** `node_modules/@cyanheads/mcp-ts-core/CLAUDE.md` contains the full API reference — builders, Context, error codes, exports, patterns. This file covers server-specific conventions only.
11
11
 
@@ -181,9 +181,15 @@ This server declares `sessionMode: 'stateless'` in `src/index.ts`: no tool calls
181
181
 
182
182
  ### Scorecard metric semantics
183
183
 
184
- Institution earnings cover federally aided undergraduate entrants working and not enrolled, regardless of completion. Earnings `years` is the published API data-file start-year domain 1996–2025, not an entry-year selector. `src/data/earnings-context.ts` holds official per-metric cohort/measurement/dollar provenance; refresh it from the dictionary when coverage changes, never infer vintage by arithmetic or sample equality.
184
+ Institution earnings cover federally aided undergraduate entrants working and not enrolled, regardless of completion. Earnings `years` is the published API data-file start-year domain 1996–2025, not an entry-year selector. `src/data/earnings-context.ts` holds official per-metric cohort/measurement/dollar provenance. During maintenance, run `bun run check:earnings-provenance` (Python 3 required) against the official dictionary; an optional local XLSX path avoids downloading it again. Resolve drift from the dictionary before release, never infer vintage by arithmetic or sample equality. This is a development check, not a per-request fetch.
185
185
 
186
- `readMetric` distinguishes numbers (including zero), unknown-cause null/absence, and explicit `PS` / `PrivacySuppressed` markers. Suppression flags/counts require the latter; unexpected values are provider contract failures. CIP lookup remains a curated set of current NCES four-digit groups, with exact group/family titles and verified descendant aliases. Program search applies CIP × credential together upstream before school pagination; totals remain school-level.
186
+ `readMetric` distinguishes numbers (including zero), unknown-cause null/absence, and explicit `PS` / `PrivacySuppressed` markers. Suppression flags/counts require the latter; unexpected values are provider contract failures. CIP lookup covers all current NCES CIP 2020 four-digit groups, with exact group/family titles and verified descendant aliases. Program search applies CIP × credential together upstream before school pagination; totals remain school-level.
187
+
188
+ `src/data/repayment-context.ts` holds dictionary provenance for discontinued declining-balance rates and the separate two- and four-year undergraduate federal Direct Loan borrower statuses. The three-year declining-balance rate uses the 2016–17 file. Profiles and value analysis retain each borrower-status horizon's OPEID6 scope, denominator when available, and reported decimal or bin. Only point estimates carry numeric values. Unsupported status rates yield an unavailable value and a notice. Comparison ranks use the historical three-year rate.
189
+
190
+ `school.operating` is current operating certification. Actual closure requires separate evidence. School search applies certification, HBCU, locale, price, and completion filters upstream before pagination. Use observed provider support for catalog sort flags, including unsortable pooled full-time retention projections.
191
+
192
+ Program tool text leads with completion earnings and renders legacy highest-credential values separately. School comparison leaves descriptive metrics, singleton results, shared OPEID6 outcomes, and mixed price periods unranked. Negative dollar selectors normalize to zero with a notice. Earnings vintages deduplicate in caller order. Six-digit CIP lookup resolves to the four-digit group used by program selectors.
187
193
 
188
194
  ---
189
195
 
@@ -285,9 +291,18 @@ src/
285
291
 
286
292
  ## Skills
287
293
 
288
- Skills are modular instructions in `framework-skills/` at the project root. Read them directly when a task matches — e.g., `framework-skills/add-tool/SKILL.md` when adding a tool. `bun run list-skills` prints the full registry. The directory is deliberately not `skills/`: Claude Code and Codex auto-load a plugin's root `skills/`, so a server that ships `.claude-plugin/` or `.codex-plugin/` would hand these development skills to every agent that installs it. Keep `skills/` free for skills meant for those agents.
294
+ Two skill trees live at the project root, written for different readers:
295
+
296
+ | Tree | For | Written by |
297
+ |:-----|:----|:-----------|
298
+ | `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) |
299
+ | `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 |
300
+
301
+ 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.
302
+
303
+ 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.
289
304
 
290
- **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.
305
+ **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.
291
306
 
292
307
  Available skills:
293
308
 
@@ -299,6 +314,7 @@ Available skills:
299
314
  | `add-app-tool` | Scaffold an MCP App tool + paired UI resource |
300
315
  | `add-resource` | Scaffold a new resource definition |
301
316
  | `add-prompt` | Scaffold a new prompt definition |
317
+ | `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 |
302
318
  | `add-service` | Scaffold a new service integration |
303
319
  | `add-test` | Scaffold test file for a tool, resource, or service |
304
320
  | `field-test` | Exercise tools/resources/prompts with real inputs, verify behavior, report issues |
@@ -346,7 +362,7 @@ When you complete a skill's checklist, check the boxes and add a completion time
346
362
  | `bun run audit:fix` | `bun audit fix` — upgrade vulnerable packages to the lowest safe version within existing ranges (`--dry-run` previews, `--latest` rewrites ranges). First response when `devcheck` flags a transitive advisory; then `bun update <name>`, then `bun dedupe` |
347
363
  | `bun run audit:refresh` | Delete `bun.lock` and reinstall. Last resort after `audit:fix`, `bun update <name>`, and `bun dedupe` — re-resolves every ranged dep (the framework pin included) and rewrites the lockfile as `lockfileVersion: 2` |
348
364
  | `bun run lint:mcp` | Run the MCP definition linter standalone (rule catalog: `api-linter` skill) |
349
- | `bun run lint:packaging` | Packaging surface checks — `server.json`/`manifest.json` env-var parity (run by devcheck) |
365
+ | `bun run lint:packaging` | Packaging surface checks — `server.json`/`manifest.json` env-var parity, `.mcpbignore` coverage, plugin manifests, the published `skills/` tree (run by devcheck) |
350
366
  | `bun run list-skills` | Print the skill registry |
351
367
  | `bun run tree` | Generate directory structure doc |
352
368
  | `bun run format` | Auto-fix formatting (safe fixes only) |
@@ -366,7 +382,7 @@ When you complete a skill's checklist, check the boxes and add a completion time
366
382
 
367
383
  ## Bundling
368
384
 
369
- `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 two classes of `node_modules/**` content that root-anchored `.mcpbignore` patterns cannot reach: dependency-shipped agent docs (`framework-skills/`, `skills/`, `.claude/`, `.agents/`, `SKILL.md`) and platform-specific native bindings, which would otherwise lock the bundle to the platform it was packed on. A server using DataCanvas therefore ships a portable bundle without the DuckDB native — `@duckdb/node-api` is an optional peer loaded lazily, so canvas tools report an actionable install hint and every other tool works normally. MCPB is stdio-only — HTTP and Cloudflare Workers deployments are unaffected. Consumers who don't need it can delete `manifest.json` and `.mcpbignore`; `lint:packaging` skips cleanly.
385
+ `npm run bundle` produces a `.mcpb` extension bundle for one-click install in Claude Desktop. `mcpb pack` reads `.mcpbignore`, not `.gitignore`, so every root path the bundle must not ship — source, tests, docs, caches, logs, agent docs, plugin manifests, gitignored local files — needs a root-anchored line there (`/docs/`, never `docs/`, which also strips `node_modules/*/docs/`); `lint:packaging` names each missing or unanchored line. Root `skills/` ships on purpose. 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/`, `AGENTS.md`, and `CLAUDE.md` at a package's root, and `SKILL.md` anywhere (a `skills/` deeper in a package is runtime code and stays); platform-specific native bindings, which would otherwise lock the bundle to the platform it was packed on; node-gyp build output beside a `binding.gyp` (`config.gypi`, Makefiles, `obj.target/`, `.deps/`, object files and debug symbols), which records the packing machine's paths — compiled `*.node` addons stay; and build-only package files — the framework's `changelog/`, `scripts/`, `templates/`, and config bases (`biome.json`, `vitest.config.base.mjs`, `config/tsconfig.base.json`), and `better-sqlite3`'s `deps/` and `src/` C sources. A server using DataCanvas therefore ships a portable bundle without the DuckDB native — `@duckdb/node-api` is an optional peer loaded lazily, so canvas tools report an actionable install hint and every other tool works normally. MCPB is stdio-only — HTTP and Cloudflare Workers deployments are unaffected. Consumers who don't need it can delete `manifest.json` and `.mcpbignore`; `lint:packaging` skips cleanly.
370
386
 
371
387
  **Adding an env var requires both files:** `server.json` (registry discovery, `environmentVariables[]`) and `manifest.json` (bundle install UX, `mcp_config.env` + `user_config`). `lint:packaging` (run by `devcheck`) verifies the env var names match, that every `user_config` option is wired into `mcp_config.env` as `"X": "${user_config.X}"` (the host substitutes nothing else — `"${X}"` reaches the server as that literal string), and that an optional string option carries `"default": ""`.
372
388
 
@@ -433,7 +449,10 @@ import { getMyService } from '@/services/my-domain/my-service.js';
433
449
  - [ ] If wrapping external API: tests include at least one sparse payload case with omitted upstream fields
434
450
  - [ ] Registered in `createApp()` arrays (directly or via barrel exports)
435
451
  - [ ] Tests use `createMockContext()` from `@cyanheads/mcp-ts-core/testing`
452
+ - [ ] `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 `.codex-plugin/mcp.json`. Neither file carries a user-supplied variable (no `""`, no `${…}` besides `${PLUGIN_ROOT}`/`${PLUGIN_DATA}`): keys reach Codex through `.codex-plugin/mcp.json` `env_vars`, which works only while `.codex-plugin/plugin.json` keeps `"mcpServers": "./.codex-plugin/mcp.json"`
436
453
  - [ ] `.codex-plugin/plugin.json` populated — `name`, `version`, `description`, `repository`, `license` from `package.json`; `interface.displayName` = the unscoped repo name (never the npm scope — `lint:packaging` enforces this); `interface.shortDescription` from `package.json` description
437
454
  - [ ] `.codex-plugin/mcp.json` updated — server name key is the unscoped repo name; every user-supplied variable (API key, contact email, instance URL) is listed in `env_vars` so Codex forwards it from the user's environment. Never write `"KEY": ""` into `env` — an empty value replaces the user's exported key and is read as unset
438
455
  - [ ] `.claude-plugin/plugin.json` populated — `name`, `version`, `description`, `author`, `repository`, `license`, `keywords` from `package.json`; inline `mcpServers` entry keyed by the unscoped repo name. Every user-supplied variable is declared under `userConfig` (`type`, `title`, `description`; `sensitive: true` for keys and tokens; `required: true` or `default: ""`) and referenced from `env` as `"KEY": "${user_config.<option>}"` — mirror the `user_config` block in `manifest.json`. Never write `"KEY": ""` into `env`
439
- - [ ] `npm run devcheck` passes
456
+ - [ ] Any new root dev path has a root-anchored line in `.mcpbignore` and passes `bun run bundle` and `lint:packaging`
457
+ - [ ] Field-test verified every gate and identifier claim in the published `skills/college-scorecard-guide` against live tool behavior
458
+ - [ ] `bun run devcheck` passes
package/CLAUDE.md CHANGED
@@ -1,11 +1,11 @@
1
1
  # Developer Protocol
2
2
 
3
3
  **Server:** college-scorecard-mcp-server
4
- **Version:** 0.4.0
5
- **Framework:** [@cyanheads/mcp-ts-core](https://www.npmjs.com/package/@cyanheads/mcp-ts-core) `^0.13.15`
6
- **Engines:** Bun ≥1.4.0, Node ≥24.0.0
7
- **MCP SDK:** `@modelcontextprotocol/server` ^2.3.1
8
- **Zod:** ^4.6.5
4
+ **Version:** 0.6.0
5
+ **Framework:** [@cyanheads/mcp-ts-core](https://www.npmjs.com/package/@cyanheads/mcp-ts-core) `^0.13.22`
6
+ **Engines:** Bun ≥1.4.4, Node ≥24.0.0
7
+ **MCP SDK:** `@modelcontextprotocol/server` ^2.4.0
8
+ **Zod:** ^4.6.7
9
9
 
10
10
  > **Read the framework docs first:** `node_modules/@cyanheads/mcp-ts-core/CLAUDE.md` contains the full API reference — builders, Context, error codes, exports, patterns. This file covers server-specific conventions only.
11
11
 
@@ -181,9 +181,15 @@ This server declares `sessionMode: 'stateless'` in `src/index.ts`: no tool calls
181
181
 
182
182
  ### Scorecard metric semantics
183
183
 
184
- Institution earnings cover federally aided undergraduate entrants working and not enrolled, regardless of completion. Earnings `years` is the published API data-file start-year domain 1996–2025, not an entry-year selector. `src/data/earnings-context.ts` holds official per-metric cohort/measurement/dollar provenance; refresh it from the dictionary when coverage changes, never infer vintage by arithmetic or sample equality.
184
+ Institution earnings cover federally aided undergraduate entrants working and not enrolled, regardless of completion. Earnings `years` is the published API data-file start-year domain 1996–2025, not an entry-year selector. `src/data/earnings-context.ts` holds official per-metric cohort/measurement/dollar provenance. During maintenance, run `bun run check:earnings-provenance` (Python 3 required) against the official dictionary; an optional local XLSX path avoids downloading it again. Resolve drift from the dictionary before release, never infer vintage by arithmetic or sample equality. This is a development check, not a per-request fetch.
185
185
 
186
- `readMetric` distinguishes numbers (including zero), unknown-cause null/absence, and explicit `PS` / `PrivacySuppressed` markers. Suppression flags/counts require the latter; unexpected values are provider contract failures. CIP lookup remains a curated set of current NCES four-digit groups, with exact group/family titles and verified descendant aliases. Program search applies CIP × credential together upstream before school pagination; totals remain school-level.
186
+ `readMetric` distinguishes numbers (including zero), unknown-cause null/absence, and explicit `PS` / `PrivacySuppressed` markers. Suppression flags/counts require the latter; unexpected values are provider contract failures. CIP lookup covers all current NCES CIP 2020 four-digit groups, with exact group/family titles and verified descendant aliases. Program search applies CIP × credential together upstream before school pagination; totals remain school-level.
187
+
188
+ `src/data/repayment-context.ts` holds dictionary provenance for discontinued declining-balance rates and the separate two- and four-year undergraduate federal Direct Loan borrower statuses. The three-year declining-balance rate uses the 2016–17 file. Profiles and value analysis retain each borrower-status horizon's OPEID6 scope, denominator when available, and reported decimal or bin. Only point estimates carry numeric values. Unsupported status rates yield an unavailable value and a notice. Comparison ranks use the historical three-year rate.
189
+
190
+ `school.operating` is current operating certification. Actual closure requires separate evidence. School search applies certification, HBCU, locale, price, and completion filters upstream before pagination. Use observed provider support for catalog sort flags, including unsortable pooled full-time retention projections.
191
+
192
+ Program tool text leads with completion earnings and renders legacy highest-credential values separately. School comparison leaves descriptive metrics, singleton results, shared OPEID6 outcomes, and mixed price periods unranked. Negative dollar selectors normalize to zero with a notice. Earnings vintages deduplicate in caller order. Six-digit CIP lookup resolves to the four-digit group used by program selectors.
187
193
 
188
194
  ---
189
195
 
@@ -285,9 +291,18 @@ src/
285
291
 
286
292
  ## Skills
287
293
 
288
- Skills are modular instructions in `framework-skills/` at the project root. Read them directly when a task matches — e.g., `framework-skills/add-tool/SKILL.md` when adding a tool. `bun run list-skills` prints the full registry. The directory is deliberately not `skills/`: Claude Code and Codex auto-load a plugin's root `skills/`, so a server that ships `.claude-plugin/` or `.codex-plugin/` would hand these development skills to every agent that installs it. Keep `skills/` free for skills meant for those agents.
294
+ Two skill trees live at the project root, written for different readers:
295
+
296
+ | Tree | For | Written by |
297
+ |:-----|:----|:-----------|
298
+ | `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) |
299
+ | `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 |
300
+
301
+ 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.
302
+
303
+ 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.
289
304
 
290
- **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.
305
+ **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.
291
306
 
292
307
  Available skills:
293
308
 
@@ -299,6 +314,7 @@ Available skills:
299
314
  | `add-app-tool` | Scaffold an MCP App tool + paired UI resource |
300
315
  | `add-resource` | Scaffold a new resource definition |
301
316
  | `add-prompt` | Scaffold a new prompt definition |
317
+ | `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 |
302
318
  | `add-service` | Scaffold a new service integration |
303
319
  | `add-test` | Scaffold test file for a tool, resource, or service |
304
320
  | `field-test` | Exercise tools/resources/prompts with real inputs, verify behavior, report issues |
@@ -346,7 +362,7 @@ When you complete a skill's checklist, check the boxes and add a completion time
346
362
  | `bun run audit:fix` | `bun audit fix` — upgrade vulnerable packages to the lowest safe version within existing ranges (`--dry-run` previews, `--latest` rewrites ranges). First response when `devcheck` flags a transitive advisory; then `bun update <name>`, then `bun dedupe` |
347
363
  | `bun run audit:refresh` | Delete `bun.lock` and reinstall. Last resort after `audit:fix`, `bun update <name>`, and `bun dedupe` — re-resolves every ranged dep (the framework pin included) and rewrites the lockfile as `lockfileVersion: 2` |
348
364
  | `bun run lint:mcp` | Run the MCP definition linter standalone (rule catalog: `api-linter` skill) |
349
- | `bun run lint:packaging` | Packaging surface checks — `server.json`/`manifest.json` env-var parity (run by devcheck) |
365
+ | `bun run lint:packaging` | Packaging surface checks — `server.json`/`manifest.json` env-var parity, `.mcpbignore` coverage, plugin manifests, the published `skills/` tree (run by devcheck) |
350
366
  | `bun run list-skills` | Print the skill registry |
351
367
  | `bun run tree` | Generate directory structure doc |
352
368
  | `bun run format` | Auto-fix formatting (safe fixes only) |
@@ -366,7 +382,7 @@ When you complete a skill's checklist, check the boxes and add a completion time
366
382
 
367
383
  ## Bundling
368
384
 
369
- `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 two classes of `node_modules/**` content that root-anchored `.mcpbignore` patterns cannot reach: dependency-shipped agent docs (`framework-skills/`, `skills/`, `.claude/`, `.agents/`, `SKILL.md`) and platform-specific native bindings, which would otherwise lock the bundle to the platform it was packed on. A server using DataCanvas therefore ships a portable bundle without the DuckDB native — `@duckdb/node-api` is an optional peer loaded lazily, so canvas tools report an actionable install hint and every other tool works normally. MCPB is stdio-only — HTTP and Cloudflare Workers deployments are unaffected. Consumers who don't need it can delete `manifest.json` and `.mcpbignore`; `lint:packaging` skips cleanly.
385
+ `npm run bundle` produces a `.mcpb` extension bundle for one-click install in Claude Desktop. `mcpb pack` reads `.mcpbignore`, not `.gitignore`, so every root path the bundle must not ship — source, tests, docs, caches, logs, agent docs, plugin manifests, gitignored local files — needs a root-anchored line there (`/docs/`, never `docs/`, which also strips `node_modules/*/docs/`); `lint:packaging` names each missing or unanchored line. Root `skills/` ships on purpose. 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/`, `AGENTS.md`, and `CLAUDE.md` at a package's root, and `SKILL.md` anywhere (a `skills/` deeper in a package is runtime code and stays); platform-specific native bindings, which would otherwise lock the bundle to the platform it was packed on; node-gyp build output beside a `binding.gyp` (`config.gypi`, Makefiles, `obj.target/`, `.deps/`, object files and debug symbols), which records the packing machine's paths — compiled `*.node` addons stay; and build-only package files — the framework's `changelog/`, `scripts/`, `templates/`, and config bases (`biome.json`, `vitest.config.base.mjs`, `config/tsconfig.base.json`), and `better-sqlite3`'s `deps/` and `src/` C sources. A server using DataCanvas therefore ships a portable bundle without the DuckDB native — `@duckdb/node-api` is an optional peer loaded lazily, so canvas tools report an actionable install hint and every other tool works normally. MCPB is stdio-only — HTTP and Cloudflare Workers deployments are unaffected. Consumers who don't need it can delete `manifest.json` and `.mcpbignore`; `lint:packaging` skips cleanly.
370
386
 
371
387
  **Adding an env var requires both files:** `server.json` (registry discovery, `environmentVariables[]`) and `manifest.json` (bundle install UX, `mcp_config.env` + `user_config`). `lint:packaging` (run by `devcheck`) verifies the env var names match, that every `user_config` option is wired into `mcp_config.env` as `"X": "${user_config.X}"` (the host substitutes nothing else — `"${X}"` reaches the server as that literal string), and that an optional string option carries `"default": ""`.
372
388
 
@@ -433,7 +449,10 @@ import { getMyService } from '@/services/my-domain/my-service.js';
433
449
  - [ ] If wrapping external API: tests include at least one sparse payload case with omitted upstream fields
434
450
  - [ ] Registered in `createApp()` arrays (directly or via barrel exports)
435
451
  - [ ] Tests use `createMockContext()` from `@cyanheads/mcp-ts-core/testing`
452
+ - [ ] `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 `.codex-plugin/mcp.json`. Neither file carries a user-supplied variable (no `""`, no `${…}` besides `${PLUGIN_ROOT}`/`${PLUGIN_DATA}`): keys reach Codex through `.codex-plugin/mcp.json` `env_vars`, which works only while `.codex-plugin/plugin.json` keeps `"mcpServers": "./.codex-plugin/mcp.json"`
436
453
  - [ ] `.codex-plugin/plugin.json` populated — `name`, `version`, `description`, `repository`, `license` from `package.json`; `interface.displayName` = the unscoped repo name (never the npm scope — `lint:packaging` enforces this); `interface.shortDescription` from `package.json` description
437
454
  - [ ] `.codex-plugin/mcp.json` updated — server name key is the unscoped repo name; every user-supplied variable (API key, contact email, instance URL) is listed in `env_vars` so Codex forwards it from the user's environment. Never write `"KEY": ""` into `env` — an empty value replaces the user's exported key and is read as unset
438
455
  - [ ] `.claude-plugin/plugin.json` populated — `name`, `version`, `description`, `author`, `repository`, `license`, `keywords` from `package.json`; inline `mcpServers` entry keyed by the unscoped repo name. Every user-supplied variable is declared under `userConfig` (`type`, `title`, `description`; `sensitive: true` for keys and tokens; `required: true` or `default: ""`) and referenced from `env` as `"KEY": "${user_config.<option>}"` — mirror the `user_config` block in `manifest.json`. Never write `"KEY": ""` into `env`
439
- - [ ] `npm run devcheck` passes
456
+ - [ ] Any new root dev path has a root-anchored line in `.mcpbignore` and passes `bun run bundle` and `lint:packaging`
457
+ - [ ] Field-test verified every gate and identifier claim in the published `skills/college-scorecard-guide` against live tool behavior
458
+ - [ ] `bun run devcheck` passes
package/Dockerfile CHANGED
@@ -14,7 +14,7 @@
14
14
  # output. A stage that compiles a native addon needs the target-arch toolchain
15
15
  # and cannot cross-compile this way — drop the flag there.
16
16
  # ==============================================================================
17
- FROM --platform=$BUILDPLATFORM oven/bun:1.4.3 AS build
17
+ FROM --platform=$BUILDPLATFORM oven/bun:1.4.4 AS build
18
18
 
19
19
  WORKDIR /usr/src/app
20
20
 
@@ -30,6 +30,7 @@ RUN --mount=type=cache,target=/root/.bun/install/cache \
30
30
 
31
31
  # Copy the rest of the source code
32
32
  COPY . .
33
+ RUN mkdir -p skills
33
34
 
34
35
  # Build the application
35
36
  RUN bun run build
@@ -41,7 +42,7 @@ RUN bun run build
41
42
  # Run the scanner and OTel installer natively, cross-installing production
42
43
  # dependencies for the target architecture. Only node_modules leaves this stage.
43
44
  # ==============================================================================
44
- FROM --platform=$BUILDPLATFORM oven/bun:1.4.3 AS deps
45
+ FROM --platform=$BUILDPLATFORM oven/bun:1.4.4 AS deps
45
46
 
46
47
  WORKDIR /usr/src/app
47
48
 
@@ -90,7 +91,7 @@ RUN rm -rf node_modules/@socketsecurity/bun-security-scanner
90
91
  #
91
92
  # Runtime-only stage: Bun runs on the target architecture at container start.
92
93
  # ==============================================================================
93
- FROM oven/bun:1.4.3-slim AS production
94
+ FROM oven/bun:1.4.4-slim AS production
94
95
 
95
96
  WORKDIR /usr/src/app
96
97
  ENV NODE_ENV=production
@@ -109,6 +110,7 @@ COPY --from=deps /usr/src/app/node_modules ./node_modules
109
110
 
110
111
  # Copy the compiled application code from the build stage
111
112
  COPY --from=build /usr/src/app/dist ./dist
113
+ COPY --from=build /usr/src/app/skills ./skills
112
114
 
113
115
  # The 'oven/bun' image already provides a non-root user named 'bun'.
114
116
  # We will use this existing user for enhanced security.
package/README.md CHANGED
@@ -1,13 +1,13 @@
1
1
  <div align="center">
2
2
  <h1>@cyanheads/college-scorecard-mcp-server</h1>
3
3
  <p><b>Search, compare, and analyze U.S. college data — costs, earnings, programs, and outcomes — via MCP. STDIO or Streamable HTTP.</b>
4
- <div>9 Tools • 2 Resources • 1 Prompt</div>
4
+ <div>9 Tools • 3 Resources • 1 Prompt • 1 Skill</div>
5
5
  </p>
6
6
  </div>
7
7
 
8
8
  <div align="center">
9
9
 
10
- [![Version](https://img.shields.io/badge/Version-0.4.0-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/college-scorecard-mcp-server) [![MCP SDK](https://img.shields.io/badge/MCP%20SDK-^2.3.1-green.svg?style=flat-square)](https://modelcontextprotocol.io/) [![npm](https://img.shields.io/npm/v/@cyanheads/college-scorecard-mcp-server?style=flat-square&logo=npm&logoColor=white)](https://www.npmjs.com/package/@cyanheads/college-scorecard-mcp-server) [![TypeScript](https://img.shields.io/badge/TypeScript-^7.0.2-3178C6.svg?style=flat-square)](https://www.typescriptlang.org/) [![Bun](https://img.shields.io/badge/Bun-v1.4.3-blueviolet.svg?style=flat-square)](https://bun.sh/)
10
+ [![Version](https://img.shields.io/badge/Version-0.6.0-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/college-scorecard-mcp-server) [![MCP SDK](https://img.shields.io/badge/MCP%20SDK-^2.4.0-green.svg?style=flat-square)](https://modelcontextprotocol.io/) [![npm](https://img.shields.io/npm/v/@cyanheads/college-scorecard-mcp-server?style=flat-square&logo=npm&logoColor=white)](https://www.npmjs.com/package/@cyanheads/college-scorecard-mcp-server) [![TypeScript](https://img.shields.io/badge/TypeScript-^7.0.2-3178C6.svg?style=flat-square)](https://www.typescriptlang.org/) [![Bun](https://img.shields.io/badge/Bun-v1.4.4-blueviolet.svg?style=flat-square)](https://bun.sh/)
11
11
 
12
12
  </div>
13
13
 
@@ -29,20 +29,20 @@
29
29
 
30
30
  ## Overview
31
31
 
32
- U.S. college data from the Department of Education College Scorecard API — costs, earnings, programs, and outcomes across roughly 6,500 Title IV institutions. Search and compare schools, look up program-level earnings by field of study, and compute ROI metrics like debt-to-earnings ratio from any MCP client. Runs as a stdio process, a local Streamable HTTP server, or the public hosted endpoint above.
32
+ US college data from the Department of Education College Scorecard: costs, earnings, programs, and outcomes across roughly 6,500 institutions. Search and compare schools, look up earnings by field of study, and compute ROI metrics like debt-to-earnings ratio.
33
33
 
34
34
  ### Tools
35
35
 
36
36
  | Tool | Description |
37
37
  |:---|:---|
38
- | `scorecard_search_schools` | Search institutions by name, location, type, size, acceptance rate, and single-sex flags; sort by supported API fields. Returns core identity and cost metrics. |
39
- | `scorecard_get_school` | Full institutional profiles — costs, admissions, outcomes, aid, demographics, single-sex flags, and completion rates. |
40
- | `scorecard_compare_schools` | Normalized side-by-side comparison of 2–5 schools on a named topic. Returns metric values, ordinal ranks, and a comparison summary. |
41
- | `scorecard_get_programs` | Field-of-study programs at one school: median 1-year earnings, cumulative Stafford/Grad PLUS debt, and IPEDS awards in the two pooled debt-cohort years. |
38
+ | `scorecard_search_schools` | Search institutions by location, price, completion, operating certification, and other school characteristics. Sort by fields with API sort support. |
39
+ | `scorecard_get_school` | Institutional profiles with costs, admissions, aid, demographics, completion, and borrower repayment outcomes. |
40
+ | `scorecard_compare_schools` | Side-by-side comparison of up to five schools. Includes ranks and leaders for eligible metrics. |
41
+ | `scorecard_get_programs` | Paged field-of-study outcomes: completion-based 1/4-year earnings, legacy highest-credential earnings, debt, and campus awards with separate provenance. |
42
42
  | `scorecard_search_programs` | Find programs by CIP and credential or keyword, retain degree identity, and rank earnings within each fetched school page. |
43
43
  | `scorecard_get_earnings` | Earnings of federally aided undergraduate entrants working and not enrolled, regardless of completion: 6/8/10-year medians, 6/10-year P25/P75, and non-male/male 6-year medians. |
44
- | `scorecard_value_analysis` | Workflow tool: parallel-fetches cost, debt, repayment, and earnings data, then computes ROI metrics — debt-to-earnings ratio and net price to earnings ratio. |
45
- | `scorecard_lookup_cip` | Search 186 curated NCES CIP 2020 four-digit groups by code, keyword, or verified descendant alias. Served offline. |
44
+ | `scorecard_value_analysis` | Combines cost, debt, repayment, and earnings data with debt-to-earnings and net-price-to-earnings ratios. |
45
+ | `scorecard_lookup_cip` | Search all 464 current NCES CIP 2020 four-digit groups by code, keyword, or related program name. Served offline. |
46
46
  | `scorecard_list_fields` | Search the Scorecard field catalog by keyword. Returns matching field paths, descriptions, data types, and sort support. Use before passing custom `fields` parameters. |
47
47
 
48
48
  ### Resources
@@ -50,10 +50,13 @@ U.S. college data from the Department of Education College Scorecard API — cos
50
50
  | Resource | Description |
51
51
  |:---|:---|
52
52
  | `scorecard://school/{id}` | Institutional profile by unit ID — injectable context for school-specific conversations |
53
- | `scorecard://programs/{id}` | Program-level outcomes for a school |
53
+ | `scorecard://programs/{id}` | First page of program-level outcomes for a school |
54
+ | `scorecard://programs/{id}/page/{offset}/{limit}` | Continuation through a school's programs in provider order |
54
55
 
55
56
  All resource data is also reachable via tools. Use `scorecard_search_schools` or `scorecard_get_school` to discover school IDs before constructing resource URIs.
56
57
 
58
+ The [college-scorecard-guide](./skills/college-scorecard-guide/SKILL.md) covers campus/CIP identity, paging, outcome populations, OPEID6 pooling, and price-basis gates. It ships with plugins, npm, Docker, and MCPB, and is served through the MCP Skills extension.
59
+
57
60
  ### Prompts
58
61
 
59
62
  | Prompt | Description |
@@ -64,71 +67,78 @@ All resource data is also reachable via tools. Use `scorecard_search_schools` or
64
67
 
65
68
  ### `scorecard_search_schools` <sub>tool</sub>
66
69
 
67
- - Search by name, state, ownership, degree level, size, acceptance rate, CIP code, or ZIP and distance; `per_page` is capped at 100 with zero-indexed `page`. State lists accept `WA,CA` and case variants. ZIP-only queries use the provider default radius; ZIP+4 and nonnegative decimal radii (bare miles, `mi`, or `km`) are supported. Reversed range bounds normalize to ascending order.
68
- - Returns core identity and cost metrics for quick scanning.
69
- - `sort` forwards a supported API expression such as `latest.cost.avg_net_price.overall:asc` (`:desc` reverses it). `men_only` and `women_only` accept true or false; omission includes unknown flags, while false selects only explicit zero values.
70
+ - Search by name, state, ownership, degree level, size, acceptance rate, CIP code, or ZIP and distance (miles or `km`). State lists accept `WA,CA`.
71
+ - Apply `max_net_price` (inclusive dollars), `min_completion_rate` (inclusive 0–1), and `locale` (`city`, `suburb`, `town`, or `rural`) before paging. These filters require reported values. Negative price ceilings normalize to $0 with a notice.
72
+ - `operating`, `hbcu`, `men_only`, and `women_only` match explicit true or false values. Omit a flag to include unknown values. `operating: false` means the school lacks current operating certification. Verify actual closure separately.
73
+ - Use `scorecard_list_fields` to choose a supported sort such as `latest.cost.avg_net_price.overall:asc`. Omit sort to preserve API order. `page` starts at zero and `per_page` is capped at 100.
70
74
 
71
75
  ---
72
76
 
73
77
  ### `scorecard_get_school` <sub>tool</sub>
74
78
 
75
- - Accepts a positive safe integer school ID or an array of up to 1,000 elements. Base-10 strings accept whitespace, leading zeros, plus, decimal, and exponent forms when they resolve to a positive safe integer. IDs are deduplicated before the first 100 distinct IDs are fetched; `missing_ids` identifies processed IDs without records, and `unprocessed_ids` identifies unchecked excess.
76
- - Default profiles cover costs, admissions, outcomes, financial aid, demographics, and completion rates. Custom `fields` returns real ID/name plus `selected_fields` containing requested JSON values, including null, zero, false, and nested arrays. Program leaf projections retain the provider's parent subtree under `latest.programs.cip_4_digit`, preserving sparse rows and array positions; `absent_fields` lists requested paths omitted from that response. Unrequested profile metrics are omitted.
77
- - HBCU status reads `school.minority_serving.historically_black`; HBCU and single-sex flags preserve true/false when known and remain unknown when absent/null.
79
+ - Accepts a school ID or an array of up to 1,000. The first 100 distinct IDs are fetched; `missing_ids` lists fetched IDs with no record and `unprocessed_ids` lists the excess.
80
+ - Default profiles cover costs, admissions, outcomes, financial aid, demographics, and completion rates. Custom `fields` returns `selected_fields` with the requested values, and `absent_fields` lists requested paths missing from the response.
81
+ - Projected program arrays use `program_limit` (default 20, capped at 100) and `program_offset`; follow each school's `program_pages` continuation. Raw values and provider positions stay aligned.
82
+ - Profiles include operating certification, HBCU, and single-sex flags as booleans when known, omitting unknown flags. The discontinued three-year declining-balance rate carries its 2016–17 file, repayment-entry cohorts, and measurement period.
83
+ - `borrower_repayment` reports making progress, default, and paid in full at two and four years for OPEID6-pooled undergraduate federal Direct Loan borrowers. Each horizon has its own provenance and cohort count when available. Published bins are preserved verbatim, and only decimal point estimates have numeric values.
78
84
 
79
85
  ---
80
86
 
81
87
  ### `scorecard_compare_schools` <sub>tool</sub>
82
88
 
83
- - Accepts 2–5 school unit IDs and one topic: `costs`, `admissions`, `outcomes`, or `aid`.
84
- - Returns metric values and competition ranks from one API call: equal values share a rank (1, 1, 3), unavailable values are unranked, and summaries name tied leaders. Each cell's `availability` distinguishes available, unavailable with unknown cause, and confirmed privacy suppression.
89
+ - Accepts 2–1,000 raw unit IDs and compares the first five distinct normalized IDs on `costs`, `admissions`, `outcomes`, or `aid`. `missing_ids` were queried; `unprocessed_ids` were not.
90
+ - Equal eligible values share a rank (1, 1, 3). Admissions metrics, aid participation rates, shared OPEID6 outcome rows, and mixed academic/program-year price rows are unranked. A single found school yields values without ranks or leaders.
91
+ - Each cell's `availability` distinguishes available, unavailable, and privacy-suppressed values. Repayment rows rank the historical three-year declining-balance measure and identify its vintage.
85
92
 
86
93
  ---
87
94
 
88
95
  ### `scorecard_get_programs` <sub>tool</sub>
89
96
 
90
- - Accepts one school ID, with optional CIP code, `credential_level`, and minimum earnings filters.
91
- - Existing schools with no reported or matching programs return a successful empty collection; a nonexistent school remains a lookup error. Dotted and undotted four-digit CIP codes are equivalent.
92
- - Returns median earnings and the matching count of graduates working and not enrolled 1 year after their highest credential. `earnings_availability` is `available`, `unavailable`, or `privacy_suppressed`; zero is available, null/absence has unknown cause, and only explicit provider privacy markers set `suppressed`. `suppressed_count` and `unavailable_count` distinguish those populations.
93
- - `median_debt` is completers' cumulative Stafford/Grad PLUS borrowing across attended institutions at the same academic level. `ipeds_awards_year1` and `ipeds_awards_year2` count awards in each year of the pooled debt cohort, not enrollment or unique students.
97
+ - Accepts one school ID, with optional CIP code (dotted or undotted), `credential_level`, and minimum earnings filters. A school with no matching programs returns an empty collection; a nonexistent school is a lookup error.
98
+ - Text results lead with completion-based one- and four-year medians, counts, and availability. Legacy `earnings_1yr_median` and `earnings_count` appear separately with their highest-credential provenance.
99
+ - `min_earnings` and ordering use completion-based one-year earnings, with no legacy fallback. Page with `limit` (default 20, capped at 100) and `offset`; `total` counts matches before paging, and `nextOffset` is null at the end.
100
+ - `median_debt` is completers' cumulative Stafford/Grad PLUS borrowing. `ipeds_awards_year1` and `ipeds_awards_year2` count awards in each year of the pooled debt cohort, not students.
94
101
 
95
102
  ---
96
103
 
97
104
  ### `scorecard_search_programs` <sub>tool</sub>
98
105
 
99
- - Search by CIP code or program name, with optional integer `credential_level`, state, ownership, net price, earnings, and debt filters; zero-indexed `page` and `per_page` (up to 100) paginate schools, so returned program rows may exceed `per_page`. CIP and credential selectors apply together upstream before pagination; credential codes are 1–8 and 99, and unknown integer selectors may return no matches.
100
- - Returns school IDs and names alongside program metrics and provider `credential_level` / `credential_title`, retaining separate credentials and future numeric credential levels; missing identity fields stay absent. Available earnings sort first; availability states and counts have the same meaning as in `scorecard_get_programs`.
101
- - `min_earnings` filters locally; totals and pagination remain upstream school counts before local filtering. Earnings/debt thresholds are inclusive and exclude unknown values. Debt and award fields have the same meanings as in `scorecard_get_programs`.
106
+ - Search by CIP code or program name, with optional integer `credential_level` (1–8 and 99), state, ownership, net price, earnings, and debt filters. `page`/`per_page` paginate schools; `offset`/`limit` page matching program rows within that school page.
107
+ - Returns school IDs and names with program metrics, `credential_level`, and `credential_title`. Available earnings sort first; availability, debt, and award fields match `scorecard_get_programs`.
108
+ - `min_earnings` filters locally, so totals and pagination are upstream school counts before filtering. Earnings and debt thresholds are inclusive and exclude unknown values. Negative `max_net_price` and `max_debt` normalize to $0 with a notice.
109
+ - Follow `next` until null: it drains program rows on the current school page before advancing. `shown` counts returned programs; `schools_shown` counts fetched schools. Rankings use current completion-based one-year earnings within the fetched page only.
102
110
 
103
111
  ---
104
112
 
105
113
  ### `scorecard_get_earnings` <sub>tool</sub>
106
114
 
107
- - Accepts one school ID and optional API data-file start `years` from 1996–2025. These are file vintages, not entry years; valid files without observations succeed. Omit `years` or pass `[]` for snapshot only.
108
- - Institution earnings cover federally aided undergraduate entrants working and not enrolled, regardless of completion. Returns medians at 6/8/10 years after entry, 6/10-year P25/P75, and non-male/male 6-year medians.
109
- - `current_availability` and `trend[].availability` classify each supported metric; aggregate `suppressed` requires every current metric to carry an explicit privacy marker. Null and omitted values do not prove suppression.
110
- - `current_context` and `trend[].context` give per-metric `data_file_year`, verified academic `entry_cohorts`, calendar `measurement_years`, and inflation `dollar_year`. Metadata comes from the official cohort maps; unknown parts are omitted. Values retain the provider's dollar basis, so cross-vintage comparisons require checking it. Each requested file adds six- and ten-year medians without extra requests.
115
+ - Accepts one school ID and optional data-file start `years` from 1996–2025 (file vintages, not entry years). Duplicate years collapse in caller order. Omit `years` or pass `[]` for the snapshot only.
116
+ - Covers federally aided undergraduate entrants working and not enrolled, regardless of completion: medians at 6/8/10 years after entry, 6/10-year P25/P75, and non-male/male 6-year medians.
117
+ - `current_availability` and `trend[].availability` classify each metric. `current_context` and `trend[].context` give `data_file_year`, `entry_cohorts`, `measurement_years`, and `dollar_year`; values keep the provider's dollar basis, so check it before comparing across vintages.
111
118
 
112
119
  ---
113
120
 
114
121
  ### `scorecard_value_analysis` <sub>tool</sub>
115
122
 
116
- - Accepts one school ID; optional `family_income` selects the applicable net price bracket ($0–30k, $30k–48k, $48k–75k, $75k–110k, or $110k+).
117
- - Returns ratios alongside their source figures. `effective_net_price` discloses the price used, and `net_price_basis` is `income_bracket`, `overall`, or `unavailable`. A missing bracket preserves requested `family_income` / `applicable_income_bracket` and explains the established overall-price fallback in `data_notes`. Zero estimates remain valid; ratios require positive annual earnings.
123
+ - Accepts one school ID. Optional `family_income` selects the applicable net price bracket ($0–30k, $30k–48k, $48k–75k, $75k–110k, or $110k+). Negative income normalizes to $0 with a notice.
124
+ - Returns ratios alongside their source figures. `effective_net_price` is the price used, and `net_price_basis` is `income_bracket`, `overall`, or `unavailable`.
125
+ - If the bracket is missing, the overall price is used and `data_notes` says so. Ratios require positive annual earnings.
126
+ - Tuition and attendance independently prefer academic-year prices, otherwise reported program-year prices; each has its own basis. Program tuition never substitutes for net price. OPEID6-pooled outcomes are identified alongside campus prices.
127
+ - Returns the same two- and four-year borrower statuses and historical three-year declining-balance context as the default school profile.
118
128
 
119
129
  ---
120
130
 
121
131
  ### `scorecard_lookup_cip` <sub>tool</sub>
122
132
 
123
- - Search by nonblank keyword or partial name (default limit 20); requests up to 1,000 are accepted and served at a cap of 50. Totals count before slicing; exact-cap results are not truncated, and no matches return an empty collection with guidance.
124
- - Dotted `11.07` and undotted `1107` queries are equivalent. Returns exact group titles and family identities from 186 current NCES CIP 2020 four-digit groups; verified six-digit descendant names act as keyword aliases, returning their canonical group. This curated set is a subset of the full taxonomy.
133
+ - Search by nonblank keyword or partial name (default limit 20, capped at 50). Dotted `11.07` and undotted `1107` are equivalent; continue with `offset` and returned `nextOffset` while more results remain.
134
+ - Returns exact group titles and family identities from all 464 current NCES CIP 2020 four-digit groups. Six-digit queries such as `11.0701` and `110701` resolve to their four-digit group with a notice. Program tools take the four-digit group. No matches return an empty collection with guidance.
125
135
 
126
136
  ---
127
137
 
128
138
  ### `scorecard_list_fields` <sub>tool</sub>
129
139
 
130
- - Search 78 curated offline field entries by nonblank query (default limit 30); requests up to 1,000 are accepted and served at a cap of 100. Totals count before slicing; no matches return an empty collection with guidance. Use before passing custom `fields` to `scorecard_get_school`.
131
- - Returns field paths, descriptions, data types, categories, and API sorting support; `tip` flags results containing unsortable fields.
140
+ - Search 94 curated offline field entries by nonblank query (default limit 30, capped at 100). Use before passing custom `fields` to `scorecard_get_school`, including pooled full-time retention for four-year and less-than-four-year institutions.
141
+ - Returns field paths, descriptions, data types, categories, and observed API sorting support. `tip` flags results containing unsortable fields.
132
142
 
133
143
  ---
134
144
 
@@ -142,16 +152,17 @@ All resource data is also reachable via tools. Use `scorecard_search_schools` or
142
152
 
143
153
  ### `scorecard://programs/{id}` <sub>resource</sub>
144
154
 
145
- - Program-level outcomes as `application/json` — CIP code, title, credential level, 1-year earnings, cumulative Stafford/Grad PLUS debt, and `ipeds_awards_year1` / `ipeds_awards_year2` with the same meanings as the program tools; missing metrics are null
155
+ - First 20 program rows as `application/json`, in provider order, with current/legacy earnings populations, scope, provenance, debt, and campus awards. Legacy nullable metrics retain null; new metrics expose availability.
146
156
  - `id` is the school unit ID from `scorecard_search_schools`
147
157
  - `list` returns a handful of example school URIs; use `scorecard_search_schools` to discover others
158
+ - Follow `nextUri` through `scorecard://programs/{id}/page/{offset}/{limit}` until null. Page limits above 100 normalize to 100, with a hard ceiling of 1,000; offsets are zero-based. Tool ordering differs, so do not reuse offsets between tools and resources.
148
159
 
149
160
  ---
150
161
 
151
162
  ### `scorecard_compare_prompt` <sub>prompt</sub>
152
163
 
153
164
  - Arguments: `school_names` (comma-separated list) and `focus` (`costs` | `outcomes` | `programs`), both required
154
- - Costs/outcomes sequence school discovery, `scorecard_compare_schools`, profiles, and value analysis. Programs sequence school discovery, CIP lookup, and `scorecard_get_programs` per school; compare matching credentials using returned earnings, cumulative debt, and IPEDS awards in the two pooled debt-cohort years. Missing values and award-year dates remain unknown.
165
+ - `costs` and `outcomes` run school discovery, `scorecard_compare_schools`, profiles, and value analysis. `programs` runs school discovery, CIP lookup, and `scorecard_get_programs` per school, then compares completion earnings, debt, and IPEDS awards for matching credentials.
155
166
 
156
167
  ## Features
157
168
 
@@ -160,19 +171,17 @@ Built on [`@cyanheads/mcp-ts-core`](https://github.com/cyanheads/mcp-ts-core): s
160
171
  College Scorecard-specific:
161
172
 
162
173
  - Full College Scorecard API coverage: ~6,500 Title IV institutions, ~2,800 data fields spanning costs, outcomes, demographics, financial aid, and field-of-study earnings
163
- - Program-level earnings: median earnings of graduates working and not enrolled 1 year after their highest credential, per school × CIP code × credential level
174
+ - Program completion earnings at one and four years, alongside legacy highest-credential outcomes; each series carries its population and vintage
164
175
  - Field pre-selection per tool — curated field sets appropriate to each tool's purpose; optional `fields` override for custom queries
165
- - Embedded CIP taxonomy (186 groups) and field catalog (78 fields) served offline
176
+ - Embedded CIP taxonomy (464 groups) and field catalog (94 fields) served offline
166
177
  - Geographic filtering via U.S. zip code + distance radius
167
178
 
168
179
  Agent-friendly output:
169
180
 
170
- - Availability distinguishes numbers (including zero), unknown-cause missing values, and explicit `PS` / `PrivacySuppressed` provider markers. Confirmed privacy suppression does not identify its particular cause.
171
- - Derived metrics alongside source figures in `scorecard_value_analysis` — agents can verify arithmetic and branch on computed values, not raw numbers
172
- - Ordinal ranks in `scorecard_compare_schools` place each school's metric values within the comparison set
173
- - School sorting uses indexed API fields; check `scorecard_list_fields` before choosing a sort expression. Six-year earnings does not support API sorting. Program earnings ordering applies within each fetched school page.
174
- - Searches echo normalized effective selectors and pagination defaults on both output surfaces, distinguishing no upstream matches, exhausted pages, and locally filtered program pages. Blank optional selectors are unset; CIP precedence omits an ignored program name from the echo.
175
- - Provider failures retain safe status/detail with distinct validation, authentication, throttling, timeout, and availability reasons. Permanent validation/authentication failures are not retried; resource failures include recovery and a request ID. Availability uses `api_error`; cancellation remains `RequestCancelled`.
181
+ - Availability distinguishes numbers (including zero), missing values, and explicit `PS` / `PrivacySuppressed` provider markers
182
+ - Derived metrics sit alongside source figures in `scorecard_value_analysis`, and `scorecard_compare_schools` adds ordinal ranks
183
+ - School sorting uses fields with observed API sort support. Six-year earnings and pooled full-time retention can be projected but cannot be used as sort fields.
184
+ - Provider failures report distinct validation, authentication, throttling, timeout, and availability reasons; availability uses `api_error` and cancellation remains `RequestCancelled`
176
185
 
177
186
  ## Getting started
178
187
 
@@ -259,7 +268,7 @@ MCP_TRANSPORT_TYPE=http MCP_HTTP_PORT=3010 SCORECARD_API_KEY=... bun run start:h
259
268
 
260
269
  ### Prerequisites
261
270
 
262
- - [Bun v1.4.0](https://bun.sh/) or higher (or Node.js v24+).
271
+ - [Bun v1.4.4](https://bun.sh/) or higher (or Node.js v24+).
263
272
  - A College Scorecard API key — free registration at [api.data.gov/signup](https://api.data.gov/signup/). Rate limit: 1,000 requests/hour per key.
264
273
 
265
274
  ### Installation
@@ -295,16 +304,18 @@ All configuration is validated at startup via Zod schemas in `src/config/server-
295
304
 
296
305
  | Variable | Description | Default |
297
306
  |:---|:---|:---|
298
- | `SCORECARD_API_KEY` | **Required.** API key from [api.data.gov](https://api.data.gov/signup/). 1,000 req/hour rate limit. | — |
307
+ | `SCORECARD_API_KEY` | **Required.** API key from [api.data.gov](https://api.data.gov/signup/) (1,000 req/hour). | — |
299
308
  | `MCP_TRANSPORT_TYPE` | Transport: `stdio` or `http` | `stdio` |
300
309
  | `MCP_HTTP_PORT` | HTTP server port | `3010` |
301
310
  | `MCP_HTTP_ENDPOINT_PATH` | HTTP endpoint path where the MCP server is mounted | `/mcp` |
302
- | `MCP_PUBLIC_URL` | Public origin override for TLS-terminating reverse-proxy deployments | none |
303
- | `MCP_SESSION_MODE` | HTTP session mode: `stateless`, `stateful`, or `auto`. The server declares `stateless` in code; an explicit env value overrides it. | `stateless` |
311
+ | `MCP_PUBLIC_URL` | Public HTTP or HTTPS base URL for TLS-terminating reverse-proxy deployments. Omit credentials, query parameters, and fragments. | none |
312
+ | `MCP_SESSION_MODE` | HTTP session mode: `stateless`, `stateful`, or `auto` | `stateless` |
304
313
  | `MCP_AUTH_MODE` | Authentication: `none`, `jwt`, or `oauth` | `none` |
314
+ | `MCP_JWT_EXPECTED_AUDIENCE` | Required `aud` claim in JWT mode. An unset audience logs a startup warning. | none |
315
+ | `MCP_JWT_EXPECTED_ISSUER` | Required `iss` claim in JWT mode. Configure it alongside the audience. | none |
305
316
  | `MCP_LOG_LEVEL` | Log level (`debug`, `info`, `warning`, `error`, etc.) | `info` |
306
- | `MCP_GC_PRESSURE_INTERVAL_MS` | Opt-in forced-GC pressure loop (ms, Bun only). Try `60000` if heap growth is observed under sustained HTTP load. | `0` (disabled) |
307
- | `LOGS_DIR` | Directory for log files (Node.js only) | `<project-root>/logs` |
317
+ | `MCP_GC_PRESSURE_INTERVAL_MS` | Opt-in forced-GC loop interval in ms (Bun only); try `60000` if heap grows under HTTP load | `0` (disabled) |
318
+ | `LOGS_DIR` | Directory for log files. Relative paths resolve from the server package root. | `<server-package-root>/logs` |
308
319
  | `STORAGE_PROVIDER_TYPE` | Storage backend: `in-memory`, `filesystem`, `supabase`, `cloudflare-kv/r2/d1` | `in-memory` |
309
320
  | `OTEL_ENABLED` | Enable [OpenTelemetry instrumentation](https://github.com/cyanheads/mcp-ts-core/tree/main/docs/telemetry) | `false` |
310
321
 
@@ -351,6 +362,7 @@ The Dockerfile defaults to HTTP transport, stateless session mode, and logs to `
351
362
  | `src/config` | Server-specific environment variable parsing and validation with Zod. |
352
363
  | `src/mcp-server/tools` | Tool definitions (`*.tool.ts`). Nine tools across search, profile, programs, earnings, and analysis. |
353
364
  | `src/mcp-server/resources` | Resource definitions. School profile and program outcomes resources. |
365
+ | `skills/college-scorecard-guide` | Published usage gates for school/program comparisons and complete retrieval. |
354
366
  | `src/mcp-server/prompts` | Prompt definitions. Multi-school comparison prompt. |
355
367
  | `src/services` | `ScorecardService` — fetch wrapper with retry, field selection, and pagination against the College Scorecard API. |
356
368
  | `tests/` | Unit and integration tests mirroring `src/`. |
@@ -363,6 +375,7 @@ See [`CLAUDE.md`](./CLAUDE.md) for development guidelines and architectural rule
363
375
  - Use `ctx.log` for request-scoped logging, `ctx.state` for tenant-scoped storage
364
376
  - Register new tools, resources, and prompts in the `createApp()` arrays in `src/index.ts`
365
377
  - Wrap external API calls: validate raw → normalize to domain type → return output schema; never fabricate missing fields
378
+ - During maintenance, run `bun run check:earnings-provenance` (Python 3 required) to compare embedded institution earnings metadata with the official dictionary. Pass a local XLSX path to reuse a download; requests never fetch the dictionary.
366
379
 
367
380
  ## Contributing
368
381
 
@@ -0,0 +1,41 @@
1
+ ---
2
+ summary: "Page program inventories and compare college outcomes with explicit populations, pooling, and price periods."
3
+ breaking: false
4
+ security: false
5
+ ---
6
+
7
+ # 0.5.0 — 2026-11-03
8
+
9
+ ## Added
10
+
11
+ - Add paged program resources and the college-scorecard-guide skill resource.
12
+ - **Program paging**: tools and custom program projections expose row limits, offsets, totals, and continuation; search drains program rows before advancing school pages. ([#28](https://github.com/cyanheads/college-scorecard-mcp-server/issues/28))
13
+ - **Completion earnings**: program outputs include separate one-/four-year medians, counts, availability, and dictionary provenance; one-year completion earnings drive filtering and ordering. Legacy highest-credential fields retain their population. ([#42](https://github.com/cyanheads/college-scorecard-mcp-server/issues/42))
14
+ - **CIP lookup**: cover all 464 current CIP 2020 four-digit groups, retain verified descendant aliases, and provide offset continuation. ([#46](https://github.com/cyanheads/college-scorecard-mcp-server/issues/46))
15
+ - **Comparison shortlists**: accept up to 1,000 raw IDs, compare the first five distinct normalized IDs, and report missing versus unprocessed IDs separately. ([#47](https://github.com/cyanheads/college-scorecard-mcp-server/issues/47))
16
+ - **Agent Plugins**: publish root plugin metadata and stdio connection configuration.
17
+
18
+ ## Changed
19
+
20
+ - **Output formatting**: preserve literal upstream/caller text in Markdown and render school website values as links.
21
+ - **Packaging and maintenance**: sync framework skills and validation scripts, retain SDK skills runtime modules in portable bundles, ship the guide across install surfaces, and use a one-day dependency release-age guard.
22
+
23
+ ## Fixed
24
+
25
+ - **Institution earnings provenance**: use the official 2022 data-file contexts and 2024 dollar basis for latest metrics, complete historical 2022 context, and add a maintenance dictionary check. ([#41](https://github.com/cyanheads/college-scorecard-mcp-server/issues/41))
26
+ - **OPEID6 pooling**: expose aggregation identity/scope and leave shared pooled comparison rows unranked; value analysis discloses pooled outcomes beside campus prices. ([#43](https://github.com/cyanheads/college-scorecard-mcp-server/issues/43))
27
+ - **Program-year prices**: retain tuition and attendance with independent price bases; mixed-period comparison rows stay unranked and unavailable prices carry no speculative explanation. ([#44](https://github.com/cyanheads/college-scorecard-mcp-server/issues/44))
28
+ - **Predominant degrees**: label 0 as “Not classified” and 4 as “Entirely graduate-degree granting.” ([#45](https://github.com/cyanheads/college-scorecard-mcp-server/issues/45))
29
+ - **Value analysis**: render the numeric debt-to-earnings ratio without uncited qualitative assessments. ([#48](https://github.com/cyanheads/college-scorecard-mcp-server/issues/48))
30
+
31
+ ## Dependencies
32
+
33
+ - `@cyanheads/mcp-ts-core` ^0.13.15 → ^0.13.18.
34
+ - `zod` ^4.6.5 → ^4.6.7.
35
+ - `@biomejs/biome` ^2.5.15 → ^2.6.4.
36
+ - `@types/node` ^26.6.5 → ^26.6.6.
37
+ - `ignore` ^7.0.12 → ^7.0.14.
38
+ - `tsc-alias` ^1.9.7 → ^1.9.10.
39
+ - `vitest` ^5.0.3 → ^5.0.5.
40
+ - Bun runtime and Docker images 1.4.3 → 1.4.4.
41
+ - Transitive MCP SDK `@modelcontextprotocol/server` 2.3.1 → 2.4.0.