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 b9939cc9..b0f9b8b2 100644 --- a/api/mcp.ts +++ b/api/mcp.ts @@ -123,7 +123,8 @@ 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: 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" }); 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/highlight.ts b/src/build/highlight.ts new file mode 100644 index 00000000..3852ef68 --- /dev/null +++ b/src/build/highlight.ts @@ -0,0 +1,43 @@ +/** Escape HTML special characters so highlighted JSON can be injected into markup. */ +function escapeHtml(value: string): string { + return value.replace(/&/g, "&").replace(//g, ">"); +} + +/** + * Minimal JSON syntax highlighter. Returns HTML where each token is wrapped in a + * span with a Tailwind text color, tuned for the site's dark code blocks + * (`bg-slate-900`). Punctuation is left in the surrounding text color. + * + * Colours: keys sky, strings emerald, numbers violet, booleans amber, null rose. + */ +export function highlightJson(json: string): string { + const token = + /("(?:\\u[a-zA-Z0-9]{4}|\\[^u]|[^\\"])*"(?:\s*:)?|\b(?:true|false|null)\b|-?\d+(?:\.\d*)?(?:[eE][+-]?\d+)?)/g; + + return json.replace(token, (match) => { + let cls = "text-violet-300"; + if (/^"/.test(match)) { + cls = /:$/.test(match) ? "text-sky-300" : "text-emerald-300"; + } else if (/^(true|false)$/.test(match)) { + cls = "text-amber-300"; + } else if (match === "null") { + cls = "text-rose-300"; + } + return `${escapeHtml(match)}`; + }); +} + +/** + * A self-contained code block for JSON: a header bar with a label and a Copy + * button, and a body that wraps long lines instead of overflowing. The copy + * button is wired by `[data-json-copy]`; it copies the sibling `` text. + */ +export function jsonBlock(json: string, label = "JSON"): string { + return `
+
+ ${escapeHtml(label)} + +
+
${highlightJson(json)}
+
`; +} diff --git a/src/build/render-mcp.ts b/src/build/render-mcp.ts new file mode 100644 index 00000000..357a19dc --- /dev/null +++ b/src/build/render-mcp.ts @@ -0,0 +1,34 @@ +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"; +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/build/render.ts b/src/build/render.ts index 69da773b..5f6805f4 100644 --- a/src/build/render.ts +++ b/src/build/render.ts @@ -27,6 +27,7 @@ import { } from "../data/urls.js"; import { modelId, type Catalog, type Model } from "../schema/model.js"; import { fitDescription, fitTitle } from "./meta.js"; +import { highlightJson } from "./highlight.js"; import { buildHomeStructuredData } from "./structured-data.js"; const LAYOUT_PATH = path.join(VIEWS_DIR, "layout.ejs"); @@ -49,6 +50,7 @@ export const viewHelpers = { parameterPagePath, parameterAnchorId, providerPagePath, + highlightJson, }; export interface HubLink { 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 new file mode 100644 index 00000000..479788a6 --- /dev/null +++ b/src/data/mcp.ts @@ -0,0 +1,278 @@ +/** 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; + /** Inline SVG logo (monochrome `currentColor` or brand colors). */ + logo: string; + /** How the install instruction is shown: a one-liner or a JSON config. */ + kind: "cli" | "json"; + command: string; + note?: string; +} + +const CLAUDE_CODE_LOGO = + ''; + +const CODEX_LOGO = + ''; + +const OPENCODE_LOGO = + ''; + +const CURSOR_LOGO = + ''; + +const WINDSURF_LOGO = + ''; + +const GENERIC_LOGO = + ''; + +const MCP_SERVERS_JSON = JSON.stringify( + { + mcpServers: { + modelparams: { type: "http", url: MCP_ENDPOINT }, + }, + }, + null, + 2, +); + +export const MCP_CLIENTS: McpClient[] = [ + { + id: "claude-code", + name: "Claude Code", + logo: CLAUDE_CODE_LOGO, + kind: "cli", + command: `claude mcp add --transport http modelparams ${MCP_ENDPOINT}`, + }, + { + id: "codex", + name: "Codex", + logo: CODEX_LOGO, + kind: "cli", + command: `codex mcp add modelparams --url ${MCP_ENDPOINT}`, + }, + { + id: "opencode", + name: "OpenCode", + logo: OPENCODE_LOGO, + kind: "json", + command: JSON.stringify( + { + mcp: { + modelparams: { type: "remote", url: MCP_ENDPOINT, enabled: true }, + }, + }, + null, + 2, + ), + note: "Add to opencode.json under `mcp`.", + }, + { + id: "cursor", + name: "Cursor", + logo: CURSOR_LOGO, + kind: "json", + command: MCP_SERVERS_JSON, + note: "Cursor Settings → MCP → add a server.", + }, + { + id: "windsurf", + name: "Windsurf", + logo: WINDSURF_LOGO, + kind: "json", + command: MCP_SERVERS_JSON, + note: "Any client that takes a JSON `mcpServers` block.", + }, + { + id: "other", + name: "Other", + logo: GENERIC_LOGO, + kind: "json", + command: MCP_SERVERS_JSON, + 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, bare slug, or wire string with baseUrl.", + }, + baseUrl: { + ...OPTIONAL_STRING, + description: "The base URL your SDK uses.", + }, + params: { + type: "object", + additionalProperties: true, + description: "Provider-native parameter paths to values.", + }, + }, + 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, bare slug, or wire string with baseUrl.", + }, + 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/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 bb52672e..590f5d9d 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 { 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"; @@ -63,6 +64,16 @@ export function makeApp(loadModels: LoadModels): express.Express { } }); + 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) => { try { const models = await loadModels(); diff --git a/src/views/api.ejs b/src/views/api.ejs index 2a53b801..a16b8784 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(`{
   "model": "anthropic/claude-3-opus-20240229",
   "valid": false,
   "issues": [
@@ -98,7 +98,7 @@
     }
   ],
   "safeParams": { "temperature": 0.5 }
-}
+}`) %>

Every issue carries a @@ -113,24 +113,9 @@

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-server — install commands, + the four tools, and their schemas.

diff --git a/src/views/index.ejs b/src/views/index.ejs index 8abd1a5f..cc90c019 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/mcp.ejs b/src/views/mcp.ejs new file mode 100644 index 00000000..29d0269a --- /dev/null +++ b/src/views/mcp.ejs @@ -0,0 +1,99 @@ +<%- include("partials/breadcrumbs", { items: [ + { name: "Home", href: "/" }, + { name: "MCP" } +] }) %> + +
+

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: + https://modelparams.dev/mcp. + Pick a client to see its install command. +

+ +
+ <% for (const client of clients) { %> + + <% } %> +
+ +
+ <% for (const client of clients) { %> +
+ <% if (client.note) { %> +

<%= client.note %>

+ <% } %> +
+
<%= client.command %>
+ +
+
+ <% } %> +
+
+ + +
+

Tools

+

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

+ +
+ <% for (const tool of tools) { %> +
+

<%= tool.name %>

+

<%= tool.description %>

+ +
+ <%- jsonBlock(JSON.stringify(tool.inputSchema, null, 2), "Input") %> + <%- jsonBlock(JSON.stringify(tool.example, null, 2), "Example output") %> +
+
+ <% } %> +
+
+ + +
+

For agents

+

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

+
+
+
diff --git a/src/views/partials/footer.ejs b/src/views/partials/footer.ejs index ad4e4e68..c4904f9c 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..f2697c10 100644 --- a/src/views/partials/header.ejs +++ b/src/views/partials/header.ejs @@ -38,6 +38,12 @@ > API + + MCP +
API + MCP GitHub

Providers

diff --git a/tailwind.config.ts b/tailwind.config.ts index 406194de..be8519fb 100644 --- a/tailwind.config.ts +++ b/tailwind.config.ts @@ -11,7 +11,7 @@ import type { Config } from "tailwindcss"; */ const config: Config = { - content: ["./src/views/**/*.ejs", "./src/client/**/*.ts"], + content: ["./src/views/**/*.ejs", "./src/client/**/*.ts", "./src/build/**/*.ts"], darkMode: "class", theme: { extend: {