@cyanheads/college-scorecard-mcp-server 0.1.9 → 0.2.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 (37) hide show
  1. package/AGENTS.md +10 -9
  2. package/CLAUDE.md +10 -9
  3. package/Dockerfile +49 -35
  4. package/README.md +32 -53
  5. package/changelog/0.2.x/0.2.0.md +47 -0
  6. package/dist/data/field-catalog.d.ts.map +1 -1
  7. package/dist/data/field-catalog.js +55 -48
  8. package/dist/data/field-catalog.js.map +1 -1
  9. package/dist/mcp-server/resources/definitions/programs.resource.d.ts.map +1 -1
  10. package/dist/mcp-server/resources/definitions/programs.resource.js +4 -3
  11. package/dist/mcp-server/resources/definitions/programs.resource.js.map +1 -1
  12. package/dist/mcp-server/tools/definitions/get-programs.tool.d.ts +3 -2
  13. package/dist/mcp-server/tools/definitions/get-programs.tool.d.ts.map +1 -1
  14. package/dist/mcp-server/tools/definitions/get-programs.tool.js +34 -15
  15. package/dist/mcp-server/tools/definitions/get-programs.tool.js.map +1 -1
  16. package/dist/mcp-server/tools/definitions/get-school.tool.d.ts +2 -0
  17. package/dist/mcp-server/tools/definitions/get-school.tool.d.ts.map +1 -1
  18. package/dist/mcp-server/tools/definitions/get-school.tool.js +14 -0
  19. package/dist/mcp-server/tools/definitions/get-school.tool.js.map +1 -1
  20. package/dist/mcp-server/tools/definitions/search-programs.tool.d.ts +2 -1
  21. package/dist/mcp-server/tools/definitions/search-programs.tool.d.ts.map +1 -1
  22. package/dist/mcp-server/tools/definitions/search-programs.tool.js +60 -33
  23. package/dist/mcp-server/tools/definitions/search-programs.tool.js.map +1 -1
  24. package/dist/mcp-server/tools/definitions/search-schools.tool.d.ts +3 -0
  25. package/dist/mcp-server/tools/definitions/search-schools.tool.d.ts.map +1 -1
  26. package/dist/mcp-server/tools/definitions/search-schools.tool.js +22 -9
  27. package/dist/mcp-server/tools/definitions/search-schools.tool.js.map +1 -1
  28. package/dist/mcp-server/tools/definitions/value-analysis.tool.js +3 -3
  29. package/dist/mcp-server/tools/definitions/value-analysis.tool.js.map +1 -1
  30. package/dist/services/scorecard/scorecard-service.d.ts +0 -1
  31. package/dist/services/scorecard/scorecard-service.d.ts.map +1 -1
  32. package/dist/services/scorecard/scorecard-service.js +8 -7
  33. package/dist/services/scorecard/scorecard-service.js.map +1 -1
  34. package/dist/services/scorecard/types.d.ts +16 -3
  35. package/dist/services/scorecard/types.d.ts.map +1 -1
  36. package/package.json +8 -7
  37. package/server.json +3 -3
package/AGENTS.md CHANGED
@@ -1,10 +1,10 @@
1
1
  # Developer Protocol
2
2
 
3
3
  **Server:** college-scorecard-mcp-server
