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. +

+
+

JSON Schema