From 5b5768cbf8907c0a809a563424a50285847c4dc4 Mon Sep 17 00:00:00 2001 From: Guillaume Gay Date: Fri, 21 Aug 2026 11:26:49 +0200 Subject: [PATCH 1/7] feat: advertise the MCP server across the site Add an MCP link to the header nav and footer, an anchor on the API page's MCP section, and a hero mention + CTA on the homepage. --- src/views/api.ejs | 2 +- src/views/index.ejs | 13 ++++++++++++- src/views/partials/footer.ejs | 1 + src/views/partials/header.ejs | 7 +++++++ 4 files changed, 21 insertions(+), 2 deletions(-) diff --git a/src/views/api.ejs b/src/views/api.ejs index 2a53b801..4eaec354 100644 --- a/src/views/api.ejs +++ b/src/views/api.ejs @@ -110,7 +110,7 @@ -
+

MCP server

Let a coding agent check the catalog itself. Hosted at diff --git a/src/views/index.ejs b/src/views/index.ejs index 8abd1a5f..1f7fe821 100644 --- a/src/views/index.ejs +++ b/src/views/index.ejs @@ -6,7 +6,7 @@

An open, community-maintained catalog of LLM API parameters — temperature, top_p, max_tokens and the rest of the request body. - Browse the UI below, query the API, or install the npm package. + Browse the UI below, query the API, install the npm package, or give your coding agent the MCP server.

@@ -52,6 +52,17 @@ Use the API + + + + + MCP for agents +
diff --git a/src/views/partials/footer.ejs b/src/views/partials/footer.ejs index ad4e4e68..71872ef8 100644 --- a/src/views/partials/footer.ejs +++ b/src/views/partials/footer.ejs @@ -20,6 +20,7 @@
API + MCP server Glossary Parameters vs. weights JSON API diff --git a/src/views/partials/header.ejs b/src/views/partials/header.ejs index 1e581e47..0e758acc 100644 --- a/src/views/partials/header.ejs +++ b/src/views/partials/header.ejs @@ -38,6 +38,12 @@ > API + + MCP +
API + MCP GitHub

Providers

