Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@ claude mcp add --transport http modelparams https://modelparams.dev/mcp
codex mcp add modelparams --url https://modelparams.dev/mcp
```

Four tools: `validate_model_params` to check a params object before you send it, `get_model_params` for one model's parameter surface and lifecycle metadata, `list_models` and `find_models_supporting` to search the catalog.
Four tools: `validate_model_params` to check a params object before you send it, `get_model_params` for one model's parameter surface and lifecycle metadata, `list_provider_models` for everything one provider serves, and `list_models` to search the catalog.

**Without MCP** — `npx skills add mnfst/modelparams.dev` installs the companion agent skill, and [llms.txt](https://modelparams.dev/llms.txt) points an agent at a URL.

Expand Down
2 changes: 1 addition & 1 deletion api/mcp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ const USAGE = {
"parameters a model accepts, and validates a params object before you send it.",
transport: "streamable-http",
stateless: true,
tools: ["validate_model_params", "get_model_params", "list_models", "find_models_supporting"],
tools: ["validate_model_params", "get_model_params", "list_models", "list_provider_models"],
install: {
"claude-code": "claude mcp add --transport http modelparams https://modelparams.dev/mcp",
codex: "codex mcp add modelparams --url https://modelparams.dev/mcp",
Expand Down
40 changes: 38 additions & 2 deletions packages/modelparams-mcp/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,9 +83,45 @@ Every parameter for one model — type, range, enum values, default, and the con

Model ids, filtered by provider or substring. Use it to resolve the exact catalog id.

### `find_models_supporting`
### `list_provider_models`

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.
Everything one provider serves, in one call: its models, the `wireId` each one needs, their lifecycle status, and the parameter surfaces they expose. For picking a model *and* configuring it in the same step — the question you have when a provider is fixed and the model isn't, which is the normal state of affairs on Bedrock and Vertex.

Models are grouped by parameter surface rather than listed one by one, because a provider's models are far less varied than their count suggests — Bedrock's 56 models are four distinct surfaces, since Converse normalises most of them:

```json
{ "provider": "bedrock" }
```

```json
{
"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…"
}
```

That grouping is what makes the whole provider fit in a tool result: flat, Bedrock's parameters are roughly an order of magnitude larger than the profile form.

## Model ids

Expand Down
27 changes: 17 additions & 10 deletions packages/modelparams-mcp/src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { CATALOG } from "modelparams";
import { z } from "zod";
import {
findModelsSupporting,
getModelParams,
listCatalogModels,
listProviderModels,
validateModelParams,
type ToolPayload,
} from "./tools.js";
Expand Down Expand Up @@ -116,21 +116,28 @@ export function createServer(version = "0.0.0"): McpServer {
);

server.registerTool(
"find_models_supporting",
"list_provider_models",
{
title: "Find models supporting a parameter",
title: "List a provider's models and their parameters",
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.",
"Everything one provider serves: its models, the wire id each one needs, their " +
"lifecycle status, and the parameter surfaces they expose — types, ranges, enum values, " +
"defaults, and conditional rules — in a single call. Models that share a parameter " +
"surface share a profile, so a provider with dozens of models stays small enough to " +
"read. Use this to pick a model and configure it in one step, especially on Bedrock and " +
"Vertex where the parameter paths differ from the underlying model's native API.",
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."),
provider: z
.string()
.describe(
'Provider slug, e.g. "bedrock", "vertex", "anthropic". Call list_models with no arguments to see them all.',
),
query: z.string().optional().describe("Case-insensitive substring match on the model id."),
limit: z.number().int().positive().max(1000).optional().describe("Default 200."),
},
annotations: READ_ONLY,
},
async (args) => reply(findModelsSupporting(args)),
async (args) => reply(listProviderModels(args)),
);

return server;
Expand Down
131 changes: 91 additions & 40 deletions packages/modelparams-mcp/src/tools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
getDefaults,
getModel,
listModels,
PROVIDER_ENDPOINTS,
PROVIDERS,
resolveByBaseUrl,
resolveModelId,
Expand Down Expand Up @@ -181,54 +182,104 @@ export function listCatalogModels(input: {
};
}

/** Which models expose a given knob — the "can I use X anywhere?" question. */
export function findModelsSupporting(input: {
parameter: string;
provider?: string;
/**
* Every parameter surface a provider serves, and which models share each one.
*
* A provider's models are far less varied than their count suggests: Bedrock's
* 56 models are four distinct parameter surfaces, because Converse normalises
* most of them. Returning one profile per surface, with the models pointing at
* it, is what makes "give me everything you have on this provider" fit in a
* tool result at all — flat, Bedrock is an order of magnitude larger.
*/
export function listProviderModels(input: {
provider: string;
query?: 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 });
}
const { provider, query, limit = 200 } = input;

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<string>();
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);
}
}
}
if (!PROVIDERS.includes(provider as Provider)) {
return {
parameter,
total: 0,
message: `No model in the catalog accepts "${parameter}".`,
similarParameters: [...known].sort().slice(0, 10),
error: "unknown_provider",
message: `"${provider}" is not a provider in the catalog.`,
providers: PROVIDERS,
};
}

let entries = CATALOG.filter((e) => e.provider === provider);
if (query) {
const needle = query.toLowerCase();
entries = entries.filter((e) => modelIdOf(e).toLowerCase().includes(needle));
}

const total = entries.length;
const shown = entries.slice(0, limit);

// Group by parameter surface. Codegen emits params in a stable order, so the
// serialised array is a sound identity for "same surface".
const byShape = new Map<string, { params: readonly Param[]; entries: typeof shown }>();
for (const entry of shown) {
const params = entry.params as readonly Param[];
const shape = JSON.stringify(params);
const bucket = byShape.get(shape);
if (bucket) bucket.entries.push(entry);
else byShape.set(shape, { params, entries: [entry] });
}

// Commonest surface first, then alphabetically — so `p1` is the provider's
// house style and the ids stay stable across identical calls.
const profiles = [...byShape.values()].sort(
(a, b) =>
b.entries.length - a.entries.length ||
modelIdOf(a.entries[0]!).localeCompare(modelIdOf(b.entries[0]!)),
);
const profileOf = new Map<string, string>();
profiles.forEach((profile, i) => {
for (const entry of profile.entries) profileOf.set(modelIdOf(entry), `p${i + 1}`);
});

const models = shown.map((entry) => {
const id = modelIdOf(entry);
return {
model: id,
authType: entry.authType,
...("wireId" in entry ? { wireId: entry.wireId } : {}),
// Lifecycle travels with the row: a tool whose whole job is helping an
// agent choose a model must not hand back a retired one unmarked.
...(entry.status !== undefined ? { status: entry.status } : {}),
...(entry.replacement !== undefined ? { replacement: entry.replacement } : {}),
...(entry.shutdownOn !== undefined ? { shutdownOn: entry.shutdownOn } : {}),
profile: profileOf.get(id),
};
});

// Bedrock reaches most models through a cross-region inference profile, so the
// wire id carries a placeholder the caller has to fill in. Say so, in the
// response that hands over those ids, rather than leaving it to the docs.
const templated = shown.some((e) => "wireId" in e && e.wireId.includes("{scope}"));

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 } : {}),
provider,
baseUrls: PROVIDER_ENDPOINTS[provider as keyof typeof PROVIDER_ENDPOINTS],
total,
returned: shown.length,
truncated: total > shown.length,
paramProfiles: profiles.map((profile, i) => ({
id: `p${i + 1}`,
modelCount: profile.entries.length,
parameterCount: profile.params.length,
params: profile.params.map(describeParam),
defaults: getDefaults(modelIdOf(profile.entries[0]!) as ModelId),
})),
docs: `https://modelparams.dev/parameters/${parameter.replace(/\./g, "-")}`,
models,
...(templated
? {
wireIdNote:
"A wireId containing {scope} needs one substitution before you send it: the " +
"routing geography of the inference profile — us, eu, apac, jp, au, ca, sa, or " +
"global. A wireId without the placeholder is already complete.",
}
: {}),
docs: `https://modelparams.dev/providers/${provider}`,
};
}
Loading
Loading