@cyanheads/college-scorecard-mcp-server 0.1.9 → 0.3.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.
- package/AGENTS.md +13 -12
- package/CLAUDE.md +13 -12
- package/Dockerfile +52 -35
- package/README.md +62 -56
- package/changelog/0.2.x/0.2.0.md +47 -0
- package/changelog/0.3.x/0.3.0.md +31 -0
- package/dist/data/field-catalog.d.ts.map +1 -1
- package/dist/data/field-catalog.js +56 -49
- package/dist/data/field-catalog.js.map +1 -1
- package/dist/mcp-server/resources/definitions/programs.resource.d.ts +73 -1
- package/dist/mcp-server/resources/definitions/programs.resource.d.ts.map +1 -1
- package/dist/mcp-server/resources/definitions/programs.resource.js +93 -6
- package/dist/mcp-server/resources/definitions/programs.resource.js.map +1 -1
- package/dist/mcp-server/resources/definitions/school.resource.d.ts +73 -1
- package/dist/mcp-server/resources/definitions/school.resource.d.ts.map +1 -1
- package/dist/mcp-server/resources/definitions/school.resource.js +89 -3
- package/dist/mcp-server/resources/definitions/school.resource.js.map +1 -1
- package/dist/mcp-server/tools/definitions/compare-schools.tool.d.ts +61 -1
- package/dist/mcp-server/tools/definitions/compare-schools.tool.d.ts.map +1 -1
- package/dist/mcp-server/tools/definitions/compare-schools.tool.js +74 -11
- package/dist/mcp-server/tools/definitions/compare-schools.tool.js.map +1 -1
- package/dist/mcp-server/tools/definitions/get-earnings.tool.d.ts +61 -1
- package/dist/mcp-server/tools/definitions/get-earnings.tool.d.ts.map +1 -1
- package/dist/mcp-server/tools/definitions/get-earnings.tool.js +73 -7
- package/dist/mcp-server/tools/definitions/get-earnings.tool.js.map +1 -1
- package/dist/mcp-server/tools/definitions/get-programs.tool.d.ts +65 -8
- package/dist/mcp-server/tools/definitions/get-programs.tool.d.ts.map +1 -1
- package/dist/mcp-server/tools/definitions/get-programs.tool.js +118 -31
- package/dist/mcp-server/tools/definitions/get-programs.tool.js.map +1 -1
- package/dist/mcp-server/tools/definitions/get-school.tool.d.ts +69 -3
- package/dist/mcp-server/tools/definitions/get-school.tool.d.ts.map +1 -1
- package/dist/mcp-server/tools/definitions/get-school.tool.js +335 -171
- package/dist/mcp-server/tools/definitions/get-school.tool.js.map +1 -1
- package/dist/mcp-server/tools/definitions/list-fields.tool.d.ts +7 -7
- package/dist/mcp-server/tools/definitions/list-fields.tool.d.ts.map +1 -1
- package/dist/mcp-server/tools/definitions/list-fields.tool.js +28 -23
- package/dist/mcp-server/tools/definitions/list-fields.tool.js.map +1 -1
- package/dist/mcp-server/tools/definitions/lookup-cip.tool.d.ts +7 -7
- package/dist/mcp-server/tools/definitions/lookup-cip.tool.d.ts.map +1 -1
- package/dist/mcp-server/tools/definitions/lookup-cip.tool.js +27 -22
- package/dist/mcp-server/tools/definitions/lookup-cip.tool.js.map +1 -1
- package/dist/mcp-server/tools/definitions/search-programs.tool.d.ts +64 -2
- package/dist/mcp-server/tools/definitions/search-programs.tool.d.ts.map +1 -1
- package/dist/mcp-server/tools/definitions/search-programs.tool.js +153 -36
- package/dist/mcp-server/tools/definitions/search-programs.tool.js.map +1 -1
- package/dist/mcp-server/tools/definitions/search-schools.tool.d.ts +65 -1
- package/dist/mcp-server/tools/definitions/search-schools.tool.d.ts.map +1 -1
- package/dist/mcp-server/tools/definitions/search-schools.tool.js +129 -18
- package/dist/mcp-server/tools/definitions/search-schools.tool.js.map +1 -1
- package/dist/mcp-server/tools/definitions/value-analysis.tool.d.ts +61 -1
- package/dist/mcp-server/tools/definitions/value-analysis.tool.d.ts.map +1 -1
- package/dist/mcp-server/tools/definitions/value-analysis.tool.js +76 -10
- package/dist/mcp-server/tools/definitions/value-analysis.tool.js.map +1 -1
- package/dist/services/scorecard/input-normalization.d.ts +21 -0
- package/dist/services/scorecard/input-normalization.d.ts.map +1 -0
- package/dist/services/scorecard/input-normalization.js +92 -0
- package/dist/services/scorecard/input-normalization.js.map +1 -0
- package/dist/services/scorecard/scorecard-service.d.ts +5 -5
- package/dist/services/scorecard/scorecard-service.d.ts.map +1 -1
- package/dist/services/scorecard/scorecard-service.js +101 -43
- package/dist/services/scorecard/scorecard-service.js.map +1 -1
- package/dist/services/scorecard/types.d.ts +25 -12
- package/dist/services/scorecard/types.d.ts.map +1 -1
- package/package.json +11 -10
- package/server.json +16 -24
package/AGENTS.md
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
# Developer Protocol
|
|
2
2
|
|
|
3
3
|
**Server:** college-scorecard-mcp-server
|
|
4
|
-
**Version:** 0.
|
|
5
|
-
**Framework:** [@cyanheads/mcp-ts-core](https://www.npmjs.com/package/@cyanheads/mcp-ts-core) `^0.13.
|
|
4
|
+
**Version:** 0.3.0
|
|
5
|
+
**Framework:** [@cyanheads/mcp-ts-core](https://www.npmjs.com/package/@cyanheads/mcp-ts-core) `^0.13.15`
|
|
6
6
|
**Engines:** Bun ≥1.4.0, Node ≥24.0.0
|
|
7
|
-
**MCP SDK:** `@modelcontextprotocol/server` ^2.
|
|
7
|
+
**MCP SDK:** `@modelcontextprotocol/server` ^2.3.1
|
|
8
8
|
**Zod:** ^4.6.5
|
|
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.
|
|
@@ -33,7 +33,7 @@ Tailor suggestions to what's actually missing or stale — don't recite the full
|
|
|
33
33
|
## Core Rules
|
|
34
34
|
|
|
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
|
-
- **Use `ctx.log`** for request-scoped logging. No `console` calls.
|
|
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
38
|
- **Need input the caller didn't supply?** `return ctx.requestInput(...)` and read `ctx.inputs` when the handler is re-entered. Never `await` for user input mid-handler.
|
|
39
39
|
- **Secrets in env vars only** — never hardcoded.
|
|
@@ -54,7 +54,7 @@ export const searchItems = tool('search_items', {
|
|
|
54
54
|
annotations: { readOnlyHint: true },
|
|
55
55
|
input: z.object({
|
|
56
56
|
query: z.string().describe('Search terms'),
|
|
57
|
-
limit: z.number().default(10).describe('Max results'),
|
|
57
|
+
limit: z.number().int().min(1).max(100).default(10).describe('Max results (1–100)'),
|
|
58
58
|
}),
|
|
59
59
|
output: z.object({
|
|
60
60
|
items: z.array(z.object({
|
|
@@ -188,13 +188,14 @@ Handlers receive a unified `ctx` object. Key properties:
|
|
|
188
188
|
| Property | Description |
|
|
189
189
|
|:---------|:------------|
|
|
190
190
|
| `ctx.log` | Request-scoped logger — `.debug()`, `.info()`, `.notice()`, `.warning()`, `.error()`. Auto-correlates requestId, traceId, tenantId. Dual-sink: Pino **and** `notifications/message` to the client, so treat it as client-visible. |
|
|
191
|
-
| `ctx.state` | Tenant-scoped KV — `.get(key)`, `.set(key, value, { ttl? })`, `.delete(key)`, `.getMany(keys)`, `.list(prefix, { cursor, limit })`. Accepts any serializable value. |
|
|
191
|
+
| `ctx.state` | Tenant-scoped KV — `.get(key)`, `.set(key, value, { ttl? })`, `.delete(key)`, `.getMany(keys)`, `.list(prefix, { cursor, limit })`. Accepts any JSON-serializable value; reads return its JSON form (a `Date` comes back as an ISO string). |
|
|
192
192
|
| `ctx.requestInput` | Suspend and ask the caller for more input — `return ctx.requestInput({ inputRequests: { key: inputRequired.elicit({ message, requestedSchema }) } })`. Never returns; the handler is re-entered with the answers. Always present. |
|
|
193
|
-
| `ctx.inputs` |
|
|
193
|
+
| `ctx.inputs` | The request's responses — `.accepted(key, schema)`, `.view(key)`, `.state()`, `.dropped` — limited to what the client declared (`elicitation` and its form/url modes, `sampling`, `roots`). Client-supplied: a consent gate trusts only a `ctx.state` record it stored when it asked, bound to the operation, caller, and target (see the `api-context` skill). |
|
|
194
|
+
| `ctx.clientCapabilities` | What the client declared for this request, `undefined` when no view exists. Decides whether to ask for optional context (e.g. roots); never a reason to skip a consent prompt. |
|
|
194
195
|
| `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). |
|
|
195
196
|
| `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`. |
|
|
196
197
|
| `ctx.signal` | `AbortSignal` for cancellation. |
|
|
197
|
-
| `ctx.requestId` |
|
|
198
|
+
| `ctx.requestId` | Request ID — the one every log record of the call carries and its error envelope returns as `data.requestId`. |
|
|
198
199
|
| `ctx.tenantId` | Tenant ID from JWT; `'default'` for stdio or HTTP with auth off. |
|
|
199
200
|
|
|
200
201
|
---
|
|
@@ -203,7 +204,7 @@ Handlers receive a unified `ctx` object. Key properties:
|
|
|
203
204
|
|
|
204
205
|
Handlers throw — the framework catches, classifies, and formats.
|
|
205
206
|
|
|
206
|
-
**Recommended: typed error contract.** Declare `errors: [{ reason, code, when, recovery, retryable?, severity?, thrownBy? }]` on `tool()` / `resource()` to receive `ctx.fail(reason, …)` typed against the reason union. TypeScript catches typos at compile time, `data.reason` is auto-populated for observability, linter enforces conformance against the handler body. `recovery` is required (≥ 5 words, lint-validated) — the single source of truth for the agent's next move.
|
|
207
|
+
**Recommended: typed error contract.** Declare `errors: [{ reason, code, when, recovery, retryable?, severity?, thrownBy? }]` on `tool()` / `resource()` to receive `ctx.fail(reason, …)` typed against the reason union. TypeScript catches typos at compile time, `data.reason` is auto-populated for observability, linter enforces conformance against the handler body. `recovery` is required (≥ 5 words, lint-validated) — the single source of truth for the agent's next move. The framework puts it on the wire whenever a failure carrying that `reason` arrives without a hint — a bare `ctx.fail('reason')` or a service throw with `data: { reason }` — as `data.recovery.hint`, mirrored into `content[]` text unless the message already contains it verbatim; override with an explicit `{ recovery: { hint: '...' } }` when dynamic runtime context matters. Every error envelope also carries `data.requestId`, the id the server's log records for that call carry, and `content[]` closes with `(reason … · request <id>)`. Mark an entry the service layer throws with `thrownBy: 'service'` so `error-contract-unthrown` skips it — lint-only metadata, nothing at runtime reads it. Baseline codes (`InternalError`, `ServiceUnavailable`, `Timeout`, `ValidationError`, `SerializationError`, `RequestCancelled`) bubble freely and don't need declaring.
|
|
207
208
|
|
|
208
209
|
```ts
|
|
209
210
|
import { JsonRpcErrorCode } from '@cyanheads/mcp-ts-core/errors';
|
|
@@ -215,7 +216,7 @@ errors: [
|
|
|
215
216
|
],
|
|
216
217
|
async handler(input, ctx) {
|
|
217
218
|
const item = await db.find(input.id);
|
|
218
|
-
if (!item) throw ctx.fail('no_match', `No item ${input.id}
|
|
219
|
+
if (!item) throw ctx.fail('no_match', `No item ${input.id}`);
|
|
219
220
|
return item;
|
|
220
221
|
}
|
|
221
222
|
```
|
|
@@ -359,7 +360,7 @@ When you complete a skill's checklist, check the boxes and add a completion time
|
|
|
359
360
|
|
|
360
361
|
## Bundling
|
|
361
362
|
|
|
362
|
-
`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. 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.
|
|
363
|
+
`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.
|
|
363
364
|
|
|
364
365
|
**Adding an env var requires both files:** `server.json` (registry discovery, `environmentVariables[]`) and `manifest.json` (bundle install UX, `mcp_config.env` + `user_config`). `lint:packaging` (run by `devcheck`) verifies the env var names match, 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": ""`.
|
|
365
366
|
|
|
@@ -396,7 +397,7 @@ security: false # optional — true ONLY for a source
|
|
|
396
397
|
|
|
397
398
|
## Publishing
|
|
398
399
|
|
|
399
|
-
**Every release goes through a release PR, straight-through** — `git-wrapup`'s "Release PR mode", mode `straight-through`. One run: `git-wrapup` lands the commit stack on `release/<version>`, pushes it, and opens the PR (title = the release commit subject, body = the
|
|
400
|
+
**Every release goes through a release PR, straight-through** — `git-wrapup`'s "Release PR mode", mode `straight-through`. One run: `git-wrapup` lands the commit stack on `release/<version>`, pushes it, and opens the PR (title = the release commit subject, body = the release digest: theme line, `## Changes`, `## Gates`, changelog link last); `release-and-publish` then fast-forwards `main` locally with `git merge --ff-only`, creates the tag on `main`'s tip, pushes `main` and the tag, deletes the branch, and publishes. A caller's brief may run a given release as `gated` instead — a `release-pr-review` pass on the open PR before `release-and-publish`. **Never merge through the GitHub UI or `gh pr merge`**: squash and rebase-merge are disabled in the repo settings because both rewrite the stack (rebase-merge also strips the SSH signatures), and a merge commit breaks the linear history.
|
|
400
401
|
|
|
401
402
|
---
|
|
402
403
|
|
package/CLAUDE.md
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
# Developer Protocol
|
|
2
2
|
|
|
3
3
|
**Server:** college-scorecard-mcp-server
|
|
4
|
-
**Version:** 0.
|
|
5
|
-
**Framework:** [@cyanheads/mcp-ts-core](https://www.npmjs.com/package/@cyanheads/mcp-ts-core) `^0.13.
|
|
4
|
+
**Version:** 0.3.0
|
|
5
|
+
**Framework:** [@cyanheads/mcp-ts-core](https://www.npmjs.com/package/@cyanheads/mcp-ts-core) `^0.13.15`
|
|
6
6
|
**Engines:** Bun ≥1.4.0, Node ≥24.0.0
|
|
7
|
-
**MCP SDK:** `@modelcontextprotocol/server` ^2.
|
|
7
|
+
**MCP SDK:** `@modelcontextprotocol/server` ^2.3.1
|
|
8
8
|
**Zod:** ^4.6.5
|
|
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.
|
|
@@ -33,7 +33,7 @@ Tailor suggestions to what's actually missing or stale — don't recite the full
|
|
|
33
33
|
## Core Rules
|
|
34
34
|
|
|
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
|
-
- **Use `ctx.log`** for request-scoped logging. No `console` calls.
|
|
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
38
|
- **Need input the caller didn't supply?** `return ctx.requestInput(...)` and read `ctx.inputs` when the handler is re-entered. Never `await` for user input mid-handler.
|
|
39
39
|
- **Secrets in env vars only** — never hardcoded.
|
|
@@ -54,7 +54,7 @@ export const searchItems = tool('search_items', {
|
|
|
54
54
|
annotations: { readOnlyHint: true },
|
|
55
55
|
input: z.object({
|
|
56
56
|
query: z.string().describe('Search terms'),
|
|
57
|
-
limit: z.number().default(10).describe('Max results'),
|
|
57
|
+
limit: z.number().int().min(1).max(100).default(10).describe('Max results (1–100)'),
|
|
58
58
|
}),
|
|
59
59
|
output: z.object({
|
|
60
60
|
items: z.array(z.object({
|
|
@@ -188,13 +188,14 @@ Handlers receive a unified `ctx` object. Key properties:
|
|
|
188
188
|
| Property | Description |
|
|
189
189
|
|:---------|:------------|
|
|
190
190
|
| `ctx.log` | Request-scoped logger — `.debug()`, `.info()`, `.notice()`, `.warning()`, `.error()`. Auto-correlates requestId, traceId, tenantId. Dual-sink: Pino **and** `notifications/message` to the client, so treat it as client-visible. |
|
|
191
|
-
| `ctx.state` | Tenant-scoped KV — `.get(key)`, `.set(key, value, { ttl? })`, `.delete(key)`, `.getMany(keys)`, `.list(prefix, { cursor, limit })`. Accepts any serializable value. |
|
|
191
|
+
| `ctx.state` | Tenant-scoped KV — `.get(key)`, `.set(key, value, { ttl? })`, `.delete(key)`, `.getMany(keys)`, `.list(prefix, { cursor, limit })`. Accepts any JSON-serializable value; reads return its JSON form (a `Date` comes back as an ISO string). |
|
|
192
192
|
| `ctx.requestInput` | Suspend and ask the caller for more input — `return ctx.requestInput({ inputRequests: { key: inputRequired.elicit({ message, requestedSchema }) } })`. Never returns; the handler is re-entered with the answers. Always present. |
|
|
193
|
-
| `ctx.inputs` |
|
|
193
|
+
| `ctx.inputs` | The request's responses — `.accepted(key, schema)`, `.view(key)`, `.state()`, `.dropped` — limited to what the client declared (`elicitation` and its form/url modes, `sampling`, `roots`). Client-supplied: a consent gate trusts only a `ctx.state` record it stored when it asked, bound to the operation, caller, and target (see the `api-context` skill). |
|
|
194
|
+
| `ctx.clientCapabilities` | What the client declared for this request, `undefined` when no view exists. Decides whether to ask for optional context (e.g. roots); never a reason to skip a consent prompt. |
|
|
194
195
|
| `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). |
|
|
195
196
|
| `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`. |
|
|
196
197
|
| `ctx.signal` | `AbortSignal` for cancellation. |
|
|
197
|
-
| `ctx.requestId` |
|
|
198
|
+
| `ctx.requestId` | Request ID — the one every log record of the call carries and its error envelope returns as `data.requestId`. |
|
|
198
199
|
| `ctx.tenantId` | Tenant ID from JWT; `'default'` for stdio or HTTP with auth off. |
|
|
199
200
|
|
|
200
201
|
---
|
|
@@ -203,7 +204,7 @@ Handlers receive a unified `ctx` object. Key properties:
|
|
|
203
204
|
|
|
204
205
|
Handlers throw — the framework catches, classifies, and formats.
|
|
205
206
|
|
|
206
|
-
**Recommended: typed error contract.** Declare `errors: [{ reason, code, when, recovery, retryable?, severity?, thrownBy? }]` on `tool()` / `resource()` to receive `ctx.fail(reason, …)` typed against the reason union. TypeScript catches typos at compile time, `data.reason` is auto-populated for observability, linter enforces conformance against the handler body. `recovery` is required (≥ 5 words, lint-validated) — the single source of truth for the agent's next move.
|
|
207
|
+
**Recommended: typed error contract.** Declare `errors: [{ reason, code, when, recovery, retryable?, severity?, thrownBy? }]` on `tool()` / `resource()` to receive `ctx.fail(reason, …)` typed against the reason union. TypeScript catches typos at compile time, `data.reason` is auto-populated for observability, linter enforces conformance against the handler body. `recovery` is required (≥ 5 words, lint-validated) — the single source of truth for the agent's next move. The framework puts it on the wire whenever a failure carrying that `reason` arrives without a hint — a bare `ctx.fail('reason')` or a service throw with `data: { reason }` — as `data.recovery.hint`, mirrored into `content[]` text unless the message already contains it verbatim; override with an explicit `{ recovery: { hint: '...' } }` when dynamic runtime context matters. Every error envelope also carries `data.requestId`, the id the server's log records for that call carry, and `content[]` closes with `(reason … · request <id>)`. Mark an entry the service layer throws with `thrownBy: 'service'` so `error-contract-unthrown` skips it — lint-only metadata, nothing at runtime reads it. Baseline codes (`InternalError`, `ServiceUnavailable`, `Timeout`, `ValidationError`, `SerializationError`, `RequestCancelled`) bubble freely and don't need declaring.
|
|
207
208
|
|
|
208
209
|
```ts
|
|
209
210
|
import { JsonRpcErrorCode } from '@cyanheads/mcp-ts-core/errors';
|
|
@@ -215,7 +216,7 @@ errors: [
|
|
|
215
216
|
],
|
|
216
217
|
async handler(input, ctx) {
|
|
217
218
|
const item = await db.find(input.id);
|
|
218
|
-
if (!item) throw ctx.fail('no_match', `No item ${input.id}
|
|
219
|
+
if (!item) throw ctx.fail('no_match', `No item ${input.id}`);
|
|
219
220
|
return item;
|
|
220
221
|
}
|
|
221
222
|
```
|
|
@@ -359,7 +360,7 @@ When you complete a skill's checklist, check the boxes and add a completion time
|
|
|
359
360
|
|
|
360
361
|
## Bundling
|
|
361
362
|
|
|
362
|
-
`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. 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.
|
|
363
|
+
`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.
|
|
363
364
|
|
|
364
365
|
**Adding an env var requires both files:** `server.json` (registry discovery, `environmentVariables[]`) and `manifest.json` (bundle install UX, `mcp_config.env` + `user_config`). `lint:packaging` (run by `devcheck`) verifies the env var names match, 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": ""`.
|
|
365
366
|
|
|
@@ -396,7 +397,7 @@ security: false # optional — true ONLY for a source
|
|
|
396
397
|
|
|
397
398
|
## Publishing
|
|
398
399
|
|
|
399
|
-
**Every release goes through a release PR, straight-through** — `git-wrapup`'s "Release PR mode", mode `straight-through`. One run: `git-wrapup` lands the commit stack on `release/<version>`, pushes it, and opens the PR (title = the release commit subject, body = the
|
|
400
|
+
**Every release goes through a release PR, straight-through** — `git-wrapup`'s "Release PR mode", mode `straight-through`. One run: `git-wrapup` lands the commit stack on `release/<version>`, pushes it, and opens the PR (title = the release commit subject, body = the release digest: theme line, `## Changes`, `## Gates`, changelog link last); `release-and-publish` then fast-forwards `main` locally with `git merge --ff-only`, creates the tag on `main`'s tip, pushes `main` and the tag, deletes the branch, and publishes. A caller's brief may run a given release as `gated` instead — a `release-pr-review` pass on the open PR before `release-and-publish`. **Never merge through the GitHub UI or `gh pr merge`**: squash and rebase-merge are disabled in the repo settings because both rewrite the stack (rebase-merge also strips the SSH signatures), and a merge commit breaks the linear history.
|
|
400
401
|
|
|
401
402
|
---
|
|
402
403
|
|
package/Dockerfile
CHANGED
|
@@ -5,8 +5,8 @@
|
|
|
5
5
|
# source code into JavaScript, and prepares the production assets.
|
|
6
6
|
#
|
|
7
7
|
# Pinned to $BUILDPLATFORM rather than the target platform: `bun run build` emits
|
|
8
|
-
# JavaScript, and only `dist/` crosses into the production stage
|
|
9
|
-
#
|
|
8
|
+
# JavaScript, and only `dist/` crosses into the production stage.
|
|
9
|
+
# Built for the target instead, the non-native leg of a
|
|
10
10
|
# `--platform linux/amd64,linux/arm64` build runs under QEMU, where bun >= 1.4
|
|
11
11
|
# aborts with a JavaScriptCore allocator assertion and fails the multi-arch push.
|
|
12
12
|
#
|
|
@@ -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.
|
|
17
|
+
FROM --platform=$BUILDPLATFORM oven/bun:1.4.3 AS build
|
|
18
18
|
|
|
19
19
|
WORKDIR /usr/src/app
|
|
20
20
|
|
|
@@ -36,59 +36,77 @@ RUN bun run build
|
|
|
36
36
|
|
|
37
37
|
|
|
38
38
|
# ==============================================================================
|
|
39
|
-
# Production Stage
|
|
39
|
+
# Production Dependencies Stage
|
|
40
40
|
#
|
|
41
|
-
#
|
|
42
|
-
#
|
|
43
|
-
# dependencies and build artifacts.
|
|
41
|
+
# Run the scanner and OTel installer natively, cross-installing production
|
|
42
|
+
# dependencies for the target architecture. Only node_modules leaves this stage.
|
|
44
43
|
# ==============================================================================
|
|
45
|
-
FROM oven/bun:1.4.
|
|
44
|
+
FROM --platform=$BUILDPLATFORM oven/bun:1.4.3 AS deps
|
|
46
45
|
|
|
47
46
|
WORKDIR /usr/src/app
|
|
48
47
|
|
|
49
|
-
#
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
# OCI image metadata (https://github.com/opencontainers/image-spec/blob/main/annotations.md)
|
|
54
|
-
ARG APP_VERSION=unknown
|
|
55
|
-
LABEL org.opencontainers.image.title="college-scorecard-mcp-server"
|
|
56
|
-
LABEL org.opencontainers.image.description="Search, compare, and analyze U.S. college data — costs, earnings, programs, and outcomes — via MCP."
|
|
57
|
-
LABEL org.opencontainers.image.licenses="Apache-2.0"
|
|
58
|
-
LABEL org.opencontainers.image.source="https://github.com/cyanheads/college-scorecard-mcp-server"
|
|
59
|
-
LABEL org.opencontainers.image.version="${APP_VERSION}"
|
|
48
|
+
# Keep the release-age gate and security scanner on every production install.
|
|
49
|
+
COPY package.json bun.lock bunfig.toml ./
|
|
50
|
+
COPY --from=build /usr/src/app/node_modules/@socketsecurity/bun-security-scanner ./node_modules/@socketsecurity/bun-security-scanner
|
|
60
51
|
|
|
61
|
-
#
|
|
62
|
-
|
|
52
|
+
# Docker uses amd64; Bun uses x64. Both support arm64 unchanged.
|
|
53
|
+
ARG TARGETOS
|
|
54
|
+
ARG TARGETARCH
|
|
55
|
+
RUN case "$TARGETARCH" in \
|
|
56
|
+
amd64) echo x64 ;; \
|
|
57
|
+
arm64) echo arm64 ;; \
|
|
58
|
+
*) echo "Unsupported TARGETARCH '$TARGETARCH': expected amd64 or arm64" >&2; exit 1 ;; \
|
|
59
|
+
esac > .bun-cpu
|
|
63
60
|
|
|
64
61
|
# Install only production dependencies, ignoring any lifecycle scripts (like 'prepare')
|
|
65
62
|
# that are not needed in the final production image.
|
|
66
63
|
# `--omit=peer` drops the framework's optional peer tiers (test runner, service
|
|
67
64
|
# SDKs, parsers) that Bun would otherwise auto-install. Anything this server
|
|
68
65
|
# actually imports belongs in its own `dependencies`, so nothing needed at
|
|
69
|
-
# runtime is lost.
|
|
70
|
-
# install re-resolves the graph and pulls every optional peer back in.
|
|
66
|
+
# runtime is lost.
|
|
71
67
|
RUN --mount=type=cache,target=/root/.bun/install/cache \
|
|
72
|
-
bun install --production --omit=peer --frozen-lockfile --ignore-scripts
|
|
68
|
+
bun install --production --omit=peer --frozen-lockfile --ignore-scripts \
|
|
69
|
+
--os="$TARGETOS" --cpu="$(cat .bun-cpu)"
|
|
73
70
|
|
|
74
71
|
# Conditionally install OpenTelemetry optional peer dependencies (Tier 3).
|
|
75
72
|
# Installed by default. Omit them for a leaner image at build time
|
|
76
73
|
# with: docker build --build-arg OTEL_ENABLED=false
|
|
74
|
+
COPY scripts/install-otel.ts ./scripts/
|
|
77
75
|
ARG OTEL_ENABLED=true
|
|
78
76
|
RUN --mount=type=cache,target=/root/.bun/install/cache \
|
|
79
77
|
if [ "$OTEL_ENABLED" = "true" ]; then \
|
|
80
|
-
bun
|
|
81
|
-
@opentelemetry/instrumentation-http \
|
|
82
|
-
@opentelemetry/exporter-metrics-otlp-http \
|
|
83
|
-
@opentelemetry/exporter-trace-otlp-http \
|
|
84
|
-
@opentelemetry/instrumentation-pino \
|
|
85
|
-
@opentelemetry/resources \
|
|
86
|
-
@opentelemetry/sdk-metrics \
|
|
87
|
-
@opentelemetry/sdk-node \
|
|
88
|
-
@opentelemetry/sdk-trace-node \
|
|
89
|
-
@opentelemetry/semantic-conventions; \
|
|
78
|
+
bun scripts/install-otel.ts --os="$TARGETOS" --cpu="$(cat .bun-cpu)"; \
|
|
90
79
|
fi
|
|
91
80
|
|
|
81
|
+
COPY scripts/prune-musl-packages.ts ./scripts/
|
|
82
|
+
RUN bun scripts/prune-musl-packages.ts
|
|
83
|
+
|
|
84
|
+
# The scanner is only needed during installation.
|
|
85
|
+
RUN rm -rf node_modules/@socketsecurity/bun-security-scanner
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
# ==============================================================================
|
|
89
|
+
# Production Stage
|
|
90
|
+
#
|
|
91
|
+
# Runtime-only stage: Bun runs on the target architecture at container start.
|
|
92
|
+
# ==============================================================================
|
|
93
|
+
FROM oven/bun:1.4.3-slim AS production
|
|
94
|
+
|
|
95
|
+
WORKDIR /usr/src/app
|
|
96
|
+
ENV NODE_ENV=production
|
|
97
|
+
|
|
98
|
+
# OCI image metadata (https://github.com/opencontainers/image-spec/blob/main/annotations.md)
|
|
99
|
+
ARG APP_VERSION=unknown
|
|
100
|
+
LABEL org.opencontainers.image.title="college-scorecard-mcp-server"
|
|
101
|
+
LABEL org.opencontainers.image.description="Search, compare, and analyze U.S. college data — costs, earnings, programs, and outcomes — via MCP."
|
|
102
|
+
LABEL org.opencontainers.image.licenses="Apache-2.0"
|
|
103
|
+
LABEL org.opencontainers.image.source="https://github.com/cyanheads/college-scorecard-mcp-server"
|
|
104
|
+
LABEL org.opencontainers.image.version="${APP_VERSION}"
|
|
105
|
+
|
|
106
|
+
# Keep the original manifest; the OTel installer rewrites the deps-stage copy.
|
|
107
|
+
COPY package.json ./
|
|
108
|
+
COPY --from=deps /usr/src/app/node_modules ./node_modules
|
|
109
|
+
|
|
92
110
|
# Copy the compiled application code from the build stage
|
|
93
111
|
COPY --from=build /usr/src/app/dist ./dist
|
|
94
112
|
|
|
@@ -113,7 +131,6 @@ ENV MCP_TRANSPORT_TYPE="http"
|
|
|
113
131
|
ENV MCP_SESSION_MODE="stateless"
|
|
114
132
|
ENV MCP_LOG_LEVEL="info"
|
|
115
133
|
ENV LOGS_DIR="/var/log/college-scorecard-mcp-server"
|
|
116
|
-
ENV MCP_FORCE_CONSOLE_LOGGING="true"
|
|
117
134
|
|
|
118
135
|
# Expose the port the server listens on
|
|
119
136
|
EXPOSE ${MCP_HTTP_PORT}
|
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/college-scorecard-mcp-server) [](https://modelcontextprotocol.io/) [](https://www.npmjs.com/package/@cyanheads/college-scorecard-mcp-server) [](https://www.typescriptlang.org/) [](https://bun.sh/)
|
|
11
11
|
|
|
12
12
|
</div>
|
|
13
13
|
|
|
@@ -19,24 +19,30 @@
|
|
|
19
19
|
|
|
20
20
|
</div>
|
|
21
21
|
|
|
22
|
+
<div align="center">
|
|
23
|
+
|
|
24
|
+
**Public Hosted Server:** [https://college-scorecard.caseyjhand.com/mcp](https://college-scorecard.caseyjhand.com/mcp)
|
|
25
|
+
|
|
26
|
+
</div>
|
|
27
|
+
|
|
22
28
|
---
|
|
23
29
|
|
|
24
30
|
## Overview
|
|
25
31
|
|
|
26
|
-
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
|
|
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.
|
|
27
33
|
|
|
28
34
|
### Tools
|
|
29
35
|
|
|
30
36
|
| Tool | Description |
|
|
31
37
|
|:---|:---|
|
|
32
|
-
| `scorecard_search_schools` | Search
|
|
33
|
-
| `scorecard_get_school` | Full institutional
|
|
34
|
-
| `scorecard_compare_schools` | Normalized side-by-side comparison of 2–5 schools on a named topic. Returns
|
|
35
|
-
| `scorecard_get_programs` |
|
|
36
|
-
| `scorecard_search_programs` | Find programs by CIP code or keyword
|
|
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. |
|
|
42
|
+
| `scorecard_search_programs` | Find programs by CIP code or keyword, with school, earnings, and debt filters; rank by earnings within each fetched school page. |
|
|
37
43
|
| `scorecard_get_earnings` | Institution-level post-graduation earnings for one school — median at 6, 8, and 10 years after entry (P25/P75 at 6 and 10 years), with optional gender breakdown. |
|
|
38
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. |
|
|
39
|
-
| `scorecard_lookup_cip` | Search
|
|
45
|
+
| `scorecard_lookup_cip` | Search 202 curated Classification of Instructional Programs (CIP) codes by keyword or partial name. Served from embedded static data — no API call or rate-limit impact. |
|
|
40
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. |
|
|
41
47
|
|
|
42
48
|
### Resources
|
|
@@ -58,89 +64,70 @@ All resource data is also reachable via tools. Use `scorecard_search_schools` or
|
|
|
58
64
|
|
|
59
65
|
### `scorecard_search_schools` <sub>tool</sub>
|
|
60
66
|
|
|
61
|
-
-
|
|
62
|
-
-
|
|
63
|
-
-
|
|
64
|
-
- Pagination (`per_page` up to 100, zero-indexed `page`)
|
|
65
|
-
- Returns core identity and cost metrics for quick scanning
|
|
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.
|
|
66
70
|
|
|
67
71
|
---
|
|
68
72
|
|
|
69
73
|
### `scorecard_get_school` <sub>tool</sub>
|
|
70
74
|
|
|
71
|
-
- Accepts a
|
|
72
|
-
-
|
|
73
|
-
-
|
|
74
|
-
- For side-by-side comparison on a specific dimension, use `scorecard_compare_schools`
|
|
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.
|
|
75
78
|
|
|
76
79
|
---
|
|
77
80
|
|
|
78
81
|
### `scorecard_compare_schools` <sub>tool</sub>
|
|
79
82
|
|
|
80
|
-
- 2–5 school unit IDs
|
|
81
|
-
-
|
|
82
|
-
- Single API call for all schools; normalization applied post-fetch
|
|
83
|
-
- Distinct from `scorecard_get_school` with multiple IDs: output shape is rows, not profiles
|
|
83
|
+
- Accepts 2–5 school unit IDs and one topic: `costs`, `admissions`, `outcomes`, or `aid`.
|
|
84
|
+
- Returns comparison rows with metric values, within-set ordinal ranks, and a comparison summary from a single API call.
|
|
84
85
|
|
|
85
86
|
---
|
|
86
87
|
|
|
87
88
|
### `scorecard_get_programs` <sub>tool</sub>
|
|
88
89
|
|
|
89
|
-
-
|
|
90
|
-
-
|
|
91
|
-
-
|
|
92
|
-
-
|
|
93
|
-
- FERPA suppression surfaced as a structured `suppressed: true` flag with `suppression_note`, not bare null
|
|
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, with `suppressed` and `suppression_note` for unavailable earnings.
|
|
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.
|
|
94
94
|
|
|
95
95
|
---
|
|
96
96
|
|
|
97
97
|
### `scorecard_search_programs` <sub>tool</sub>
|
|
98
98
|
|
|
99
|
-
-
|
|
100
|
-
-
|
|
101
|
-
-
|
|
102
|
-
- Returns school ID (unit ID) and name alongside program metrics for chaining to `scorecard_get_school` or `scorecard_get_programs`
|
|
103
|
-
- Sorting applied post-fetch since earnings fields aren't API-indexed
|
|
99
|
+
- Search by CIP code or program name, with 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`.
|
|
100
|
+
- Returns school IDs and names alongside program metrics, sorted by earnings within the fetched page.
|
|
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`.
|
|
104
102
|
|
|
105
103
|
---
|
|
106
104
|
|
|
107
105
|
### `scorecard_get_earnings` <sub>tool</sub>
|
|
108
106
|
|
|
109
|
-
-
|
|
110
|
-
-
|
|
111
|
-
-
|
|
112
|
-
- Institution-wide across all graduates — for program-specific earnings use `scorecard_get_programs`, for ROI analysis use `scorecard_value_analysis`
|
|
113
|
-
- Top-level `suppressed` / `suppression_note` flags when earnings are unavailable at every time point
|
|
107
|
+
- Accepts one school ID and optional `years` for cohort trends and a gender-breakdown option.
|
|
108
|
+
- Returns median earnings at 6, 8, and 10 years after entry, P25/P75 at 6 and 10 years, and optional 6-year female/male medians; `suppressed` and `suppression_note` flag earnings unavailable at every time point.
|
|
109
|
+
- Each requested trend year adds its 6-year and 10-year median alongside the current snapshot.
|
|
114
110
|
|
|
115
111
|
---
|
|
116
112
|
|
|
117
113
|
### `scorecard_value_analysis` <sub>tool</sub>
|
|
118
114
|
|
|
119
|
-
-
|
|
120
|
-
-
|
|
121
|
-
- `family_income` parameter selects the applicable net price bracket ($0–30k, $30k–48k, $48k–75k, $75k–110k, $110k+)
|
|
122
|
-
- Returns all source figures alongside derived metrics — callers can audit the arithmetic
|
|
123
|
-
- `data_notes` array flags any suppressed or missing fields with plain-language explanations
|
|
115
|
+
- Accepts one school ID; optional `family_income` selects the applicable net price bracket ($0–30k, $30k–48k, $48k–75k, $75k–110k, or $110k+).
|
|
116
|
+
- Returns debt-to-earnings and net-price-to-earnings ratios alongside the source figures; `data_notes` explains suppressed or missing fields.
|
|
124
117
|
|
|
125
118
|
---
|
|
126
119
|
|
|
127
120
|
### `scorecard_lookup_cip` <sub>tool</sub>
|
|
128
121
|
|
|
129
|
-
-
|
|
130
|
-
-
|
|
131
|
-
- Required before using CIP-based filters when the caller knows a program by name but not code
|
|
132
|
-
- Up to 50 results per call (`limit`, default 20)
|
|
133
|
-
- Returns matching codes with standard titles and CIP family
|
|
122
|
+
- 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.
|
|
123
|
+
- Returns codes, standard titles, and CIP families from a curated offline set of 202 common 4-digit codes, rather than the full NCES taxonomy.
|
|
134
124
|
|
|
135
125
|
---
|
|
136
126
|
|
|
137
127
|
### `scorecard_list_fields` <sub>tool</sub>
|
|
138
128
|
|
|
139
|
-
-
|
|
140
|
-
-
|
|
141
|
-
- Returns field path, description, data type, category, and whether it supports API-side sorting
|
|
142
|
-
- Up to 100 results per call (`limit`, default 30)
|
|
143
|
-
- Use before passing custom `fields` parameters to `scorecard_search_schools` or `scorecard_get_school`; a `tip` field flags when results include unsortable fields
|
|
129
|
+
- Search 80 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`.
|
|
130
|
+
- Returns field paths, descriptions, data types, categories, and API sorting support; `tip` flags results containing unsortable fields.
|
|
144
131
|
|
|
145
132
|
---
|
|
146
133
|
|
|
@@ -154,7 +141,7 @@ All resource data is also reachable via tools. Use `scorecard_search_schools` or
|
|
|
154
141
|
|
|
155
142
|
### `scorecard://programs/{id}` <sub>resource</sub>
|
|
156
143
|
|
|
157
|
-
- Program-level outcomes as `application/json` — CIP code, title, credential level, 1-year earnings, debt, and
|
|
144
|
+
- 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
|
|
158
145
|
- `id` is the school unit ID from `scorecard_search_schools`
|
|
159
146
|
- `list` returns a handful of example school URIs; use `scorecard_search_schools` to discover others
|
|
160
147
|
|
|
@@ -172,20 +159,39 @@ Built on [`@cyanheads/mcp-ts-core`](https://github.com/cyanheads/mcp-ts-core): s
|
|
|
172
159
|
College Scorecard-specific:
|
|
173
160
|
|
|
174
161
|
- 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
|
|
175
|
-
- Program-level
|
|
162
|
+
- Program-level earnings: median earnings of graduates working and not enrolled 1 year after their highest credential, per school × CIP code × credential level
|
|
176
163
|
- Field pre-selection per tool — curated field sets appropriate to each tool's purpose; optional `fields` override for custom queries
|
|
177
|
-
- Embedded CIP code taxonomy (
|
|
164
|
+
- Embedded CIP code taxonomy (202 codes) and field catalog (80 fields) served as static data — zero API calls, zero rate-limit impact
|
|
178
165
|
- Geographic filtering via U.S. zip code + distance radius
|
|
179
166
|
|
|
180
167
|
Agent-friendly output:
|
|
181
168
|
|
|
182
169
|
- FERPA suppression surfaced as structured `suppressed: true` flag with `suppression_note` — prevents hallucination of missing earnings data at selective schools with small cohorts
|
|
183
170
|
- Derived metrics alongside source figures in `scorecard_value_analysis` — agents can verify arithmetic and branch on computed values, not raw numbers
|
|
184
|
-
-
|
|
185
|
-
-
|
|
171
|
+
- Ordinal ranks in `scorecard_compare_schools` place each school's metric values within the comparison set
|
|
172
|
+
- 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.
|
|
173
|
+
- 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.
|
|
174
|
+
- 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`.
|
|
186
175
|
|
|
187
176
|
## Getting started
|
|
188
177
|
|
|
178
|
+
### Public Hosted Instance
|
|
179
|
+
|
|
180
|
+
A public instance is available at `https://college-scorecard.caseyjhand.com/mcp` — no installation required. No API key needed; the instance supplies its own. Point any MCP client at it via Streamable HTTP:
|
|
181
|
+
|
|
182
|
+
```json
|
|
183
|
+
{
|
|
184
|
+
"mcpServers": {
|
|
185
|
+
"college-scorecard-mcp-server": {
|
|
186
|
+
"type": "streamable-http",
|
|
187
|
+
"url": "https://college-scorecard.caseyjhand.com/mcp"
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
### Self-Hosted / Local
|
|
194
|
+
|
|
189
195
|
Add the following to your MCP client configuration file. See [api.data.gov/signup](https://api.data.gov/signup/) for a free API key.
|
|
190
196
|
|
|
191
197
|
```json
|