From b7f253c3ee2fa1b04168a6f8da7260e37b5afdab Mon Sep 17 00:00:00 2001 From: Guillaume Gay Date: Fri, 21 Aug 2026 11:55:15 +0200 Subject: [PATCH 2/7] feat: dedicated MCP page with content negotiation Give the MCP server its own page. The endpoint stays at /mcp; a browser GET now gets the docs page (clients still get the protocol), so one URL serves both. The page shows per-client install commands behind a picker, and each tool's input schema and example output. Served self-contained so the Vercel function needs no filesystem access. --- api/mcp.ts | 13 +- src/build/render-mcp.ts | 186 ++++++++++++++++++++++++++ src/data/mcp.ts | 238 ++++++++++++++++++++++++++++++++++ src/server/app.ts | 6 + src/views/api.ejs | 23 +--- src/views/index.ejs | 2 +- src/views/partials/footer.ejs | 2 +- src/views/partials/header.ejs | 4 +- 8 files changed, 450 insertions(+), 24 deletions(-) create mode 100644 src/build/render-mcp.ts create mode 100644 src/data/mcp.ts diff --git a/api/mcp.ts b/api/mcp.ts index b9939cc9..8d0687e4 100644 --- a/api/mcp.ts +++ b/api/mcp.ts @@ -13,6 +13,7 @@ import type { JSONRPCMessage } from "@modelcontextprotocol/sdk/types.js"; import type { Transport } from "@modelcontextprotocol/sdk/shared/transport.js"; import { createServer } from "../packages/modelparams-mcp/src/server.js"; +import { renderMcpHtml } from "../src/build/render-mcp.js"; const MAX_BODY_BYTES = 256 * 1024; @@ -123,11 +124,21 @@ export default { // A GET is either an MCP client opening the optional server-to-client // stream, which a stateless server has nothing to put on, or a person - // opening the URL. Answer each in its own terms. + // opening the URL. Answer each in its own terms: browsers get the docs + // page, event-stream clients get a 405, and plain API clients get JSON. if (request.method === "GET") { if ((request.headers.get("accept") ?? "").includes("text/event-stream")) { return json({ error: "streaming_not_supported", usage: USAGE }, 405, { Allow: "POST" }); } + if ((request.headers.get("accept") ?? "").includes("text/html")) { + return new Response(renderMcpHtml(), { + status: 200, + headers: { + "Content-Type": "text/html; charset=utf-8", + "Cache-Control": "no-store", + }, + }); + } return json(USAGE); } diff --git a/src/build/render-mcp.ts b/src/build/render-mcp.ts new file mode 100644 index 00000000..84a096fa --- /dev/null +++ b/src/build/render-mcp.ts @@ -0,0 +1,186 @@ +import { MCP_CLIENTS, MCP_ENDPOINT, MCP_TOOLS, type McpClient } from "../data/mcp.js"; + +/** + * The MCP docs page as a fully self-contained HTML document — no external + * stylesheet or script, so the Vercel function that serves the `/mcp` endpoint + * can also answer a browser GET with this page without touching the static + * build's filesystem. + */ +export function renderMcpHtml(): string { + const clients = MCP_CLIENTS; + const tools = MCP_TOOLS; + + const clientButtons = clients + .map( + (client, i) => ` + `, + ) + .join(""); + + const clientPanels = clients + .map( + (client, i) => ` +
+ ${client.note ? `

${esc(client.note)}

` : ""} +
+
${esc(client.command)}
+ +
+
`, + ) + .join(""); + + const toolSections = tools + .map( + (tool) => ` +
+

${esc(tool.name)}

+

${esc(tool.description)}

+
+
+

Input

+
${esc(JSON.stringify(tool.inputSchema, null, 2))}
+
+
+

Example output

+
${esc(JSON.stringify(tool.example, null, 2))}
+
+
+
`, + ) + .join(""); + + return ` + + + + +MCP server · modelparams.dev + + + + +
+ modelparams.dev / MCP server + ← Back to catalog +
+
+

MCP server

+

Give your coding agent the catalog, so it can check which parameters a model accepts before it calls one — instead of guessing from training data and eating a 400, or worse, having a parameter silently ignored. Hosted over Streamable HTTP, nothing to install.

+ +

Endpoint

+ ${esc(MCP_ENDPOINT)} + +

Connect a client

+

Pick a client to see its install command.

+
${clientButtons}
+ ${clientPanels} + +

Tools

+

Four read-only tools, all answering from the catalog this site is serving.

+ ${toolSections} + +
+ Prefer a plain file? The whole catalog, including this MCP server, is described in + /llms.txt and /llms-full.txt. +
+
+ + +`; +} + +function esc(value: string): string { + return value + .replace(/&/g, "&") + .replace(//g, ">"); +} diff --git a/src/data/mcp.ts b/src/data/mcp.ts new file mode 100644 index 00000000..54687736 --- /dev/null +++ b/src/data/mcp.ts @@ -0,0 +1,238 @@ +/** The MCP protocol endpoint. Browsers get the docs page here; machines get the protocol. */ +export const MCP_ENDPOINT = "https://modelparams.dev/mcp"; + +export interface McpClient { + id: string; + name: string; + /** How the install instruction is shown: a one-liner or a JSON config. */ + kind: "cli" | "json"; + command: string; + note?: string; +} + +export const MCP_CLIENTS: McpClient[] = [ + { + id: "claude-code", + name: "Claude Code", + kind: "cli", + command: `claude mcp add --transport http modelparams ${MCP_ENDPOINT}`, + }, + { + id: "codex", + name: "Codex", + kind: "cli", + command: `codex mcp add modelparams --url ${MCP_ENDPOINT}`, + }, + { + id: "opencode", + name: "OpenCode", + kind: "json", + command: JSON.stringify( + { + mcp: { + modelparams: { type: "remote", url: MCP_ENDPOINT, enabled: true }, + }, + }, + null, + 2, + ), + note: "Add to opencode.json under `mcp`.", + }, + { + id: "mcp-spec", + name: "Claude Desktop, Cursor, Windsurf, Zed", + kind: "json", + command: JSON.stringify( + { + mcpServers: { + modelparams: { type: "http", url: MCP_ENDPOINT }, + }, + }, + null, + 2, + ), + note: "Any client that takes a JSON `mcpServers` block.", + }, +]; + +export interface McpTool { + name: string; + title: string; + description: string; + inputSchema: Record; + example: Record; +} + +const STRING = { type: "string" } as const; +const OPTIONAL_STRING = { type: "string" } as const; +const OPTIONAL_INTEGER = { type: "integer", minimum: 1 } as const; + +export const MCP_TOOLS: McpTool[] = [ + { + name: "validate_model_params", + title: "Validate model parameters", + description: + "Check a set of request parameters against what a model actually accepts, before you call it. " + + "Catches unknown parameters, out-of-range values, and combinations the provider rejects — " + + "such as top_p alongside a non-default temperature on Anthropic models. Returns a corrected " + + "`safeParams` payload that is guaranteed to validate.", + inputSchema: { + type: "object", + properties: { + model: { + ...STRING, + description: + "Catalog id or bare model slug, or with baseUrl the wire string your SDK sends.", + }, + baseUrl: { + ...OPTIONAL_STRING, + description: 'The base URL your SDK uses, e.g. "https://api.fireworks.ai/inference/v1".', + }, + params: { + type: "object", + additionalProperties: true, + description: 'Provider-native parameter paths to values, e.g. {"temperature": 0.7}.', + }, + }, + required: ["model"], + additionalProperties: false, + }, + example: { + 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 }, + }, + }, + { + name: "get_model_params", + title: "Get model parameters", + description: + "Every parameter one model accepts — type, allowed range or enum values, default, and the " + + "conditional rules that gate it. Also returns lifecycle status and migration guidance when tracked.", + inputSchema: { + type: "object", + properties: { + model: { + ...STRING, + description: + "Catalog id or bare model slug, or with baseUrl the wire string your SDK sends.", + }, + baseUrl: { ...OPTIONAL_STRING, description: "The base URL your SDK uses." }, + }, + required: ["model"], + additionalProperties: false, + }, + example: { + model: "anthropic/claude-opus-4-8", + provider: "anthropic", + authType: "api_key", + status: "active", + parameterCount: 6, + params: [ + { + path: "temperature", + type: "number", + default: 1, + range: { min: 0, max: 1 }, + group: "sampling", + }, + ], + defaults: { temperature: 1 }, + docs: "https://modelparams.dev/models/anthropic/claude-opus-4-8", + }, + }, + { + name: "list_models", + title: "List catalog models", + description: + "Model ids in the catalog, optionally filtered by provider or a substring. Use it to resolve " + + "the exact id the other tools expect.", + inputSchema: { + type: "object", + properties: { + provider: { + ...OPTIONAL_STRING, + description: 'Restrict to one provider slug, e.g. "anthropic".', + }, + query: { + ...OPTIONAL_STRING, + description: "Case-insensitive substring match on the model id.", + }, + limit: { ...OPTIONAL_INTEGER, description: "Default 100." }, + }, + additionalProperties: false, + }, + example: { + total: 382, + returned: 3, + truncated: false, + providers: ["alibaba", "anthropic", "bedrock"], + models: [ + "anthropic/claude-opus-4-8", + "anthropic/claude-sonnet-4-6", + "anthropic/claude-haiku-4-5", + ], + }, + }, + { + name: "list_provider_models", + title: "List a provider's models and their parameters", + description: + "Everything one provider serves in one call: its models, the wire id each one needs, their " + + "lifecycle status, and the parameter surfaces they expose. Models that share a surface share a " + + "profile, so a provider with dozens of models stays small enough to read. Use it to pick a model " + + "and configure it in one step — especially on Bedrock and Vertex, where parameter paths differ " + + "from the model's native API.", + inputSchema: { + type: "object", + properties: { + provider: { + ...STRING, + description: 'Provider slug, e.g. "bedrock", "vertex", "anthropic".', + }, + query: { + ...OPTIONAL_STRING, + description: "Case-insensitive substring match on the model id.", + }, + limit: { ...OPTIONAL_INTEGER, description: "Default 200." }, + }, + required: ["provider"], + additionalProperties: false, + }, + example: { + provider: "bedrock", + baseUrls: ["https://bedrock-runtime.*.amazonaws.com"], + total: 56, + returned: 56, + truncated: false, + paramProfiles: [ + { + id: "p1", + modelCount: 41, + parameterCount: 4, + params: [{ path: "inferenceConfig.maxTokens", type: "integer", range: { min: 1 } }], + defaults: {}, + }, + ], + models: [ + { + model: "bedrock/claude-opus-4-6", + authType: "api_key", + wireId: "{scope}.anthropic.claude-opus-4-6-v1", + profile: "p2", + }, + ], + wireIdNote: "A wireId containing {scope} needs one substitution before you send it…", + }, + }, +]; diff --git a/src/server/app.ts b/src/server/app.ts index bb52672e..61da811b 100644 --- a/src/server/app.ts +++ b/src/server/app.ts @@ -12,6 +12,7 @@ import { renderProviderPage } from "../build/render-provider.js"; import { renderGlossaryPage } from "../build/render-glossary.js"; import { renderDisambiguationPage } from "../build/render-disambiguation.js"; import { renderApiPage } from "../build/render-api.js"; +import { renderMcpHtml } from "../build/render-mcp.js"; import { SITE_URL } from "../data/site.js"; import { DISAMBIGUATION_PATH } from "../data/urls.js"; import { modelId, type Model } from "../schema/model.js"; @@ -63,6 +64,11 @@ export function makeApp(loadModels: LoadModels): express.Express { } }); + app.get("/mcp", (_req, res) => { + res.setHeader("Cache-Control", "no-store"); + res.type("html").send(renderMcpHtml()); + }); + app.get("/glossary", async (_req, res, next) => { try { const models = await loadModels(); diff --git a/src/views/api.ejs b/src/views/api.ejs index 4eaec354..623d4eb5 100644 --- a/src/views/api.ejs +++ b/src/views/api.ejs @@ -110,27 +110,12 @@
-
+

MCP server

- Let a coding agent check the catalog itself. Hosted at - https://modelparams.dev/mcp, over Streamable HTTP — nothing to install, - and it always answers from the catalog this site is serving. -

-
-
claude mcp add --transport http modelparams https://modelparams.dev/mcp
-codex mcp add modelparams --url https://modelparams.dev/mcp
-
-

- Tools: - validate_model_params, - get_model_params, - list_models, - list_provider_models. -

-

- Using a coding agent that supports skills? Install the companion skill with - npx skills add mnfst/modelparams.dev. + Want your coding agent to check the catalog itself? The MCP server has its own page: + /mcp — install commands, + the four tools, and their schemas.

diff --git a/src/views/index.ejs b/src/views/index.ejs index 1f7fe821..b13667de 100644 --- a/src/views/index.ejs +++ b/src/views/index.ejs @@ -55,7 +55,7 @@
API - MCP server + MCP server Glossary Parameters vs. weights JSON API diff --git a/src/views/partials/header.ejs b/src/views/partials/header.ejs index 0e758acc..c206db6f 100644 --- a/src/views/partials/header.ejs +++ b/src/views/partials/header.ejs @@ -39,7 +39,7 @@ API MCP @@ -102,7 +102,7 @@
API - MCP + MCP GitHub

Providers

From c65bcf578924bf7217e91e3effc4e8b7e44f53af Mon Sep 17 00:00:00 2001 From: Guillaume Gay Date: Fri, 21 Aug 2026 12:29:51 +0200 Subject: [PATCH 3/7] fix: match MCP page to site design Rebuild the MCP page to reuse the site's compiled stylesheet, fonts, header, footer, and dark-mode toggle instead of bespoke inline CSS, so it looks like every other page. Scans src/build for tailwind classes. --- src/build/render-mcp.ts | 340 ++++++++++++++++++++++++---------------- tailwind.config.ts | 2 +- 2 files changed, 209 insertions(+), 133 deletions(-) diff --git a/src/build/render-mcp.ts b/src/build/render-mcp.ts index 84a096fa..c2c6190c 100644 --- a/src/build/render-mcp.ts +++ b/src/build/render-mcp.ts @@ -1,150 +1,223 @@ -import { MCP_CLIENTS, MCP_ENDPOINT, MCP_TOOLS, type McpClient } from "../data/mcp.js"; +import { MCP_CLIENTS, MCP_ENDPOINT, MCP_TOOLS } from "../data/mcp.js"; /** - * The MCP docs page as a fully self-contained HTML document — no external - * stylesheet or script, so the Vercel function that serves the `/mcp` endpoint - * can also answer a browser GET with this page without touching the static - * build's filesystem. + * The MCP docs page. Rendered as a self-contained HTML document that reuses the + * site's compiled stylesheet, fonts, and chrome (header/footer/theme) so it is + * visually identical to every other page — but has no ejs/filesystem dependency, + * so the Vercel function serving `/mcp` can return it for a browser GET. */ export function renderMcpHtml(): string { - const clients = MCP_CLIENTS; - const tools = MCP_TOOLS; + const clientButtons = MCP_CLIENTS.map( + (client, i) => ` + `, + ).join(""); - const clientButtons = clients - .map( - (client, i) => ` - `, - ) - .join(""); - - const clientPanels = clients - .map( - (client, i) => ` -
- ${client.note ? `

${esc(client.note)}

` : ""} -
-
${esc(client.command)}
- + const clientPanels = MCP_CLIENTS.map( + (client, i) => ` +
+ ${client.note ? `

${esc(client.note)}

` : ""} +
+
${esc(client.command)}
+
`, - ) - .join(""); + ).join(""); - const toolSections = tools - .map( - (tool) => ` -
-

${esc(tool.name)}

-

${esc(tool.description)}

-
-
-

Input

-
${esc(JSON.stringify(tool.inputSchema, null, 2))}
-
-
-

Example output

-
${esc(JSON.stringify(tool.example, null, 2))}
-
+ const toolSections = MCP_TOOLS.map( + (tool) => ` +
+

${esc(tool.name)}

+

${esc(tool.description)}

+
+
+

Input

+
${esc(JSON.stringify(tool.inputSchema, null, 2))}
+
+
+

Example output

+
${esc(JSON.stringify(tool.example, null, 2))}
-
`, - ) - .join(""); +
+
`, + ).join(""); return ` - - -MCP server · modelparams.dev - - + + + MCP server · modelparams.dev + + + + + + + + + + + + + + - -
- modelparams.dev / MCP server - ← Back to catalog + + +
+
+ + + modelparams.dev + +
+ + +
+
-
-

MCP server

-

Give your coding agent the catalog, so it can check which parameters a model accepts before it calls one — instead of guessing from training data and eating a 400, or worse, having a parameter silently ignored. Hosted over Streamable HTTP, nothing to install.

-

Endpoint

- ${esc(MCP_ENDPOINT)} +
+ + +
+

MCP server

+

+ Give your coding agent the catalog, so it can check which parameters a model accepts + before it calls one — instead of guessing from training data and eating a 400, or worse, + having a parameter silently ignored. Hosted over Streamable HTTP, nothing to install. +

+ +
+
+

Connect a client

+

+ Endpoint: ${esc(MCP_ENDPOINT)}. Pick a client to see its install command. +

+
${clientButtons}
+
${clientPanels}
+
+ +
+

Tools

+

+ Four read-only tools, all answering from the catalog this site is serving. +

+
${toolSections}
+
-

Connect a client

-

Pick a client to see its install command.

-
${clientButtons}
- ${clientPanels} +
+

For agents

+

+ Prefer a plain file? The whole catalog, including this MCP server, is described in + /llms.txt + and + /llms-full.txt. +

+
+
+
+
-

Tools

-

Four read-only tools, all answering from the catalog this site is serving.

- ${toolSections} + -
- Prefer a plain file? The whole catalog, including this MCP server, is described in - /llms.txt and /llms-full.txt. -
-
diff --git a/src/data/mcp.ts b/src/data/mcp.ts index 4b29e5f7..b9945df3 100644 --- a/src/data/mcp.ts +++ b/src/data/mcp.ts @@ -123,17 +123,16 @@ export const MCP_TOOLS: McpTool[] = [ properties: { model: { ...STRING, - description: - "Catalog id or bare model slug, or with baseUrl the wire string your SDK sends.", + description: "Catalog id, bare slug, or wire string with baseUrl.", }, baseUrl: { ...OPTIONAL_STRING, - description: 'The base URL your SDK uses, e.g. "https://api.fireworks.ai/inference/v1".', + description: "The base URL your SDK uses.", }, params: { type: "object", additionalProperties: true, - description: 'Provider-native parameter paths to values, e.g. {"temperature": 0.7}.', + description: "Provider-native parameter paths to values.", }, }, required: ["model"], @@ -166,8 +165,7 @@ export const MCP_TOOLS: McpTool[] = [ properties: { model: { ...STRING, - description: - "Catalog id or bare model slug, or with baseUrl the wire string your SDK sends.", + description: "Catalog id, bare slug, or wire string with baseUrl.", }, baseUrl: { ...OPTIONAL_STRING, description: "The base URL your SDK uses." }, }, diff --git a/src/views/api.ejs b/src/views/api.ejs index 4985080b..daa79c43 100644 --- a/src/views/api.ejs +++ b/src/views/api.ejs @@ -86,7 +86,7 @@ -d '{"model":"anthropic/claude-3-opus-20240229","params":{"temperature":0.5,"top_p":0.9}}'
-
<%- helpers.highlightJson(`{
+        
<%- helpers.highlightJson(`{
   "model": "anthropic/claude-3-opus-20240229",
   "valid": false,
   "issues": [

From a3cfba89bba9ca6b423f2eed0f5355d9408d629c Mon Sep 17 00:00:00 2001
From: Guillaume Gay 
Date: Fri, 21 Aug 2026 14:41:19 +0200
Subject: [PATCH 7/7] refactor: drop content negotiation, split docs page from
 endpoint
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Keep the MCP protocol endpoint at /mcp (GET returns the JSON usage
object, like other hosted MCP servers). Move the docs to /mcp-server as
a normal static page with the full site chrome — header with provider
menu, footer, breadcrumbs — instead of a self-contained function-served
page. Relabel the catch-all client as 'Other'.
---
 .fieldflow/inspect/fde3fce0530fad3c.json |  75 ++++++
 api/mcp.ts                               |  14 +-
 src/build/build.ts                       |   4 +
 src/build/render-mcp.ts                  | 301 +++--------------------
 src/client/main.ts                       |  72 ++++++
 src/data/mcp.ts                          |   2 +-
 src/data/urls.ts                         |   3 +
 src/server/app.ts                        |  13 +-
 src/views/api.ejs                        |   2 +-
 src/views/index.ejs                      |   2 +-
 src/views/mcp.ejs                        |  99 ++++++++
 src/views/partials/footer.ejs            |   2 +-
 src/views/partials/header.ejs            |   4 +-
 13 files changed, 302 insertions(+), 291 deletions(-)
 create mode 100644 .fieldflow/inspect/fde3fce0530fad3c.json
 create mode 100644 src/views/mcp.ejs

diff --git a/.fieldflow/inspect/fde3fce0530fad3c.json b/.fieldflow/inspect/fde3fce0530fad3c.json
new file mode 100644
index 00000000..8bd50824
--- /dev/null
+++ b/.fieldflow/inspect/fde3fce0530fad3c.json
@@ -0,0 +1,75 @@
+{
+  "manifest_version": 1,
+  "command": [
+    "gh",
+    "issue",
+    "list",
+    "--repo",
+    "mnfst/modelparams.dev",
+    "--state",
+    "open",
+    "--limit",
+    "50",
+    "--json",
+    "number,title,labels,url,updatedAt,createdAt,body"
+  ],
+  "command_hash": "fde3fce0530fad3c",
+  "root_type": "list",
+  "paths": [
+    {
+      "path": "[]",
+      "types": ["object"]
+    },
+    {
+      "path": "[].body",
+      "types": ["string"]
+    },
+    {
+      "path": "[].createdAt",
+      "types": ["string"]
+    },
+    {
+      "path": "[].labels",
+      "types": ["list"]
+    },
+    {
+      "path": "[].labels[]",
+      "types": ["object"]
+    },
+    {
+      "path": "[].labels[].color",
+      "types": ["string"]
+    },
+    {
+      "path": "[].labels[].description",
+      "types": ["string"]
+    },
+    {
+      "path": "[].labels[].id",
+      "types": ["string"]
+    },
+    {
+      "path": "[].labels[].name",
+      "types": ["string"]
+    },
+    {
+      "path": "[].number",
+      "types": ["integer"]
+    },
+    {
+      "path": "[].title",
+      "types": ["string"]
+    },
+    {
+      "path": "[].updatedAt",
+      "types": ["string"]
+    },
+    {
+      "path": "[].url",
+      "types": ["string"]
+    }
+  ],
+  "path_count": 13,
+  "input_items": 8,
+  "sampled_items": 8
+}
diff --git a/api/mcp.ts b/api/mcp.ts
index 8d0687e4..b0f9b8b2 100644
--- a/api/mcp.ts
+++ b/api/mcp.ts
@@ -13,7 +13,6 @@
 import type { JSONRPCMessage } from "@modelcontextprotocol/sdk/types.js";
 import type { Transport } from "@modelcontextprotocol/sdk/shared/transport.js";
 import { createServer } from "../packages/modelparams-mcp/src/server.js";
-import { renderMcpHtml } from "../src/build/render-mcp.js";
 
 const MAX_BODY_BYTES = 256 * 1024;
 
@@ -124,21 +123,12 @@ export default {
 
     // A GET is either an MCP client opening the optional server-to-client
     // stream, which a stateless server has nothing to put on, or a person
-    // opening the URL. Answer each in its own terms: browsers get the docs
-    // page, event-stream clients get a 405, and plain API clients get JSON.
+    // opening the URL. Answer each in its own terms: event-stream clients get
+    // a 405, everyone else gets the JSON usage object.
     if (request.method === "GET") {
       if ((request.headers.get("accept") ?? "").includes("text/event-stream")) {
         return json({ error: "streaming_not_supported", usage: USAGE }, 405, { Allow: "POST" });
       }
-      if ((request.headers.get("accept") ?? "").includes("text/html")) {
-        return new Response(renderMcpHtml(), {
-          status: 200,
-          headers: {
-            "Content-Type": "text/html; charset=utf-8",
-            "Cache-Control": "no-store",
-          },
-        });
-      }
       return json(USAGE);
     }
 
diff --git a/src/build/build.ts b/src/build/build.ts
index 086a28ee..93593e63 100644
--- a/src/build/build.ts
+++ b/src/build/build.ts
@@ -23,6 +23,7 @@ import {
   API_PATH,
   DISAMBIGUATION_PATH,
   GLOSSARY_PATH,
+  MCP_PATH,
   modelPagePath,
   parameterPagePath,
   providerPagePath,
@@ -36,6 +37,7 @@ import { renderIndex } from "./render.js";
 import { renderApiPage } from "./render-api.js";
 import { renderDisambiguationPage } from "./render-disambiguation.js";
 import { renderGlossaryPage } from "./render-glossary.js";
+import { renderMcpPage } from "./render-mcp.js";
 import { renderModelPage } from "./render-model.js";
 import { defaultSummary,renderParameterPage, rangeSummary } from "./render-parameter.js";
 import { renderNotFoundPage } from "./render-not-found.js";
@@ -80,6 +82,7 @@ async function writeRobotsAndSitemap(models: Model[]): Promise {
     { path: GLOSSARY_PATH, priority: "0.7", lastmod: freshest(models) },
     { path: DISAMBIGUATION_PATH, priority: "0.6", lastmod: freshest(models) },
     { path: API_PATH, priority: "0.5", lastmod: freshest(models) },
+    { path: MCP_PATH, priority: "0.5", lastmod: freshest(models) },
     ...uniqueProviders(models).map((provider) => ({
       path: providerPagePath(provider),
       priority: "0.8",
@@ -133,6 +136,7 @@ async function writeHtmlPages(models: Model[]): Promise {
     "utf8",
   );
   await fs.writeFile(path.join(DIST_DIR, "api.html"), await renderApiPage(models), "utf8");
+  await fs.writeFile(path.join(DIST_DIR, "mcp-server.html"), await renderMcpPage(models), "utf8");
   await fs.writeFile(path.join(DIST_DIR, "404.html"), await renderNotFoundPage(models), "utf8");
 }
 
diff --git a/src/build/render-mcp.ts b/src/build/render-mcp.ts
index a98e9678..357a19dc 100644
--- a/src/build/render-mcp.ts
+++ b/src/build/render-mcp.ts
@@ -1,271 +1,34 @@
-import { MCP_CLIENTS, MCP_ENDPOINT, MCP_TOOLS } from "../data/mcp.js";
+import path from "node:path";
+import ejs from "ejs";
+import { MCP_CLIENTS, MCP_TOOLS } from "../data/mcp.js";
+import { VIEWS_DIR } from "../data/paths.js";
+import { SITE_NAME, SITE_URL } from "../data/site.js";
+import { MCP_PATH, absolute, ogImagePath } from "../data/urls.js";
+import { type Model } from "../schema/model.js";
 import { jsonBlock } from "./highlight.js";
-
-/**
- * The MCP docs page. Rendered as a self-contained HTML document that reuses the
- * site's compiled stylesheet, fonts, and chrome (header/footer/theme) so it is
- * visually identical to every other page — but has no ejs/filesystem dependency,
- * so the Vercel function serving `/mcp` can return it for a browser GET.
- */
-export function renderMcpHtml(): string {
-  const clientButtons = MCP_CLIENTS.map(
-    (client, i) => `
-        `,
-  ).join("");
-
-  const clientPanels = MCP_CLIENTS.map(
-    (client, i) => `
-        
- ${client.note ? `

${esc(client.note)}

` : ""} -
-
${esc(client.command)}
- -
-
`, - ).join(""); - - const toolSections = MCP_TOOLS.map( - (tool) => ` -
-

${esc(tool.name)}

-

${esc(tool.description)}

-
- ${jsonBlock(JSON.stringify(tool.inputSchema, null, 2), "Input")} - ${jsonBlock(JSON.stringify(tool.example, null, 2), "Example output")} -
-
`, - ).join(""); - - return ` - - - - - MCP server · modelparams.dev - - - - - - - - - - - - - - - - - -
-
- - - modelparams.dev - -
- - -
-
-
- -
- - -
-

MCP server

-

- Give your coding agent the catalog, so it can check which parameters a model accepts - before it calls one — instead of guessing from training data and eating a 400, or worse, - having a parameter silently ignored. Hosted over Streamable HTTP, nothing to install. -

- -
-
-

Connect a client

-

- Endpoint: ${esc(MCP_ENDPOINT)}. Pick a client to see its install command. -

-
${clientButtons}
-
${clientPanels}
-
- -
-

Tools

-

- Four read-only tools, all answering from the catalog this site is serving. -

-
${toolSections}
-
- -
-

For agents

-

- Prefer a plain file? The whole catalog, including this MCP server, is described in - /llms.txt - and - /llms-full.txt. -

-
-
-
-
- - - - - -`; -} - -function esc(value: string): string { - return value - .replace(/&/g, "&") - .replace(//g, ">"); +import { hubLinks, renderShell, viewHelpers } from "./render.js"; + +const MCP_TITLE = `MCP server · ${SITE_NAME}`; +const MCP_DESCRIPTION = + "A Model Context Protocol server for the modelparams.dev catalog — let a coding agent check which parameters a model accepts before it calls one."; + +export async function renderMcpPage(allModels: Model[]): Promise { + const body = await ejs.renderFile(path.join(VIEWS_DIR, "mcp.ejs"), { + clients: MCP_CLIENTS, + tools: MCP_TOOLS, + jsonBlock, + helpers: viewHelpers, + }); + + return renderShell( + { + title: MCP_TITLE, + description: MCP_DESCRIPTION, + canonicalUrl: absolute(SITE_URL, MCP_PATH), + ogImage: ogImagePath(MCP_PATH), + structuredData: "{}", + providerHubs: hubLinks(allModels), + }, + body, + ); } diff --git a/src/client/main.ts b/src/client/main.ts index d117537e..b86edf4d 100644 --- a/src/client/main.ts +++ b/src/client/main.ts @@ -117,6 +117,76 @@ function setupCopyNpm(): void { }); } +function setupMcpClients(): void { + const buttons = document.querySelectorAll("[data-mcp-client]"); + const panels = document.querySelectorAll("[data-mcp-panel]"); + if (buttons.length === 0) return; + + const activeClass = [ + "border-slate-900", + "bg-slate-900", + "text-white", + "dark:border-white", + "dark:bg-white", + "dark:text-slate-900", + ]; + const idleClass = [ + "border-slate-300", + "text-slate-700", + "hover:border-slate-500", + "dark:border-slate-700", + "dark:text-slate-300", + ]; + + buttons.forEach((button) => { + button.addEventListener("click", () => { + const id = button.dataset.mcpClient; + buttons.forEach((b) => { + const on = b === button; + b.classList.remove(...(on ? idleClass : activeClass)); + b.classList.add(...(on ? activeClass : idleClass)); + }); + panels.forEach((panel) => panel.classList.toggle("hidden", panel.dataset.mcpPanel !== id)); + }); + }); + + const copyButtons = document.querySelectorAll("[data-copy-mcp]"); + copyButtons.forEach((button) => { + const idle = button.querySelector("[data-copy-mcp-idle]"); + const done = button.querySelector("[data-copy-mcp-done]"); + let timer = 0; + button.addEventListener("click", async () => { + const code = document.querySelector( + `[data-mcp-command="${button.dataset.copyMcp}"]`, + ); + if (!code) return; + await copyText(code.textContent?.trim() ?? ""); + idle?.classList.add("hidden"); + done?.classList.remove("hidden"); + window.clearTimeout(timer); + timer = window.setTimeout(() => { + idle?.classList.remove("hidden"); + done?.classList.add("hidden"); + }, 2000); + }); + }); +} + +function setupJsonCopy(): void { + document.querySelectorAll("[data-json-copy]").forEach((button) => { + button.addEventListener("click", async () => { + const code = button.closest("figure")?.querySelector("code"); + if (!code) return; + await copyText(code.textContent?.trim() ?? ""); + const original = button.textContent; + button.textContent = "Copied"; + window.setTimeout(() => { + button.textContent = original; + }, 2000); + }); + }); +} + function setupThemeToggle(): void { const toggle = document.querySelector("[data-theme-toggle]"); if (!toggle) return; @@ -534,6 +604,8 @@ function setupMobileMenu(): void { document.addEventListener("DOMContentLoaded", () => { setupThemeToggle(); setupCopyNpm(); + setupMcpClients(); + setupJsonCopy(); setupHowToUseModal(); setupCopyHowToUse(); setupProvidersMenu(); diff --git a/src/data/mcp.ts b/src/data/mcp.ts index b9945df3..479788a6 100644 --- a/src/data/mcp.ts +++ b/src/data/mcp.ts @@ -89,7 +89,7 @@ export const MCP_CLIENTS: McpClient[] = [ }, { id: "other", - name: "Claude Desktop, Zed, …", + name: "Other", logo: GENERIC_LOGO, kind: "json", command: MCP_SERVERS_JSON, diff --git a/src/data/urls.ts b/src/data/urls.ts index e7966b8d..02798592 100644 --- a/src/data/urls.ts +++ b/src/data/urls.ts @@ -22,6 +22,9 @@ export const GLOSSARY_PATH = "/glossary"; /** API documentation page. The HTML docs, not the JSON endpoints under /api/v1. */ export const API_PATH = "/api"; +/** MCP server documentation page. The protocol endpoint itself lives at /mcp. */ +export const MCP_PATH = "/mcp-server"; + /** * The page that separates the two meanings of "model parameters": weight count * versus the request settings this catalog documents. Every other page links diff --git a/src/server/app.ts b/src/server/app.ts index 61da811b..590f5d9d 100644 --- a/src/server/app.ts +++ b/src/server/app.ts @@ -12,7 +12,7 @@ import { renderProviderPage } from "../build/render-provider.js"; import { renderGlossaryPage } from "../build/render-glossary.js"; import { renderDisambiguationPage } from "../build/render-disambiguation.js"; import { renderApiPage } from "../build/render-api.js"; -import { renderMcpHtml } from "../build/render-mcp.js"; +import { renderMcpPage } from "../build/render-mcp.js"; import { SITE_URL } from "../data/site.js"; import { DISAMBIGUATION_PATH } from "../data/urls.js"; import { modelId, type Model } from "../schema/model.js"; @@ -64,9 +64,14 @@ export function makeApp(loadModels: LoadModels): express.Express { } }); - app.get("/mcp", (_req, res) => { - res.setHeader("Cache-Control", "no-store"); - res.type("html").send(renderMcpHtml()); + app.get("/mcp-server", async (_req, res, next) => { + try { + const models = await loadModels(); + res.setHeader("Cache-Control", "no-store"); + res.type("html").send(await renderMcpPage(models)); + } catch (err) { + next(err); + } }); app.get("/glossary", async (_req, res, next) => { diff --git a/src/views/api.ejs b/src/views/api.ejs index daa79c43..a16b8784 100644 --- a/src/views/api.ejs +++ b/src/views/api.ejs @@ -114,7 +114,7 @@

MCP server

Want your coding agent to check the catalog itself? The MCP server has its own page: - /mcp — install commands, + /mcp-server — install commands, the four tools, and their schemas.

diff --git a/src/views/index.ejs b/src/views/index.ejs index b13667de..cc90c019 100644 --- a/src/views/index.ejs +++ b/src/views/index.ejs @@ -55,7 +55,7 @@

MCP server

+

+ Give your coding agent the catalog, so it can check which parameters a model accepts + before it calls one — instead of guessing from training data and eating a 400, or worse, + having a parameter silently ignored. Hosted over Streamable HTTP, nothing to install. +

+ +
+ diff --git a/src/views/partials/footer.ejs b/src/views/partials/footer.ejs index 5be46441..c4904f9c 100644 --- a/src/views/partials/footer.ejs +++ b/src/views/partials/footer.ejs @@ -20,7 +20,7 @@
API - MCP server + MCP server Glossary Parameters vs. weights JSON API diff --git a/src/views/partials/header.ejs b/src/views/partials/header.ejs index c206db6f..f2697c10 100644 --- a/src/views/partials/header.ejs +++ b/src/views/partials/header.ejs @@ -39,7 +39,7 @@ API MCP @@ -102,7 +102,7 @@
API - MCP + MCP GitHub

Providers