4
- **Version:** 0.1.9
5
- **Framework:** [@cyanheads/mcp-ts-core](https://www.npmjs.com/package/@cyanheads/mcp-ts-core) `^0.13.6`
4
+ **Version:** 0.2.0
5
+ **Framework:** [@cyanheads/mcp-ts-core](https://www.npmjs.com/package/@cyanheads/mcp-ts-core) `^0.13.10`
6
6
  **Engines:** Bun ≥1.4.0, Node ≥24.0.0
7
- **MCP SDK:** `@modelcontextprotocol/server` ^2.0.0
7
+ **MCP SDK:** `@modelcontextprotocol/server` ^2.1.0
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.
@@ -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` | Reader over a retried request's responses — `.accepted(key, schema)`, `.view(key)`, `.state()`, `.dropped`. Empty on the first round. |
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` | Unique request ID. |
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. Pass `ctx.recoveryFor('reason')` as the throw's data to put it on the wire (`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. Forwarding it is lint-enforced per throw site (`error-contract-recovery-unforwarded`). 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
+ **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}`, ctx.recoveryFor('no_match'));
219
+ if (!item) throw ctx.fail('no_match', `No item ${input.id}`);
219
220
  return item;
220
221
  }
221
222
  ```
package/CLAUDE.md CHANGED
@@ -1,10 +1,10 @@
1
1
  # Developer Protocol
2
2
 
3
3
  **Server:** college-scorecard-mcp-server
4
- **Version:** 0.1.9
5
- **Framework:** [@cyanheads/mcp-ts-core](https://www.npmjs.com/package/@cyanheads/mcp-ts-core) `^0.13.6`
4
+ **Version:** 0.2.0
5
+ **Framework:** [@cyanheads/mcp-ts-core](https://www.npmjs.com/package/@cyanheads/mcp-ts-core) `^0.13.10`
6
6
  **Engines:** Bun ≥1.4.0, Node ≥24.0.0
7
- **MCP SDK:** `@modelcontextprotocol/server` ^2.0.0
7
+ **MCP SDK:** `@modelcontextprotocol/server` ^2.1.0
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.
@@ -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` | Reader over a retried request's responses — `.accepted(key, schema)`, `.view(key)`, `.state()`, `.dropped`. Empty on the first round. |
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` | Unique request ID. |
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. Pass `ctx.recoveryFor('reason')` as the throw's data to put it on the wire (`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. Forwarding it is lint-enforced per throw site (`error-contract-recovery-unforwarded`). 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
+ **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}`, ctx.recoveryFor('no_match'));
219
+ if (!item) throw ctx.fail('no_match', `No item ${input.id}`);
219
220
  return item;
220
221
  }
221
222
  ```
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, which runs its
9
- # own target-arch install. Built for the target instead, the non-native leg of a
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.0 AS build
17
+ FROM --platform=$BUILDPLATFORM oven/bun:1.4.2 AS build
18
18
 
19
19
  WORKDIR /usr/src/app
20
20
 
@@ -36,59 +36,74 @@ RUN bun run build
36
36
 
37
37
 
38
38
  # ==============================================================================
39
- # Production Stage
39
+ # Production Dependencies Stage
40
40
  #
41
- # This stage creates a minimal, optimized, and secure image for running the
42
- # application. It uses a slim base image and only includes production
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.0-slim AS production
44
+ FROM --platform=$BUILDPLATFORM oven/bun:1.4.2 AS deps
46
45
 
47
46
  WORKDIR /usr/src/app
48
47
 
49
- # Set the environment to production for performance and to ensure only
50
- # production dependencies are installed.
51
- ENV NODE_ENV=production
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
52
51
 
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}"
60
-
61
- # Copy dependency manifests
62
- COPY package.json bun.lock ./
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. The OTEL step below carries the same flag — without it, that
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 add --omit=dev --omit=peer --ignore-scripts @hono/otel \
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
+ # The scanner is only needed during installation.
82
+ RUN rm -rf node_modules/@socketsecurity/bun-security-scanner
83
+
84
+
85
+ # ==============================================================================
86
+ # Production Stage
87
+ #
88
+ # Runtime-only stage: Bun runs on the target architecture at container start.
89
+ # ==============================================================================
90
+ FROM oven/bun:1.4.2-slim AS production
91
+
92
+ WORKDIR /usr/src/app
93
+ ENV NODE_ENV=production
94
+
95
+ # OCI image metadata (https://github.com/opencontainers/image-spec/blob/main/annotations.md)
96
+ ARG APP_VERSION=unknown
97
+ LABEL org.opencontainers.image.title="college-scorecard-mcp-server"
98
+ LABEL org.opencontainers.image.description="Search, compare, and analyze U.S. college data — costs, earnings, programs, and outcomes — via MCP."
99
+ LABEL org.opencontainers.image.licenses="Apache-2.0"
100
+ LABEL org.opencontainers.image.source="https://github.com/cyanheads/college-scorecard-mcp-server"
101
+ LABEL org.opencontainers.image.version="${APP_VERSION}"
102
+
103
+ # Keep the original manifest; the OTel installer rewrites the deps-stage copy.
104
+ COPY package.json ./
105
+ COPY --from=deps /usr/src/app/node_modules ./node_modules
106
+
92
107
  # Copy the compiled application code from the build stage
93
108
  COPY --from=build /usr/src/app/dist ./dist
94
109
 
@@ -113,7 +128,6 @@ ENV MCP_TRANSPORT_TYPE="http"
113
128
  ENV MCP_SESSION_MODE="stateless"
114
129
  ENV MCP_LOG_LEVEL="info"
115
130
  ENV LOGS_DIR="/var/log/college-scorecard-mcp-server"
116
- ENV MCP_FORCE_CONSOLE_LOGGING="true"
117
131
 
118
132
  # Expose the port the server listens on
119
133
  EXPOSE ${MCP_HTTP_PORT}
package/README.md CHANGED
@@ -7,7 +7,7 @@
7
7
 
8
8
  <div align="center">
9
9
 
10
- [![Version](https://img.shields.io/badge/Version-0.1.9-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.0.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.0-blueviolet.svg?style=flat-square)](https://bun.sh/)
10
+ [![Version](https://img.shields.io/badge/Version-0.2.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.1.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.2-blueviolet.svg?style=flat-square)](https://bun.sh/)
11
11
 
12
12
  </div>
13
13
 
@@ -29,14 +29,14 @@ U.S. college data from the Department of Education College Scorecard API — cos
29
29
 
30
30
  | Tool | Description |
31
31
  |:---|:---|
32
- | `scorecard_search_schools` | Search and filter institutions by name, location, type, size, and acceptance rate range. Returns core identity and cost metrics. |
33
- | `scorecard_get_school` | Full institutional profile for one or more school IDs — costs, admissions, outcomes, aid, demographics, and completion rates. |
32
+ | `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. |
33
+ | `scorecard_get_school` | Full institutional profiles — costs, admissions, outcomes, aid, demographics, single-sex flags, and completion rates. |
34
34
  | `scorecard_compare_schools` | Normalized side-by-side comparison of 2–5 schools on a named topic. Returns percentile-ranked rows and relative deltas within the result set. |
35
- | `scorecard_get_programs` | All field-of-study programs at one school: median 1-year post-graduation earnings, debt at graduation, and enrollment figures. |
36
- | `scorecard_search_programs` | Find programs by CIP code or keyword across all institutions, ranked by median earnings. Accepts school-side filters (state, ownership, max cost). |
35
+ | `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. |
36
+ | `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
37
  | `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
38
  | `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 a curated ~160-code Classification of Instructional Programs (CIP) taxonomy by keyword or partial name. Served from embedded static data — no API call or rate-limit impact. |
39
+ | `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
40
  | `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
41
 
42
42
  ### Resources
@@ -58,89 +58,68 @@ All resource data is also reachable via tools. Use `scorecard_search_schools` or
58
58
 
59
59
  ### `scorecard_search_schools` <sub>tool</sub>
60
60
 
61
- - Free-text name search plus typed filters: state, ownership (public/private nonprofit/private for-profit), degree level, size range, acceptance rate range
62
- - Geographic proximity filtering by U.S. zip code and distance (miles or km)
63
- - CIP code filter to find schools offering a specific program family
64
- - Pagination (`per_page` up to 100, zero-indexed `page`)
65
- - Returns core identity and cost metrics for quick scanning
61
+ - Search by name, state, ownership, degree level, size, acceptance rate, CIP code, or zip code and distance; `per_page` is capped at 100 with zero-indexed `page`.
62
+ - Returns core identity and cost metrics for quick scanning.
63
+ - `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
64
 
67
65
  ---
68
66
 
69
67
  ### `scorecard_get_school` <sub>tool</sub>
70
68
 
71
- - Accepts a single ID or an array of IDs (batch fetch up to 100 per call)
72
- - Covers costs, admissions, outcomes, financial aid, demographics, and completion rates
73
- - Optional `fields` override for callers who need a narrower or broader field set
74
- - For side-by-side comparison on a specific dimension, use `scorecard_compare_schools`
69
+ - Accepts a single school ID or an array of up to 100 IDs per call.
70
+ - Returns institutional profiles covering costs, admissions, outcomes, financial aid, demographics, and completion rates; `fields` overrides the default selection. `men_only` and `women_only` preserve true/false when known and are absent when unknown.
75
71
 
76
72
  ---
77
73
 
78
74
  ### `scorecard_compare_schools` <sub>tool</sub>
79
75
 
80
- - 2–5 school unit IDs per call; four topics — `costs`, `admissions`, `outcomes`, `aid` — each pulls a curated topic-specific field set
81
- - Computes within-set percentile ranks and relative deltas — structured output an agent cannot reconstruct from raw profiles
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
76
+ - Accepts 2–5 school unit IDs and one topic: `costs`, `admissions`, `outcomes`, or `aid`.
77
+ - Returns comparison rows with within-set percentile ranks and relative deltas from a single API call.
84
78
 
85
79
  ---
86
80
 
87
81
  ### `scorecard_get_programs` <sub>tool</sub>
88
82
 
89
- - Returns median 1-year post-graduation earnings, median debt at graduation, and enrollment figures per program
90
- - Filter by CIP code to return only matching programs
91
- - Filter by `credential_level` (certificate through doctoral/professional) and minimum earnings threshold
92
- - Primary source for program-level earnings — institution-level earnings at 6/8/10 years are available via `scorecard_get_earnings`
93
- - FERPA suppression surfaced as a structured `suppressed: true` flag with `suppression_note`, not bare null
83
+ - Accepts one school ID, with optional CIP code, `credential_level`, and minimum earnings filters.
84
+ - 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.
85
+ - `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
86
 
95
87
  ---
96
88
 
97
89
  ### `scorecard_search_programs` <sub>tool</sub>
98
90
 
99
- - Program-centric: "which schools in Washington have CS programs with median earnings over $80k?"
100
- - School-side filters: state, ownership, max net price; program-side filters: min earnings, max debt
101
- - Pagination (`per_page` up to 100, zero-indexed `page`) is at the school level — a school with multiple matching programs can return more rows than `per_page`
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
91
+ - 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`.
92
+ - Returns school IDs and names alongside program metrics, sorted by earnings within the fetched page.
93
+ - `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
94
 
105
95
  ---
106
96
 
107
97
  ### `scorecard_get_earnings` <sub>tool</sub>
108
98
 
109
- - Median earnings at 6, 8, and 10 years after entry, with P25/P75 percentiles at 6 and 10 years
110
- - Optional 6-year gender breakdown (`earnings_6yr_female_median` / `earnings_6yr_male_median`) when reported
111
- - Optional `years` array of cohort entry years returns a per-year trend row (6yr and 10yr median) alongside the current snapshot
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
99
+ - Accepts one school ID and optional `years` for cohort trends and a gender-breakdown option.
100
+ - 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.
101
+ - Each requested trend year adds its 6-year and 10-year median alongside the current snapshot.
114
102
 
115
103
  ---
116
104
 
117
105
  ### `scorecard_value_analysis` <sub>tool</sub>
118
106
 
119
- - Parallel-fetches cost/debt/repayment and earnings data in two concurrent requests
120
- - Computes ROI metrics the API doesn't pre-calculate: debt-to-earnings ratio (median debt / 6-year earnings) and net price to annualized 6-year earnings ratio
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
107
+ - Accepts one school ID; optional `family_income` selects the applicable net price bracket ($0–30k, $30k–48k, $48k–75k, $75k–110k, or $110k+).
108
+ - Returns debt-to-earnings and net-price-to-earnings ratios alongside the source figures; `data_notes` explains suppressed or missing fields.
124
109
 
125
110
  ---
126
111
 
127
112
  ### `scorecard_lookup_cip` <sub>tool</sub>
128
113
 
129
- - Curated set of ~160 common 4-digit CIP codes embedded as static data — not the full NCES taxonomy
130
- - No API call required — zero rate-limit impact, works offline
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
114
+ - Search by keyword or partial name with `limit` up to 50 (default 20); use before CIP filters when the code is unknown.
115
+ - 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
116
 
135
117
  ---
136
118
 
137
119
  ### `scorecard_list_fields` <sub>tool</sub>
138
120
 
139
- - ~79 field entries curated from the Scorecard data dictionary, embedded as static data
140
- - No API call required — zero rate-limit impact
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
121
+ - Search 80 curated offline field entries with `limit` up to 100 (default 30); use before passing custom `fields` to `scorecard_get_school`.
122
+ - Returns field paths, descriptions, data types, categories, and API sorting support; `tip` flags results containing unsortable fields.
144
123
 
145
124
  ---
146
125
 
@@ -154,7 +133,7 @@ All resource data is also reachable via tools. Use `scorecard_search_schools` or
154
133
 
155
134
  ### `scorecard://programs/{id}` <sub>resource</sub>
156
135
 
157
- - Program-level outcomes as `application/json` — CIP code, title, credential level, 1-year earnings, debt, and enrollment per program
136
+ - 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
137
  - `id` is the school unit ID from `scorecard_search_schools`
159
138
  - `list` returns a handful of example school URIs; use `scorecard_search_schools` to discover others
160
139
 
@@ -172,9 +151,9 @@ Built on [`@cyanheads/mcp-ts-core`](https://github.com/cyanheads/mcp-ts-core): s
172
151
  College Scorecard-specific:
173
152
 
174
153
  - 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 post-graduation earnings: median earnings 1 year after graduation per school × CIP code combination
154
+ - Program-level earnings: median earnings of graduates working and not enrolled 1 year after their highest credential, per school × CIP code × credential level
176
155
  - 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 (~160 codes) and field catalog (~79 fields) served as static data — zero API calls, zero rate-limit impact
156
+ - Embedded CIP code taxonomy (202 codes) and field catalog (80 fields) served as static data — zero API calls, zero rate-limit impact
178
157
  - Geographic filtering via U.S. zip code + distance radius
179
158
 
180
159
  Agent-friendly output:
@@ -182,7 +161,7 @@ Agent-friendly output:
182
161
  - FERPA suppression surfaced as structured `suppressed: true` flag with `suppression_note` — prevents hallucination of missing earnings data at selective schools with small cohorts
183
162
  - Derived metrics alongside source figures in `scorecard_value_analysis` — agents can verify arithmetic and branch on computed values, not raw numbers
184
163
  - Percentile ranks and relative deltas in `scorecard_compare_schools` — structured output an agent cannot reconstruct from raw profiles without knowing the full comparison set
185
- - Post-fetch sorting documented and handled transparently — callers never hit API errors on non-indexed sort fields
164
+ - 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.
186
165
 
187
166
  ## Getting started
188
167
 
@@ -0,0 +1,47 @@
1
+ ---
2
+ summary: "Program enrollment output is removed in favor of ipeds_awards_year1 and ipeds_awards_year2, with program debt and earnings counts read from the correct Scorecard fields. School search gains sort and single-sex filters, and the value-analysis net-price ratio is no longer inflated sixfold."
3
+ breaking: true
4
+ security: false
5
+ ---
6
+
7
+ # 0.2.0 — 2026-09-30
8
+
9
+ **Breaking:** program `enrollment` is gone from `scorecard_get_programs`, `scorecard_search_programs`, and `scorecard://programs/{id}`. Read `ipeds_awards_year1` and `ipeds_awards_year2` instead — IPEDS awards conferred in each of the two pooled debt-cohort years. They count awards, not enrollment or unique students, and no program-level enrollment figure replaces the old field.
10
+
11
+ ## Added
12
+
13
+ - **`scorecard_search_schools` `sort`, `men_only`, `women_only`** ([#8](https://github.com/cyanheads/college-scorecard-mcp-server/issues/8)) — `sort` forwards an API sort expression such as `latest.cost.avg_net_price.overall:asc`. Each flag sends `1` for true and `0` for false; omitting it applies no filter and keeps schools with an unknown flag.
14
+ - **`scorecard_get_school` `men_only` / `women_only`** ([#8](https://github.com/cyanheads/college-scorecard-mcp-server/issues/8)) — booleans when the API reports the flag, absent when it does not; rendered as Yes/No.
15
+ - **`ipeds_awards_year1` / `ipeds_awards_year2`** ([#16](https://github.com/cyanheads/college-scorecard-mcp-server/issues/16)) on both program tools and the programs resource, read from `counts.ipeds_awards1` / `counts.ipeds_awards2`. Tools omit an unknown count; the resource returns `null`.
16
+
17
+ ## Changed
18
+
19
+ - **Program `median_debt`** ([#16](https://github.com/cyanheads/college-scorecard-mcp-server/issues/16)) now reads `debt.staff_grad_plus.all.all_inst.median` — completers' median cumulative Stafford/Grad PLUS borrowing across institutions at the same academic level. `max_debt` filters on the same path, is inclusive, and now excludes programs with unknown debt.
20
+ - **`scorecard_get_programs` `earnings_count`** ([#16](https://github.com/cyanheads/college-scorecard-mcp-server/issues/16)) now reads `earnings.highest.1_yr.working_not_enrolled.overall_count`, the population behind the 1-year median.
21
+ - **`scorecard_search_programs` `min_earnings`** ([#18](https://github.com/cyanheads/college-scorecard-mcp-server/issues/18)) filters the fetched school page locally instead of being sent upstream, where the earnings field is not indexed. It is inclusive and now excludes programs with unavailable earnings; `total` and pagination stay school counts before local filtering.
22
+ - **`scorecard_list_fields` catalog** ([#8](https://github.com/cyanheads/college-scorecard-mcp-server/issues/8), [#16](https://github.com/cyanheads/college-scorecard-mcp-server/issues/16)) — `sortable` flags follow the data dictionary's INDEX column (`latest.earnings.6_yrs_after_entry.median` is no longer marked sortable), and the program entries use the paths above plus `credential.level`.
23
+ - **Docker image** — production dependencies and the OpenTelemetry peers (`scripts/install-otel.ts`) now install in a `$BUILDPLATFORM` stage, cross-installed for the target OS and CPU. Base images move to `oven/bun:1.4.2`; the image no longer sets `MCP_FORCE_CONSOLE_LOGGING`.
24
+ - **Packaging** — the npm `files` allowlist excludes `dist/*.mcpb`, and the `.mcpb` bundle leaves out `docs/idea.md` and `logs/`.
25
+ - **Framework scripts and skills** synced to `@cyanheads/mcp-ts-core` 0.13.10 — `lint-packaging` adds checks 13–15 (npm bundle exclusion, `manifest.json` version parity, Dockerfile build platform) and `release-github` gains `--check`. 31 skill directories and `CLAUDE.md` / `AGENTS.md` follow.
26
+ - **README and `docs/design.md`** describe the corrected program fields, school sorting, and page-local program ranking; embedded data counts are corrected to 202 CIP codes and 80 catalog fields.
27
+ - **Contract tests** ([#10](https://github.com/cyanheads/college-scorecard-mcp-server/issues/10)) — `tests/services/scorecard-contract.test.ts` drives the tools through the real `ScorecardService` over a fetch fake, including the empty-ID `ValidationError` that fails before any request.
28
+
29
+ ## Removed
30
+
31
+ - **Program `enrollment`** ([#16](https://github.com/cyanheads/college-scorecard-mcp-server/issues/16)) from `scorecard_get_programs`, `scorecard_search_programs`, and `scorecard://programs/{id}` — see the migration note above.
32
+
33
+ ## Fixed
34
+
35
+ - **`scorecard_value_analysis` `net_price_to_annual_earnings`** ([#20](https://github.com/cyanheads/college-scorecard-mcp-server/issues/20)) — the ratio divided `earnings_6yr_median`, already an annual figure, by six; it is now net price / `earnings_6yr_median`, so values are one sixth of what earlier versions returned. The description no longer claims a Carnegie peer-group comparison.
36
+ - **Full final pages no longer report `truncated: true`** ([#19](https://github.com/cyanheads/college-scorecard-mcp-server/issues/19)) — `scorecard_search_schools` and `scorecard_search_programs` derive continuation from the API's page metadata, so next-page guidance appears only when a later page exists.
37
+ - **`scorecard_search_programs` notices compose** ([#9](https://github.com/cyanheads/college-scorecard-mcp-server/issues/9)) — suppressed-earnings or no-match guidance and next-page guidance share one `notice` instead of the pagination text replacing the other.
38
+ - **A rejected API key fails after one attempt** ([#13](https://github.com/cyanheads/college-scorecard-mcp-server/issues/13)) — HTTP 401/403 and `API_KEY_MISSING` / `API_KEY_INVALID` responses carry `data.retryable: false`. HTTP 429 and 5xx responses still retry.
39
+
40
+ ## Dependencies
41
+
42
+ - `@cyanheads/mcp-ts-core` `^0.13.6` → `^0.13.10` — resolves `@modelcontextprotocol/server` 2.1.0. Error envelopes now carry `data.requestId`, and error text closes with `(reason … · request <id>)`.
43
+ - `@socketsecurity/bun-security-scanner` `^1.1.2` → `^1.1.3` (dev)
44
+ - `@types/node` `^26.6.2` → `^26.6.3` (dev)
45
+ - `ignore` `^7.0.9` → `^7.0.10` (dev)
46
+ - `vitest` `^5.0.1` → `^5.0.2` (dev)
47
+ - `packageManager` `bun@1.4.0` → `bun@1.4.2`
@@ -1 +1 @@
1
- {"version":3,"file":"field-catalog.d.ts","sourceRoot":"","sources":["../../src/data/field-catalog.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,MAAM,WAAW,UAAU;IACzB,wBAAwB;IACxB,QAAQ,EAAE,MAAM,CAAC;IACjB,iCAAiC;IACjC,WAAW,EAAE,MAAM,CAAC;IACpB,2DAA2D;IAC3D,IAAI,EAAE,MAAM,CAAC;IACb,mDAAmD;IACnD,QAAQ,EAAE,OAAO,CAAC;IAClB,gBAAgB;IAChB,IAAI,EAAE,SAAS,GAAG,OAAO,GAAG,QAAQ,GAAG,SAAS,CAAC;CAClD;AAED,mFAAmF;AACnF,eAAO,MAAM,aAAa,EAAE,UAAU,EAmkBrC,CAAC;AAEF;;;GAGG;AACH,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,SAAK,GAAG,UAAU,EAAE,CAsB1E"}
1
+ {"version":3,"file":"field-catalog.d.ts","sourceRoot":"","sources":["../../src/data/field-catalog.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,MAAM,WAAW,UAAU;IACzB,wBAAwB;IACxB,QAAQ,EAAE,MAAM,CAAC;IACjB,iCAAiC;IACjC,WAAW,EAAE,MAAM,CAAC;IACpB,2DAA2D;IAC3D,IAAI,EAAE,MAAM,CAAC;IACb,mDAAmD;IACnD,QAAQ,EAAE,OAAO,CAAC;IAClB,gBAAgB;IAChB,IAAI,EAAE,SAAS,GAAG,OAAO,GAAG,QAAQ,GAAG,SAAS,CAAC;CAClD;AAED,mFAAmF;AACnF,eAAO,MAAM,aAAa,EAAE,UAAU,EA+kBrC,CAAC;AAEF;;;GAGG;AACH,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,SAAK,GAAG,UAAU,EAAE,CAsB1E"}