From fa43b79f9e954d004c035ecf1ea4c52a850ab878 Mon Sep 17 00:00:00 2001 From: Bruno Perez Date: Thu, 30 Jul 2026 15:11:49 +0200 Subject: [PATCH] feat: let agents look up model params over MCP Third of four, on top of the validate endpoint. Adds modelparams-mcp: an MCP server over the catalog with four read-only tools. validate_model_params is the one that earns its keep, returning both the diagnosis and a corrected safeParams payload. The others cover get_model_params, list_models and find_models_supporting. An agent writing LLM code from training data gets parameters wrong constantly, because the data goes stale as providers drop knobs. This gives it something to look up instead. The catalog is compiled in, so the server needs no network access and no API key. Both packages now publish from the one release workflow, in lockstep at the same version, modelparams first so the exact dependency the server pins already resolves on the registry. Publishing from a single workflow rather than two triggered by the same push keeps that order deterministic. The lockstep pin also fixes a caret trap: the server declared "modelparams": "^0.0.1", and for 0.0.x versions that resolves to >=0.0.1 <0.0.2, which would have frozen it on the first catalog forever. Note for whoever cuts the first release: modelparams-mcp needs its own npm trusted publisher pointing at release-modelparams.yml, or the publish step fails. --- .github/workflows/ci.yml | 38 ++ .github/workflows/release-modelparams.yml | 68 +- CONTRIBUTING.md | 8 +- README.md | 11 +- package-lock.json | 610 +++++++++++++++++- packages/modelparams-mcp/LICENSE | 21 + packages/modelparams-mcp/README.md | 97 +++ packages/modelparams-mcp/package.json | 59 ++ packages/modelparams-mcp/src/index.ts | 24 + packages/modelparams-mcp/src/server.ts | 117 ++++ packages/modelparams-mcp/src/tools.ts | 194 ++++++ packages/modelparams-mcp/tests/server.test.ts | 205 ++++++ packages/modelparams-mcp/tsconfig.build.json | 6 + packages/modelparams-mcp/tsconfig.json | 16 + packages/modelparams-mcp/vitest.config.ts | 10 + src/build/build.ts | 1 + src/data/llms.ts | 12 + src/views/api.ejs | 19 + 18 files changed, 1492 insertions(+), 24 deletions(-) create mode 100644 packages/modelparams-mcp/LICENSE create mode 100644 packages/modelparams-mcp/README.md create mode 100644 packages/modelparams-mcp/package.json create mode 100644 packages/modelparams-mcp/src/index.ts create mode 100644 packages/modelparams-mcp/src/server.ts create mode 100644 packages/modelparams-mcp/src/tools.ts create mode 100644 packages/modelparams-mcp/tests/server.test.ts create mode 100644 packages/modelparams-mcp/tsconfig.build.json create mode 100644 packages/modelparams-mcp/tsconfig.json create mode 100644 packages/modelparams-mcp/vitest.config.ts 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