diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index fad13a5..ad81a72 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -88,3 +88,41 @@ jobs:
- name: Type-level tests (tsd)
run: npm run test:types --workspace=modelparams
+
+ modelparams-mcp-pkg:
+ name: modelparams-mcp package (build + tests)
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: Set up Node
+ uses: actions/setup-node@v4
+ with:
+ node-version: "20"
+ cache: "npm"
+
+ - name: Install dependencies
+ run: npm ci
+
+ # The MCP server imports the built catalog from the sibling package.
+ - name: Build modelparams
+ run: npm run build --workspace=modelparams
+
+ - name: Typecheck package
+ run: npm run typecheck --workspace=modelparams-mcp
+
+ - name: Build package
+ run: npm run build --workspace=modelparams-mcp
+
+ - name: Tests
+ run: npm test --workspace=modelparams-mcp
+
+ - name: Server starts and answers over stdio
+ run: |
+ printf '%s\n' \
+ '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"ci","version":"1"}}}' \
+ '{"jsonrpc":"2.0","method":"notifications/initialized"}' \
+ '{"jsonrpc":"2.0","id":2,"method":"tools/list"}' \
+ | node packages/modelparams-mcp/dist/index.js 2>/dev/null \
+ | grep -q 'validate_model_params' \
+ || { echo "::error::MCP server did not advertise its tools over stdio"; exit 1; }
diff --git a/.github/workflows/release-modelparams.yml b/.github/workflows/release-modelparams.yml
index c451648..93086ab 100644
--- a/.github/workflows/release-modelparams.yml
+++ b/.github/workflows/release-modelparams.yml
@@ -1,12 +1,23 @@
name: Release modelparams
-# Publishes the `modelparams` npm package when the catalog or codegen pipeline
-# changes on main. Versioning is driven by the same diff classifier
-# (`findRemovedParams`) that `param-guard.yml` uses on PRs:
+# Publishes the `modelparams` and `modelparams-mcp` npm packages when the catalog
+# or codegen pipeline changes on main. Versioning is driven by the same diff
+# classifier (`findRemovedParams`) that `param-guard.yml` uses on PRs:
# • any param removed on a still-existing model → MAJOR
# • any other catalog change → PATCH
# • no semantic catalog change → skipped
#
+# Both packages ship in LOCKSTEP at the same version, and `modelparams-mcp` pins
+# an exact `modelparams` dependency. That's deliberate: the MCP server answers
+# from the catalog compiled into its dependency, so "which catalog does
+# modelparams-mcp@x.y.z carry?" has to have one answer. It also avoids a caret
+# trap — for 0.0.x versions `^0.0.1` means `>=0.0.1 <0.0.2`, which would freeze
+# the server on the first catalog forever.
+#
+# Publishing both from one workflow (rather than two triggered by the same push)
+# keeps the order deterministic: `modelparams` goes out before the package that
+# depends on it.
+#
# Tag-only release: the workflow never pushes to the (protected) main branch.
# The published version is the latest `modelparams@x.y.z` git tag, bumped by the
# classifier; the first release seeds from packages/modelparams/package.json.
@@ -14,9 +25,11 @@ name: Release modelparams
# the git tags are the source of truth for the published version.
#
# Auth: npm OIDC trusted publishing (no token). Requires npm >= 11.5.1, which
-# ships with Node 24. Configure the trusted publisher for the `modelparams`
-# package on npmjs.com (Settings → Trusted Publishers → GitHub Actions →
-# org=mnfst, repo=modelparams.dev, workflow=release-modelparams.yml).
+# ships with Node 24. Each package needs its OWN trusted publisher on npmjs.com
+# (Settings → Trusted Publishers → GitHub Actions → org=mnfst,
+# repo=modelparams.dev, workflow=release-modelparams.yml) — configure it for
+# `modelparams-mcp` as well as `modelparams`, both pointing at this filename.
+# Renaming this file breaks trusted publishing for both.
on:
push:
@@ -24,6 +37,7 @@ on:
paths:
- "models/**"
- "packages/modelparams/**"
+ - "packages/modelparams-mcp/**"
- "src/schema/model.ts"
- "src/data/load.ts"
- "src/data/removals.ts"
@@ -84,27 +98,55 @@ jobs:
- name: Type-level tests (tsd)
run: npm run test:types --workspace=modelparams
+ - name: Build MCP server
+ run: npm run build --workspace=modelparams-mcp
+
+ - name: MCP server tests
+ run: npm test --workspace=modelparams-mcp
+
+ # The catalog classifier only sees `models/**`. An MCP-only change is real
+ # work that still needs shipping, so force a patch when its source moved.
+ - name: Detect MCP-only changes
+ id: mcp
+ run: |
+ if git diff --quiet HEAD~1 HEAD -- packages/modelparams-mcp; then
+ echo "changed=false" >> "$GITHUB_OUTPUT"
+ else
+ echo "changed=true" >> "$GITHUB_OUTPUT"
+ echo "::notice::modelparams-mcp source changed — forcing at least a patch release."
+ fi
+
- name: Compute next version
id: bump
run: npx tsx packages/modelparams/scripts/compute-version.ts
env:
BASE_REF: "HEAD~1"
- FORCE_LEVEL: ${{ inputs.force_level }}
+ FORCE_LEVEL: ${{ inputs.force_level || (steps.mcp.outputs.changed == 'true' && 'patch' || '') }}
- name: Skip publish (no semantic change)
if: steps.bump.outputs.next == ''
run: echo "::notice::No semantic catalog change since the last release — nothing to publish."
- - name: Set package version (no commit)
+ - name: Set package versions (no commit)
if: steps.bump.outputs.next != ''
env:
NEXT: ${{ steps.bump.outputs.next }}
- run: npm version "$NEXT" --no-git-tag-version --allow-same-version --workspace=modelparams
-
- - name: Publish to npm
+ run: |
+ npm version "$NEXT" --no-git-tag-version --allow-same-version --workspace=modelparams
+ npm version "$NEXT" --no-git-tag-version --allow-same-version --workspace=modelparams-mcp
+ # Pin the exact catalog this server was built and tested against.
+ npm pkg set "dependencies.modelparams=$NEXT" --workspace=modelparams-mcp
+ echo "modelparams-mcp depends on modelparams@$(npm pkg get dependencies.modelparams --workspace=modelparams-mcp)"
+
+ - name: Publish modelparams to npm
if: steps.bump.outputs.next != ''
run: npm publish --workspace=modelparams --provenance --access public
+ # Second, so the exact dependency it pins already resolves on the registry.
+ - name: Publish modelparams-mcp to npm
+ if: steps.bump.outputs.next != ''
+ run: npm publish --workspace=modelparams-mcp --provenance --access public
+
- name: Create GitHub release + tag
if: steps.bump.outputs.next != ''
uses: softprops/action-gh-release@v2
@@ -112,3 +154,7 @@ jobs:
tag_name: "modelparams@${{ steps.bump.outputs.next }}"
name: "modelparams@${{ steps.bump.outputs.next }}"
generate_release_notes: true
+ body: |
+ Published to npm:
+ - `modelparams@${{ steps.bump.outputs.next }}`
+ - `modelparams-mcp@${{ steps.bump.outputs.next }}` (pins `modelparams@${{ steps.bump.outputs.next }}`)
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 1e3f4ac..e0c3f44 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -184,13 +184,19 @@ Everything the catalog ships beyond the static site:
- `api/` — Vercel Functions, currently `POST /api/v1/validate`. These serve paths the
static build doesn't emit; the rest of `/api/v1/*` stays static JSON from `dist/`.
+- `packages/modelparams/` — the npm package: generated types plus the runtime
+ validation helpers. `src/generated/` is committed and CI checks it against the YAML,
+ so run `npm run codegen --workspace=modelparams` after changing the catalog.
+- `packages/modelparams-mcp/` — the MCP server. Ships in lockstep with `modelparams`
+ and pins it exactly.
Conventions:
- TypeScript, ES modules, strict mode.
- No file over 300 lines, no function over 50 lines.
- Format with Prettier, lint with ESLint. `npm run format` and `npm run lint` will set you straight.
-- Tests live under `tests/` and run with Vitest.
+- Tests live under `tests/` and run with Vitest. Each package has its own suite;
+ `npm test --workspaces` runs them.
- `npm run typecheck` covers the site and `api/` (via `tsconfig.api.json`).
## Pull requests
diff --git a/README.md b/README.md
index a35ae92..928841c 100644
--- a/README.md
+++ b/README.md
@@ -72,6 +72,14 @@ curl -s https://modelparams.dev/api/v1/validate \
}
```
+## Agents
+
+```bash
+npx -y modelparams-mcp # MCP server: 4 tools, stdio, no network needed
+```
+
+The server exposes `validate_model_params`, `get_model_params`, `list_models`, and `find_models_supporting`. Details in the [package README](packages/modelparams-mcp/README.md). There's also [llms.txt](https://modelparams.dev/llms.txt) if you'd rather just point an agent at a URL.
+
## Adding a model
Drop a YAML file in `models//`, open a PR, and CI validates it against the schema. Details in [CONTRIBUTING.md](CONTRIBUTING.md). Can't open a PR? [File an issue](https://github.com/mnfst/modelparams.dev/issues/new/choose) with a link to the docs.
@@ -83,7 +91,8 @@ npm install
npm run dev # http://localhost:3000
npm run build # → dist/
npm run validate # check every YAML
-npm test
+npm test # site tests, including the /api/v1/validate function
+npm test --workspaces # + both published packages
```
## License
diff --git a/package-lock.json b/package-lock.json
index a316e72..75d7bfd 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -618,6 +618,18 @@
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
}
},
+ "node_modules/@hono/node-server": {
+ "version": "2.0.12",
+ "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-2.0.12.tgz",
+ "integrity": "sha512-eWpQYr67tqJLeaSUl0Q+TquuYfUdTibpOJlUMV2FfUP7+KqCC5TufnwnlXL6mobZBJbGAYRd7ZvEBDCbLInjhg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=20"
+ },
+ "peerDependencies": {
+ "hono": "^4"
+ }
+ },
"node_modules/@humanwhocodes/config-array": {
"version": "0.13.0",
"resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz",
@@ -738,6 +750,379 @@
"integrity": "sha512-ZDflEq0uUvAkH4WK4h3qNvvY09ts4OqUb5azD7A0xKfcuYhffGwB1Q/As2RguZYq4Gh4v925CJ8iodiClzc4zw==",
"license": "MIT"
},
+ "node_modules/@modelcontextprotocol/sdk": {
+ "version": "1.30.0",
+ "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.30.0.tgz",
+ "integrity": "sha512-xKd8OIzlqNzcqcNumGAa6g+PW2kjD5vrpcKOnfldAUPP3j7lnqMPwlTXQm8gF+UwH72z0lqaRbjr9hqGz0eITA==",
+ "license": "MIT",
+ "dependencies": {
+ "@hono/node-server": "^1.19.9 || ^2.0.5",
+ "ajv": "^8.17.1",
+ "ajv-formats": "^3.0.1",
+ "content-type": "^1.0.5",
+ "cors": "^2.8.5",
+ "cross-spawn": "^7.0.5",
+ "eventsource": "^3.0.2",
+ "eventsource-parser": "^3.0.0",
+ "express": "^5.2.1",
+ "express-rate-limit": "^8.2.1",
+ "hono": "^4.11.4",
+ "jose": "^6.1.3",
+ "json-schema-typed": "^8.0.2",
+ "pkce-challenge": "^5.0.0",
+ "raw-body": "^3.0.0",
+ "zod": "^3.25 || ^4.0",
+ "zod-to-json-schema": "^3.25.1"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "peerDependencies": {
+ "@cfworker/json-schema": "^4.1.1",
+ "zod": "^3.25 || ^4.0"
+ },
+ "peerDependenciesMeta": {
+ "@cfworker/json-schema": {
+ "optional": true
+ },
+ "zod": {
+ "optional": false
+ }
+ }
+ },
+ "node_modules/@modelcontextprotocol/sdk/node_modules/accepts": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz",
+ "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==",
+ "license": "MIT",
+ "dependencies": {
+ "mime-types": "^3.0.0",
+ "negotiator": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/@modelcontextprotocol/sdk/node_modules/ajv": {
+ "version": "8.20.0",
+ "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz",
+ "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==",
+ "license": "MIT",
+ "dependencies": {
+ "fast-deep-equal": "^3.1.3",
+ "fast-uri": "^3.0.1",
+ "json-schema-traverse": "^1.0.0",
+ "require-from-string": "^2.0.2"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/epoberezkin"
+ }
+ },
+ "node_modules/@modelcontextprotocol/sdk/node_modules/body-parser": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.3.0.tgz",
+ "integrity": "sha512-2cGmJupaNgg+QUwVLAucDuWuoMZ6EX9iHDRswZ5lsNYEmwPaRknMPCLZz07yTzVq/83p4o/wzbDZbBrTvGGTIw==",
+ "license": "MIT",
+ "dependencies": {
+ "bytes": "^3.1.2",
+ "content-type": "^2.0.0",
+ "debug": "^4.4.3",
+ "http-errors": "^2.0.1",
+ "iconv-lite": "^0.7.2",
+ "on-finished": "^2.4.1",
+ "qs": "^6.15.2",
+ "raw-body": "^3.0.2",
+ "type-is": "^2.1.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/@modelcontextprotocol/sdk/node_modules/body-parser/node_modules/content-type": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.0.0.tgz",
+ "integrity": "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/@modelcontextprotocol/sdk/node_modules/content-disposition": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.1.0.tgz",
+ "integrity": "sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/@modelcontextprotocol/sdk/node_modules/cookie-signature": {
+ "version": "1.2.2",
+ "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz",
+ "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.6.0"
+ }
+ },
+ "node_modules/@modelcontextprotocol/sdk/node_modules/express": {
+ "version": "5.2.1",
+ "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz",
+ "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==",
+ "license": "MIT",
+ "dependencies": {
+ "accepts": "^2.0.0",
+ "body-parser": "^2.2.1",
+ "content-disposition": "^1.0.0",
+ "content-type": "^1.0.5",
+ "cookie": "^0.7.1",
+ "cookie-signature": "^1.2.1",
+ "debug": "^4.4.0",
+ "depd": "^2.0.0",
+ "encodeurl": "^2.0.0",
+ "escape-html": "^1.0.3",
+ "etag": "^1.8.1",
+ "finalhandler": "^2.1.0",
+ "fresh": "^2.0.0",
+ "http-errors": "^2.0.0",
+ "merge-descriptors": "^2.0.0",
+ "mime-types": "^3.0.0",
+ "on-finished": "^2.4.1",
+ "once": "^1.4.0",
+ "parseurl": "^1.3.3",
+ "proxy-addr": "^2.0.7",
+ "qs": "^6.14.0",
+ "range-parser": "^1.2.1",
+ "router": "^2.2.0",
+ "send": "^1.1.0",
+ "serve-static": "^2.2.0",
+ "statuses": "^2.0.1",
+ "type-is": "^2.0.1",
+ "vary": "^1.1.2"
+ },
+ "engines": {
+ "node": ">= 18"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/@modelcontextprotocol/sdk/node_modules/finalhandler": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz",
+ "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==",
+ "license": "MIT",
+ "dependencies": {
+ "debug": "^4.4.0",
+ "encodeurl": "^2.0.0",
+ "escape-html": "^1.0.3",
+ "on-finished": "^2.4.1",
+ "parseurl": "^1.3.3",
+ "statuses": "^2.0.1"
+ },
+ "engines": {
+ "node": ">= 18.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/@modelcontextprotocol/sdk/node_modules/fresh": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz",
+ "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/@modelcontextprotocol/sdk/node_modules/iconv-lite": {
+ "version": "0.7.3",
+ "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.3.tgz",
+ "integrity": "sha512-IKXpvIzjnC9XTAUbVBcMfGS0EPaIXtW6v+zr+RRp+hqULEpo0owZax6wyRwPOJbWbzjYspQwusTsfVr0ifh4uQ==",
+ "license": "MIT",
+ "dependencies": {
+ "safer-buffer": ">= 2.1.2 < 3.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/@modelcontextprotocol/sdk/node_modules/json-schema-traverse": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
+ "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
+ "license": "MIT"
+ },
+ "node_modules/@modelcontextprotocol/sdk/node_modules/media-typer": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.1.tgz",
+ "integrity": "sha512-yz3xRaG20c6/BOzvYoDaGtPmGscs7YivItZEEqe6GbwNfHuxu9YNmvnEkMzKldAGY4/80pRcQRZSEnhquk9XuQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/@modelcontextprotocol/sdk/node_modules/merge-descriptors": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz",
+ "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/@modelcontextprotocol/sdk/node_modules/mime-db": {
+ "version": "1.54.0",
+ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz",
+ "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/@modelcontextprotocol/sdk/node_modules/mime-types": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz",
+ "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==",
+ "license": "MIT",
+ "dependencies": {
+ "mime-db": "^1.54.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/@modelcontextprotocol/sdk/node_modules/negotiator": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz",
+ "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/@modelcontextprotocol/sdk/node_modules/raw-body": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz",
+ "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==",
+ "license": "MIT",
+ "dependencies": {
+ "bytes": "~3.1.2",
+ "http-errors": "~2.0.1",
+ "iconv-lite": "~0.7.0",
+ "unpipe": "~1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.10"
+ }
+ },
+ "node_modules/@modelcontextprotocol/sdk/node_modules/send": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz",
+ "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==",
+ "license": "MIT",
+ "dependencies": {
+ "debug": "^4.4.3",
+ "encodeurl": "^2.0.0",
+ "escape-html": "^1.0.3",
+ "etag": "^1.8.1",
+ "fresh": "^2.0.0",
+ "http-errors": "^2.0.1",
+ "mime-types": "^3.0.2",
+ "ms": "^2.1.3",
+ "on-finished": "^2.4.1",
+ "range-parser": "^1.2.1",
+ "statuses": "^2.0.2"
+ },
+ "engines": {
+ "node": ">= 18"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/@modelcontextprotocol/sdk/node_modules/serve-static": {
+ "version": "2.2.1",
+ "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz",
+ "integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==",
+ "license": "MIT",
+ "dependencies": {
+ "encodeurl": "^2.0.0",
+ "escape-html": "^1.0.3",
+ "parseurl": "^1.3.3",
+ "send": "^1.2.0"
+ },
+ "engines": {
+ "node": ">= 18"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/@modelcontextprotocol/sdk/node_modules/type-is": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.1.0.tgz",
+ "integrity": "sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA==",
+ "license": "MIT",
+ "dependencies": {
+ "content-type": "^2.0.0",
+ "media-typer": "^1.1.0",
+ "mime-types": "^3.0.0"
+ },
+ "engines": {
+ "node": ">= 18"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/@modelcontextprotocol/sdk/node_modules/type-is/node_modules/content-type": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.0.0.tgz",
+ "integrity": "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
"node_modules/@nodelib/fs.scandir": {
"version": "2.1.5",
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
@@ -1898,6 +2283,45 @@
"url": "https://github.com/sponsors/epoberezkin"
}
},
+ "node_modules/ajv-formats": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz",
+ "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==",
+ "license": "MIT",
+ "dependencies": {
+ "ajv": "^8.0.0"
+ },
+ "peerDependencies": {
+ "ajv": "^8.0.0"
+ },
+ "peerDependenciesMeta": {
+ "ajv": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/ajv-formats/node_modules/ajv": {
+ "version": "8.20.0",
+ "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz",
+ "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==",
+ "license": "MIT",
+ "dependencies": {
+ "fast-deep-equal": "^3.1.3",
+ "fast-uri": "^3.0.1",
+ "json-schema-traverse": "^1.0.0",
+ "require-from-string": "^2.0.2"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/epoberezkin"
+ }
+ },
+ "node_modules/ajv-formats/node_modules/json-schema-traverse": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
+ "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
+ "license": "MIT"
+ },
"node_modules/ansi-escapes": {
"version": "7.3.0",
"resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-7.3.0.tgz",
@@ -2491,11 +2915,27 @@
"integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==",
"license": "MIT"
},
+ "node_modules/cors": {
+ "version": "2.8.6",
+ "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz",
+ "integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==",
+ "license": "MIT",
+ "dependencies": {
+ "object-assign": "^4",
+ "vary": "^1"
+ },
+ "engines": {
+ "node": ">= 0.10"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
"node_modules/cross-spawn": {
"version": "7.0.6",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
"integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
- "dev": true,
"license": "MIT",
"dependencies": {
"path-key": "^3.1.0",
@@ -2523,7 +2963,6 @@
"version": "4.4.3",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
"integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
- "dev": true,
"license": "MIT",
"dependencies": {
"ms": "^2.1.3"
@@ -3147,6 +3586,27 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/eventsource": {
+ "version": "3.0.7",
+ "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-3.0.7.tgz",
+ "integrity": "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==",
+ "license": "MIT",
+ "dependencies": {
+ "eventsource-parser": "^3.0.1"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/eventsource-parser": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.1.0.tgz",
+ "integrity": "sha512-kJezFj9YFAMLeORyi7aCLxLbD5/qWMQnoMVlVPyHIll7lgRJCc3JVln9Vgl9nwQi0YkMnhdGTMNn7CkRRAptMg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
"node_modules/execa": {
"version": "8.0.1",
"resolved": "https://registry.npmjs.org/execa/-/execa-8.0.1.tgz",
@@ -3217,6 +3677,25 @@
"url": "https://opencollective.com/express"
}
},
+ "node_modules/express-rate-limit": {
+ "version": "8.6.1",
+ "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.6.1.tgz",
+ "integrity": "sha512-0D493aP61w0TJ2A0wy27riRsO7FMQ7FK+KUHOKCSfPvYo0R55aiC6emCVgFUeShH0fq0ICPVzNcgoS+BsbXQCA==",
+ "license": "MIT",
+ "dependencies": {
+ "debug": "^4.4.3",
+ "ip-address": "^10.2.0"
+ },
+ "engines": {
+ "node": ">= 16"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/express-rate-limit"
+ },
+ "peerDependencies": {
+ "express": ">= 4.11"
+ }
+ },
"node_modules/express/node_modules/debug": {
"version": "2.6.9",
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
@@ -3236,7 +3715,6 @@
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
- "dev": true,
"license": "MIT"
},
"node_modules/fast-glob": {
@@ -3270,6 +3748,22 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/fast-uri": {
+ "version": "3.1.4",
+ "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.4.tgz",
+ "integrity": "sha512-8JnbkQ4juDyvYs4mgFGQqg4yCYtFDtUtmp2QIQq11ZZe5CFQ5wcqm1rqDgAh/QdMySuBnPzMUiJUNZG5N/AiQw==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/fastify"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/fastify"
+ }
+ ],
+ "license": "BSD-3-Clause"
+ },
"node_modules/fastq": {
"version": "1.20.1",
"resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz",
@@ -3694,6 +4188,15 @@
"node": ">= 0.4"
}
},
+ "node_modules/hono": {
+ "version": "4.12.32",
+ "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.32.tgz",
+ "integrity": "sha512-XcuyW9qE2kJn07PkecMOBd5Vq/hMy7mmGw+idz1yblbg9N17ijJODrvPkn7/dwL3Kulj8LcRJ69DLOWf91dRUg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=16.9.0"
+ }
+ },
"node_modules/hosted-git-info": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-4.1.0.tgz",
@@ -3830,6 +4333,15 @@
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
"license": "ISC"
},
+ "node_modules/ip-address": {
+ "version": "10.3.1",
+ "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.3.1.tgz",
+ "integrity": "sha512-1e9d3kb97NHJTIJDZW9rKqW2h6+dFa50Dy0fpPSMQp2ADje5gvKsXmdiK6dwY5t76TaTt5+P5N1Y/LoToIxP6g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 12"
+ }
+ },
"node_modules/ipaddr.js": {
"version": "1.9.1",
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
@@ -3951,6 +4463,12 @@
"node": ">=0.10.0"
}
},
+ "node_modules/is-promise": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz",
+ "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==",
+ "license": "MIT"
+ },
"node_modules/is-stream": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz",
@@ -3981,7 +4499,6 @@
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
"integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
- "dev": true,
"license": "ISC"
},
"node_modules/jake": {
@@ -4037,6 +4554,15 @@
"jiti": "bin/jiti.js"
}
},
+ "node_modules/jose": {
+ "version": "6.2.4",
+ "resolved": "https://registry.npmjs.org/jose/-/jose-6.2.4.tgz",
+ "integrity": "sha512-N8acGzVsQy6M/fjFcxtysNc4Q379TcM5dM/qKkNtsHFji88yANnXTr7BLeP75iPnFwBfQzM/jg2BZ9+HZrHCZA==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/panva"
+ }
+ },
"node_modules/js-tokens": {
"version": "9.0.1",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz",
@@ -4077,6 +4603,12 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/json-schema-typed": {
+ "version": "8.0.2",
+ "resolved": "https://registry.npmjs.org/json-schema-typed/-/json-schema-typed-8.0.2.tgz",
+ "integrity": "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==",
+ "license": "BSD-2-Clause"
+ },
"node_modules/json-stable-stringify-without-jsonify": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz",
@@ -4626,6 +5158,10 @@
"resolved": "packages/modelparams",
"link": true
},
+ "node_modules/modelparams-mcp": {
+ "resolved": "packages/modelparams-mcp",
+ "link": true
+ },
"node_modules/ms": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
@@ -4745,7 +5281,6 @@
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
"integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
- "dev": true,
"license": "MIT",
"engines": {
"node": ">=0.10.0"
@@ -4789,7 +5324,6 @@
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
"integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
- "dev": true,
"license": "ISC",
"dependencies": {
"wrappy": "1"
@@ -4936,7 +5470,6 @@
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
"integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
- "dev": true,
"license": "MIT",
"engines": {
"node": ">=8"
@@ -5034,6 +5567,15 @@
"node": ">= 6"
}
},
+ "node_modules/pkce-challenge": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/pkce-challenge/-/pkce-challenge-5.0.1.tgz",
+ "integrity": "sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=16.20.0"
+ }
+ },
"node_modules/pkg-types": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.3.1.tgz",
@@ -5563,6 +6105,15 @@
"node": ">=8"
}
},
+ "node_modules/require-from-string": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz",
+ "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
"node_modules/resolve": {
"version": "1.22.12",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.12.tgz",
@@ -5715,6 +6266,32 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/router": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz",
+ "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==",
+ "license": "MIT",
+ "dependencies": {
+ "debug": "^4.4.0",
+ "depd": "^2.0.0",
+ "is-promise": "^4.0.0",
+ "parseurl": "^1.3.3",
+ "path-to-regexp": "^8.0.0"
+ },
+ "engines": {
+ "node": ">= 18"
+ }
+ },
+ "node_modules/router/node_modules/path-to-regexp": {
+ "version": "8.4.2",
+ "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.4.2.tgz",
+ "integrity": "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==",
+ "license": "MIT",
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
"node_modules/run-parallel": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz",
@@ -5842,7 +6419,6 @@
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
"integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
- "dev": true,
"license": "MIT",
"dependencies": {
"shebang-regex": "^3.0.0"
@@ -5855,7 +6431,6 @@
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
"integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
- "dev": true,
"license": "MIT",
"engines": {
"node": ">=8"
@@ -7255,7 +7830,6 @@
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
"integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
- "dev": true,
"license": "ISC",
"dependencies": {
"isexe": "^2.0.0"
@@ -7358,7 +7932,6 @@
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
- "dev": true,
"license": "ISC"
},
"node_modules/yallist": {
@@ -7434,6 +8007,21 @@
"engines": {
"node": ">=18"
}
+ },
+ "packages/modelparams-mcp": {
+ "version": "0.0.1",
+ "license": "MIT",
+ "dependencies": {
+ "@modelcontextprotocol/sdk": "^1.30.0",
+ "modelparams": "0.0.1",
+ "zod": "^3.25.0"
+ },
+ "bin": {
+ "modelparams-mcp": "dist/index.js"
+ },
+ "engines": {
+ "node": ">=20"
+ }
}
}
}
diff --git a/packages/modelparams-mcp/LICENSE b/packages/modelparams-mcp/LICENSE
new file mode 100644
index 0000000..ca8c3f8
--- /dev/null
+++ b/packages/modelparams-mcp/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2026 modelparams.dev contributors
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/packages/modelparams-mcp/README.md b/packages/modelparams-mcp/README.md
new file mode 100644
index 0000000..032c12e
--- /dev/null
+++ b/packages/modelparams-mcp/README.md
@@ -0,0 +1,97 @@
+# modelparams-mcp
+
+> **MCP server for the [modelparams.dev](https://modelparams.dev) catalog.** Let an agent check which parameters a model accepts — before it calls one.
+
+```bash
+npx -y modelparams-mcp
+```
+
+Model parameters aren't uniform and don't hold still. `gpt-5.5` has no `temperature`. Claude Opus 4.7 dropped it. Anthropic rejects `top_p` unless `temperature` is 1. Reasoning models reject sampling knobs outright. An agent writing LLM code from training data gets this wrong constantly, and the failure is either a 400 or — worse — a silently ignored parameter and drifting evals.
+
+This server gives the agent a way to look it up instead.
+
+## Install
+
+The server speaks MCP over stdio and bundles the catalog, so it needs no network access and no API key.
+
+**Claude Code**
+
+```bash
+claude mcp add modelparams -- npx -y modelparams-mcp
+```
+
+**Any client with a JSON config** (Claude Desktop, Cursor, Windsurf, Zed, …)
+
+```json
+{
+ "mcpServers": {
+ "modelparams": {
+ "command": "npx",
+ "args": ["-y", "modelparams-mcp"]
+ }
+ }
+}
+```
+
+## Tools
+
+### `validate_model_params`
+
+The one that earns its keep. Give it a model and the params you're about to send:
+
+```json
+{ "model": "claude-3-opus-20240229", "params": { "temperature": 0.5, "top_p": 0.9 } }
+```
+
+```json
+{
+ "model": "anthropic/claude-3-opus-20240229",
+ "valid": false,
+ "summary": "1 parameter(s) would be rejected or silently ignored by anthropic/claude-3-opus-20240229.",
+ "issues": [
+ {
+ "path": "top_p",
+ "code": "not_applicable",
+ "message": "top_p does not apply when temperature ≠ 1",
+ "conflictsWith": ["temperature"]
+ }
+ ],
+ "safeParams": { "temperature": 0.5 }
+}
+```
+
+`safeParams` is always a payload that would pass validation, so an agent that doesn't want to reason about the rules can take it and move on.
+
+Issue codes: `unknown_parameter` (no such knob on this model), `invalid_value` (out of range or not in the enum), `not_applicable` (the knob exists but conflicts with another value in the same request).
+
+### `get_model_params`
+
+Every parameter for one model — type, range, enum values, default, and the conditional rules that gate it.
+
+### `list_models`
+
+Model ids, filtered by provider or substring. Use it to resolve the exact catalog id.
+
+### `find_models_supporting`
+
+Which models expose a given parameter — `reasoning_effort`, `thinking.budget_tokens`, `top_k`. Answers "which models let me set X". Returns near-miss suggestions when nothing matches, since callers usually have the provider's spelling rather than the catalog's.
+
+## Model ids
+
+Ids are `provider/model`. Subscription contracts append `-subscription`, because the same model often exposes different parameters via an API key than via a subscription. Every tool also accepts a bare model slug when only one provider publishes it.
+
+## Related
+
+- **[modelparams](https://www.npmjs.com/package/modelparams)** — the TypeScript package: `ParamsOf` for compile-time safety, `parseParams` and `dropUnsupported` at runtime.
+- **[HTTP API](https://modelparams.dev/api)** — same data as CORS-enabled static JSON, plus `POST /api/v1/validate`.
+- **[Catalog](https://modelparams.dev)** — browse it, or open a PR against the YAML.
+
+## How it's built
+
+The catalog is compiled in at publish time from the YAML source of truth at [github.com/mnfst/modelparams.dev](https://github.com/mnfst/modelparams.dev/tree/main/models). No runtime fetch, no cache to invalidate — update the package to get new models.
+
+This package and [`modelparams`](https://www.npmjs.com/package/modelparams) ship in lockstep at the same version, and this one pins an exact `modelparams` dependency. So `modelparams-mcp@x.y.z` always carries exactly one known catalog — run `npm update modelparams-mcp` (or `npx -y modelparams-mcp@latest`) to pick up newly added models and corrected parameters.
+
+## License
+
+MIT
diff --git a/packages/modelparams-mcp/package.json b/packages/modelparams-mcp/package.json
new file mode 100644
index 0000000..627c30b
--- /dev/null
+++ b/packages/modelparams-mcp/package.json
@@ -0,0 +1,59 @@
+{
+ "name": "modelparams-mcp",
+ "version": "0.0.1",
+ "description": "MCP server for the modelparams.dev catalog — let an agent check which parameters a model accepts before it calls one.",
+ "keywords": [
+ "mcp",
+ "model-context-protocol",
+ "llm",
+ "agent",
+ "openai",
+ "anthropic",
+ "model-parameters",
+ "validation",
+ "tools"
+ ],
+ "license": "MIT",
+ "author": "modelparams.dev contributors",
+ "homepage": "https://modelparams.dev",
+ "repository": {
+ "type": "git",
+ "url": "git+https://github.com/mnfst/modelparams.dev.git",
+ "directory": "packages/modelparams-mcp"
+ },
+ "bugs": {
+ "url": "https://github.com/mnfst/modelparams.dev/issues"
+ },
+ "type": "module",
+ "bin": {
+ "modelparams-mcp": "./dist/index.js"
+ },
+ "main": "./dist/index.js",
+ "types": "./dist/index.d.ts",
+ "exports": {
+ ".": {
+ "types": "./dist/index.d.ts",
+ "import": "./dist/index.js"
+ },
+ "./package.json": "./package.json"
+ },
+ "files": [
+ "dist",
+ "README.md",
+ "LICENSE"
+ ],
+ "engines": {
+ "node": ">=20"
+ },
+ "scripts": {
+ "build": "tsc -p tsconfig.build.json && chmod +x dist/index.js",
+ "typecheck": "tsc --noEmit",
+ "test": "vitest run",
+ "prepublishOnly": "npm run build && npm run test"
+ },
+ "dependencies": {
+ "@modelcontextprotocol/sdk": "^1.30.0",
+ "zod": "^3.25.0",
+ "modelparams": "0.0.1"
+ }
+}
diff --git a/packages/modelparams-mcp/src/index.ts b/packages/modelparams-mcp/src/index.ts
new file mode 100644
index 0000000..b0e4166
--- /dev/null
+++ b/packages/modelparams-mcp/src/index.ts
@@ -0,0 +1,24 @@
+#!/usr/bin/env node
+import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
+import { createServer } from "./server.js";
+
+export { createServer } from "./server.js";
+export * from "./tools.js";
+
+async function main(): Promise {
+ const server = createServer();
+ // stdout is the transport — anything written there that isn't a protocol
+ // message corrupts the session, so diagnostics go to stderr.
+ await server.connect(new StdioServerTransport());
+ console.error("modelparams MCP server ready on stdio");
+}
+
+const isDirectRun =
+ process.argv[1] !== undefined && import.meta.url === `file://${process.argv[1]}`;
+
+if (isDirectRun) {
+ main().catch((err) => {
+ console.error("modelparams MCP server failed to start:", err);
+ process.exit(1);
+ });
+}
diff --git a/packages/modelparams-mcp/src/server.ts b/packages/modelparams-mcp/src/server.ts
new file mode 100644
index 0000000..870b7f7
--- /dev/null
+++ b/packages/modelparams-mcp/src/server.ts
@@ -0,0 +1,117 @@
+import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
+import { z } from "zod";
+import {
+ findModelsSupporting,
+ getModelParams,
+ listCatalogModels,
+ validateModelParams,
+ type ToolPayload,
+} from "./tools.js";
+
+/** Every tool here is a pure read over data compiled into the package. */
+const READ_ONLY = { readOnlyHint: true, idempotentHint: true, openWorldHint: false } as const;
+
+function reply(payload: ToolPayload) {
+ return { content: [{ type: "text" as const, text: JSON.stringify(payload, null, 2) }] };
+}
+
+/**
+ * Build the server. Kept separate from the stdio entry point so tests can drive
+ * it over an in-memory transport.
+ */
+export function createServer(): McpServer {
+ const server = new McpServer(
+ { name: "modelparams", version: "0.0.1" },
+ {
+ instructions:
+ "Answers what parameters an LLM accepts, using the modelparams.dev catalog of " +
+ "239 models. Call validate_model_params before issuing any LLM request that sets " +
+ "non-default parameters, and when diagnosing a 400 from a provider — it catches " +
+ "unsupported parameters and invalid combinations that a provider would reject or " +
+ "silently ignore.",
+ },
+ );
+
+ server.registerTool(
+ "validate_model_params",
+ {
+ title: "Validate model parameters",
+ description:
+ "Check a set of LLM request parameters against what a model actually accepts, before " +
+ "calling the provider. Catches unknown parameters, out-of-range values, and " +
+ "combinations the provider rejects — such as top_p alongside a non-default " +
+ "temperature on Anthropic models, or sampling knobs on reasoning models. Returns a " +
+ "corrected `safeParams` payload that is guaranteed to validate. Use this whenever " +
+ "writing or debugging code that calls an LLM API with non-default parameters.",
+ inputSchema: {
+ model: z
+ .string()
+ .describe('Catalog id ("anthropic/claude-opus-4-7") or bare model slug ("gpt-5.5").'),
+ params: z
+ .record(z.unknown())
+ .optional()
+ .describe(
+ 'Provider-native parameter paths to values, e.g. {"temperature": 0.7, "thinking.type": "enabled"}.',
+ ),
+ },
+ annotations: READ_ONLY,
+ },
+ async (args) => reply(validateModelParams(args)),
+ );
+
+ server.registerTool(
+ "get_model_params",
+ {
+ title: "Get model parameters",
+ description:
+ "List every parameter a model accepts, with its type, allowed range or enum values, " +
+ "default, and any conditional rules governing when it applies. Use before writing " +
+ "code that calls a model, or when building a model settings UI.",
+ inputSchema: {
+ model: z.string().describe('Catalog id ("openai/gpt-5.5") or bare model slug ("gpt-5.5").'),
+ },
+ annotations: READ_ONLY,
+ },
+ async (args) => reply(getModelParams(args)),
+ );
+
+ server.registerTool(
+ "list_models",
+ {
+ title: "List catalog models",
+ description:
+ "List model ids in the modelparams.dev catalog, optionally filtered by provider or a " +
+ "substring. Use to discover the exact id the other tools expect.",
+ inputSchema: {
+ provider: z
+ .string()
+ .optional()
+ .describe('Restrict to one provider slug, e.g. "anthropic", "openai", "google".'),
+ query: z.string().optional().describe("Case-insensitive substring match on the model id."),
+ limit: z.number().int().positive().max(1000).optional().describe("Default 100."),
+ },
+ annotations: READ_ONLY,
+ },
+ async (args) => reply(listCatalogModels(args)),
+ );
+
+ server.registerTool(
+ "find_models_supporting",
+ {
+ title: "Find models supporting a parameter",
+ description:
+ 'Find which models accept a given parameter, e.g. "reasoning_effort", ' +
+ '"thinking.budget_tokens", or "top_k". Use to answer "which models support X" or to ' +
+ "pick a model that exposes a knob you need.",
+ inputSchema: {
+ parameter: z.string().describe('Exact parameter path, e.g. "top_k" or "thinking.type".'),
+ provider: z.string().optional().describe("Restrict to one provider slug."),
+ limit: z.number().int().positive().max(1000).optional().describe("Default 100."),
+ },
+ annotations: READ_ONLY,
+ },
+ async (args) => reply(findModelsSupporting(args)),
+ );
+
+ return server;
+}
diff --git a/packages/modelparams-mcp/src/tools.ts b/packages/modelparams-mcp/src/tools.ts
new file mode 100644
index 0000000..6d47785
--- /dev/null
+++ b/packages/modelparams-mcp/src/tools.ts
@@ -0,0 +1,194 @@
+import {
+ CATALOG,
+ dropUnsupported,
+ getDefaults,
+ getModel,
+ listModels,
+ PROVIDERS,
+ resolveModelId,
+ type ModelId,
+ type Param,
+ type Provider,
+} from "modelparams";
+
+/** Everything a tool returns: a JSON-serialisable payload the agent can act on. */
+export type ToolPayload = Record;
+
+function modelIdOf(entry: (typeof CATALOG)[number]): ModelId {
+ const suffix = entry.authType === "api_key" ? "" : "-subscription";
+ return `${entry.provider}/${entry.model}${suffix}` as ModelId;
+}
+
+/**
+ * Turn an unresolvable model reference into a payload that tells the agent how
+ * to recover, rather than a bare error it will retry verbatim.
+ */
+function unresolved(
+ input: string,
+ result: Exclude, { ok: true }>,
+): ToolPayload {
+ if (result.reason === "ambiguous") {
+ return {
+ error: "ambiguous_model",
+ message: `"${input}" is published by more than one provider. Retry with one of the qualified ids below.`,
+ matches: result.matches,
+ };
+ }
+ return {
+ error: "unknown_model",
+ message: `"${input}" is not in the catalog. Call list_models to browse, or retry with one of the suggestions below.`,
+ suggestions: result.suggestions,
+ };
+}
+
+function describeParam(param: Param): ToolPayload {
+ return {
+ path: param.path,
+ label: param.label,
+ type: param.type,
+ group: param.group,
+ description: param.description,
+ ...(param.default !== undefined ? { default: param.default } : {}),
+ ...(param.range ? { range: param.range } : {}),
+ ...(param.values ? { values: param.values } : {}),
+ ...(param.applicability ? { appliesOnlyWhen: param.applicability } : {}),
+ };
+}
+
+/**
+ * The hero tool: does this request survive contact with the provider?
+ *
+ * Returns the diagnosis and a corrected payload, so an agent that doesn't want
+ * to reason about the rules can just take `safeParams` and proceed.
+ */
+export function validateModelParams(input: {
+ model: string;
+ params?: Record;
+}): ToolPayload {
+ const resolved = resolveModelId(input.model);
+ if (!resolved.ok) return unresolved(input.model, resolved);
+
+ const supplied = input.params ?? {};
+ const { params: safeParams, dropped } = dropUnsupported(resolved.id, supplied);
+ const entry = getModel(resolved.id);
+
+ return {
+ model: resolved.id,
+ provider: entry.provider,
+ authType: entry.authType,
+ valid: dropped.length === 0,
+ summary:
+ dropped.length === 0
+ ? `All ${Object.keys(supplied).length} parameter(s) are accepted by ${resolved.id}.`
+ : `${dropped.length} parameter(s) would be rejected or silently ignored by ${resolved.id}.`,
+ issues: dropped.map((d) => ({
+ path: d.path,
+ code: d.code,
+ message: d.reason,
+ ...(d.conflictsWith ? { conflictsWith: d.conflictsWith } : {}),
+ })),
+ safeParams,
+ docs: `https://modelparams.dev/models/${resolved.id}`,
+ };
+}
+
+/** Full parameter surface for one model, including conditional rules. */
+export function getModelParams(input: { model: string }): ToolPayload {
+ const resolved = resolveModelId(input.model);
+ if (!resolved.ok) return unresolved(input.model, resolved);
+
+ const entry = getModel(resolved.id);
+ const params = entry.params as readonly Param[];
+ return {
+ model: resolved.id,
+ provider: entry.provider,
+ authType: entry.authType,
+ parameterCount: params.length,
+ params: params.map(describeParam),
+ defaults: getDefaults(resolved.id),
+ docs: `https://modelparams.dev/models/${resolved.id}`,
+ };
+}
+
+/** Browse the catalog to find the exact id the other tools expect. */
+export function listCatalogModels(input: {
+ provider?: string;
+ query?: string;
+ limit?: number;
+}): ToolPayload {
+ const { provider, query, limit = 100 } = input;
+
+ if (provider && !PROVIDERS.includes(provider as Provider)) {
+ return {
+ error: "unknown_provider",
+ message: `"${provider}" is not a provider in the catalog.`,
+ providers: PROVIDERS,
+ };
+ }
+
+ let ids = provider ? listModels({ provider: provider as Provider }) : listModels();
+ if (query) {
+ const needle = query.toLowerCase();
+ ids = ids.filter((id) => id.toLowerCase().includes(needle));
+ }
+
+ return {
+ total: ids.length,
+ returned: Math.min(ids.length, limit),
+ truncated: ids.length > limit,
+ providers: provider ? [provider] : PROVIDERS,
+ models: ids.slice(0, limit),
+ };
+}
+
+/** Which models expose a given knob — the "can I use X anywhere?" question. */
+export function findModelsSupporting(input: {
+ parameter: string;
+ provider?: string;
+ limit?: number;
+}): ToolPayload {
+ const { parameter, provider, limit = 100 } = input;
+ const needle = parameter.toLowerCase();
+
+ const matches: { model: ModelId; param: Param }[] = [];
+ for (const entry of CATALOG) {
+ if (provider && entry.provider !== provider) continue;
+ const param = (entry.params as readonly Param[]).find((p) => p.path.toLowerCase() === needle);
+ if (param) matches.push({ model: modelIdOf(entry), param });
+ }
+
+ if (matches.length === 0) {
+ // A near-miss list beats an empty result — the caller usually has the
+ // provider's spelling of the parameter, not the catalog's.
+ const known = new Set();
+ for (const entry of CATALOG) {
+ for (const p of entry.params as readonly Param[]) {
+ if (p.path.toLowerCase().includes(needle) || needle.includes(p.path.toLowerCase())) {
+ known.add(p.path);
+ }
+ }
+ }
+ return {
+ parameter,
+ total: 0,
+ message: `No model in the catalog accepts "${parameter}".`,
+ similarParameters: [...known].sort().slice(0, 10),
+ };
+ }
+
+ return {
+ parameter,
+ total: matches.length,
+ returned: Math.min(matches.length, limit),
+ truncated: matches.length > limit,
+ models: matches.slice(0, limit).map(({ model, param }) => ({
+ model,
+ type: param.type,
+ ...(param.default !== undefined ? { default: param.default } : {}),
+ ...(param.range ? { range: param.range } : {}),
+ ...(param.values ? { values: param.values } : {}),
+ ...(param.applicability ? { appliesOnlyWhen: param.applicability } : {}),
+ })),
+ docs: `https://modelparams.dev/parameters/${parameter.replace(/\./g, "-")}`,
+ };
+}
diff --git a/packages/modelparams-mcp/tests/server.test.ts b/packages/modelparams-mcp/tests/server.test.ts
new file mode 100644
index 0000000..517e359
--- /dev/null
+++ b/packages/modelparams-mcp/tests/server.test.ts
@@ -0,0 +1,205 @@
+import { Client } from "@modelcontextprotocol/sdk/client/index.js";
+import { InMemoryTransport } from "@modelcontextprotocol/sdk/inMemory.js";
+import { afterEach, beforeEach, describe, expect, it } from "vitest";
+import { createServer } from "../src/server.js";
+
+let client: Client;
+
+interface ParamInfo {
+ path: string;
+ type: string;
+ values?: string[];
+ appliesOnlyWhen?: unknown;
+}
+
+interface ValidateResult {
+ model: string;
+ provider: string;
+ valid: boolean;
+ summary: string;
+ issues: { path: string; code: string; message: string; conflictsWith?: string[] }[];
+ safeParams: Record;
+ error?: string;
+ suggestions?: string[];
+}
+
+interface ModelParamsResult {
+ model: string;
+ parameterCount: number;
+ params: ParamInfo[];
+}
+
+interface ListResult {
+ total: number;
+ truncated: boolean;
+ models: string[];
+ providers: string[];
+ error?: string;
+}
+
+interface FindResult {
+ parameter: string;
+ total: number;
+ models: { model: string; type: string }[];
+ similarParameters?: string[];
+}
+
+/** Parse the JSON payload a tool replies with. */
+async function call(name: string, args: Record): Promise {
+ const result = (await client.callTool({ name, arguments: args })) as {
+ content: { type: string; text: string }[];
+ };
+ return JSON.parse(result.content[0]!.text) as T;
+}
+
+beforeEach(async () => {
+ const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
+ client = new Client({ name: "test", version: "0.0.0" });
+ await Promise.all([createServer().connect(serverTransport), client.connect(clientTransport)]);
+});
+
+afterEach(async () => {
+ await client.close();
+});
+
+describe("tool discovery", () => {
+ it("advertises the four catalog tools", async () => {
+ const { tools } = await client.listTools();
+ expect(tools.map((t) => t.name).sort()).toEqual([
+ "find_models_supporting",
+ "get_model_params",
+ "list_models",
+ "validate_model_params",
+ ]);
+ });
+
+ it("marks every tool read-only", async () => {
+ const { tools } = await client.listTools();
+ for (const tool of tools) {
+ expect(tool.annotations?.readOnlyHint, tool.name).toBe(true);
+ }
+ });
+
+ it("describes each tool well enough for an agent to choose it", async () => {
+ const { tools } = await client.listTools();
+ for (const tool of tools) {
+ expect(tool.description!.length, tool.name).toBeGreaterThan(80);
+ }
+ });
+});
+
+describe("validate_model_params", () => {
+ it("passes a valid request", async () => {
+ const out = await call("validate_model_params", {
+ model: "anthropic/claude-3-opus-20240229",
+ params: { max_tokens: 1024, temperature: 1 },
+ });
+ expect(out.valid).toBe(true);
+ expect(out.issues).toEqual([]);
+ });
+
+ it("catches a conditional conflict and returns a corrected payload", async () => {
+ const out = await call("validate_model_params", {
+ model: "anthropic/claude-3-opus-20240229",
+ params: { temperature: 0.5, top_p: 0.9, max_tokens: 1024 },
+ });
+ expect(out.valid).toBe(false);
+ expect(out.issues[0]!.path).toBe("top_p");
+ expect(out.issues[0]!.code).toBe("not_applicable");
+ expect(out.safeParams).toEqual({ temperature: 0.5, max_tokens: 1024 });
+ });
+
+ it("catches a parameter the model does not expose", async () => {
+ const out = await call("validate_model_params", {
+ model: "openai/gpt-5.5",
+ params: { temperature: 0.7 },
+ });
+ expect(out.valid).toBe(false);
+ expect(out.issues[0]!.code).toBe("unknown_parameter");
+ });
+
+ it("treats an omitted params object as an empty request", async () => {
+ const out = await call("validate_model_params", { model: "openai/gpt-5.5" });
+ expect(out.valid).toBe(true);
+ });
+
+ it("guides recovery from an unknown model", async () => {
+ const out = await call("validate_model_params", {
+ model: "claude-opus-9",
+ params: {},
+ });
+ expect(out.error).toBe("unknown_model");
+ expect(Array.isArray(out.suggestions)).toBe(true);
+ });
+});
+
+describe("get_model_params", () => {
+ it("returns typed parameters and defaults", async () => {
+ const out = await call("get_model_params", { model: "gpt-5.5" });
+ expect(out.model).toBe("openai/gpt-5.5");
+ expect(out.parameterCount).toBeGreaterThan(0);
+ const effort = out.params.find((p) => p.path === "reasoning_effort");
+ expect(effort!.type).toBe("enum");
+ expect(effort!.values).toContain("low");
+ });
+
+ it("surfaces conditional rules", async () => {
+ const out = await call("get_model_params", {
+ model: "anthropic/claude-opus-4-7",
+ });
+ const display = out.params.find((p) => p.path === "thinking.display");
+ expect(display!.appliesOnlyWhen).toBeDefined();
+ });
+});
+
+describe("list_models", () => {
+ it("lists the whole catalog", async () => {
+ const out = await call("list_models", {});
+ expect(out.total).toBeGreaterThan(200);
+ });
+
+ it("filters by provider", async () => {
+ const out = await call("list_models", { provider: "anthropic" });
+ expect(out.total).toBeGreaterThan(0);
+ expect(out.models.every((m) => m.startsWith("anthropic/"))).toBe(true);
+ });
+
+ it("filters by substring", async () => {
+ const out = await call("list_models", { query: "opus" });
+ expect(out.models.every((m) => m.includes("opus"))).toBe(true);
+ });
+
+ it("reports an unknown provider with the valid set", async () => {
+ const out = await call("list_models", { provider: "nope" });
+ expect(out.error).toBe("unknown_provider");
+ expect(out.providers).toContain("openai");
+ });
+
+ it("flags truncation rather than silently cutting off", async () => {
+ const out = await call("list_models", { limit: 5 });
+ expect(out.models).toHaveLength(5);
+ expect(out.truncated).toBe(true);
+ });
+});
+
+describe("find_models_supporting", () => {
+ it("finds models exposing a parameter", async () => {
+ const out = await call("find_models_supporting", { parameter: "top_k" });
+ expect(out.total).toBeGreaterThan(0);
+ expect(out.models[0]!.model).toBeTruthy();
+ });
+
+ it("scopes to a provider", async () => {
+ const out = await call("find_models_supporting", {
+ parameter: "top_k",
+ provider: "anthropic",
+ });
+ expect(out.models.every((m) => m.model.startsWith("anthropic/"))).toBe(true);
+ });
+
+ it("suggests near misses when nothing matches", async () => {
+ const out = await call("find_models_supporting", { parameter: "temperatur" });
+ expect(out.total).toBe(0);
+ expect(out.similarParameters).toContain("temperature");
+ });
+});
diff --git a/packages/modelparams-mcp/tsconfig.build.json b/packages/modelparams-mcp/tsconfig.build.json
new file mode 100644
index 0000000..cc5774c
--- /dev/null
+++ b/packages/modelparams-mcp/tsconfig.build.json
@@ -0,0 +1,6 @@
+{
+ "extends": "./tsconfig.json",
+ "compilerOptions": {
+ "noEmit": false
+ }
+}
diff --git a/packages/modelparams-mcp/tsconfig.json b/packages/modelparams-mcp/tsconfig.json
new file mode 100644
index 0000000..37a155e
--- /dev/null
+++ b/packages/modelparams-mcp/tsconfig.json
@@ -0,0 +1,16 @@
+{
+ "extends": "../../tsconfig.json",
+ "compilerOptions": {
+ "rootDir": "./src",
+ "outDir": "./dist",
+ "declaration": true,
+ "declarationMap": true,
+ "sourceMap": true,
+ "composite": false,
+ "moduleResolution": "NodeNext",
+ "module": "NodeNext",
+ "paths": {}
+ },
+ "include": ["src/**/*.ts"],
+ "exclude": ["node_modules", "dist", "tests"]
+}
diff --git a/packages/modelparams-mcp/vitest.config.ts b/packages/modelparams-mcp/vitest.config.ts
new file mode 100644
index 0000000..ad1485e
--- /dev/null
+++ b/packages/modelparams-mcp/vitest.config.ts
@@ -0,0 +1,10 @@
+import { defineConfig } from "vitest/config";
+
+export default defineConfig({
+ test: {
+ environment: "node",
+ include: ["tests/**/*.test.ts"],
+ globals: false,
+ reporters: "default",
+ },
+});
diff --git a/src/build/build.ts b/src/build/build.ts
index c984c41..88253f1 100644
--- a/src/build/build.ts
+++ b/src/build/build.ts
@@ -159,6 +159,7 @@ async function writeApiIndex(modelCount: number): Promise {
validate: "POST /api/v1/validate",
},
modelCount,
+ mcp: "npx -y modelparams-mcp",
docs: "https://github.com/mnfst/modelparams.dev#api",
};
await writeJson(path.join(DIST_API_DIR, "index.json"), body);
diff --git a/src/data/llms.ts b/src/data/llms.ts
index 4616760..bd3ef9c 100644
--- a/src/data/llms.ts
+++ b/src/data/llms.ts
@@ -80,6 +80,18 @@ function guideApi(siteUrl: string): string[] {
"exists but conflicts with another value in the same request, listed in",
"`conflictsWith`).",
"",
+ "## MCP server",
+ "",
+ "Agents can query the catalog over the Model Context Protocol instead of HTTP. The",
+ "server runs on stdio and bundles the catalog, so it needs no network access:",
+ "",
+ "```bash",
+ "npx -y modelparams-mcp",
+ "```",
+ "",
+ "Tools: `validate_model_params`, `get_model_params`, `list_models`,",
+ "`find_models_supporting`.",
+ "",
"## JSON Schema",
"",
"Every entry validates against a JSON Schema you can use in your editor or pipeline:",
diff --git a/src/views/api.ejs b/src/views/api.ejs
index 4eb8c0a..0df5744 100644
--- a/src/views/api.ejs
+++ b/src/views/api.ejs
@@ -109,6 +109,25 @@
+
+
+ MCP server
+
+ Let a coding agent check the catalog itself. The server speaks the Model Context Protocol over stdio and bundles
+ the catalog, so it works with no network access.
+
+
+
npx -y modelparams-mcp
+
+
+ Tools:
+ validate_model_params,
+ get_model_params,
+ list_models,
+ find_models_supporting.
+
+
+