skip to content
MCP and plugins

SDK

The MCP server

Put the whole model catalog in front of the agent that is already driving your editor.


Your coding agent runs on one model. That is usually the right call for the session, and usually the wrong call for at least one step in it: the bulk rewrite that wants something cheap, the design question that wants something that reasons, the docstring where you would honestly like a second opinion.

The MCP server gives the agent a door to every model on the gateway, mid-task, without changing what drives the session. And because every call comes back with its exact settled cost, the agent can be told things like “use the cheap model unless the diff touches auth” and actually held to it.

Build it once

The server ships in the SDK package. Build it, note the path:

terminal
git clone https://github.com/ConiferKit/use-conifer
cd use-conifer && npm install && npm run build

Add it to your editor

Every MCP client wants the same three facts — a command, its arguments, and the key — spelled in its own config format. Pick yours:

Claude Code

One command:

terminal
claude mcp add conifer --env CONIFER_API_KEY=sk-conifer--- node /path/to/use-conifer/bin/conifer-mcp.mjs

Or add it to .mcp.json in the project root (this is the file claude mcp add --scope project writes):

.mcp.json
{
  "mcpServers": {
    "conifer": {
      "command": "node",
      "args": ["/path/to/use-conifer/bin/conifer-mcp.mjs"],
      "env": { "CONIFER_API_KEY": "sk-conifer-…" }
    }
  }
}

claude mcp list should show conifer … ✔ Connected. Then try: “use conifer_compare to try this function’s docstring on claude-haiku-4-5 and gpt-oss-20b”.

Claude Desktop

The chat app is a different program from Claude Code, with a different config file. Open Settings → Developer → Edit Config — that opens claude_desktop_config.json — and add:

claude_desktop_config.json
{
  "mcpServers": {
    "conifer": {
      "command": "/absolute/path/to/node",
      "args": ["/path/to/use-conifer/bin/conifer-mcp.mjs"],
      "env": { "CONIFER_API_KEY": "sk-conifer-…" }
    }
  }
}

Fully restart the app (quit, not just close the window). The tools appear under the search-and-tools icon in the chat box. This is the config where the absolute node path matters most — the desktop app launches with no shell environment at all.

Codex

~/.codex/config.toml
[mcp_servers.conifer]
command = "node"
args = ["/path/to/use-conifer/bin/conifer-mcp.mjs"]
env = { "CONIFER_API_KEY" = "sk-conifer-…" }

Codex reads the block at startup; restart it after editing. The env line matters — Codex does not pass your shell environment to MCP servers unless told to.

Cursor

Create .cursor/mcp.json in the project (or ~/.cursor/mcp.json for every project), then enable the server under Settings → Cursor Settings → Tools & MCP:

.cursor/mcp.json
{
  "mcpServers": {
    "conifer": {
      "command": "node",
      "args": ["/path/to/use-conifer/bin/conifer-mcp.mjs"],
      "env": { "CONIFER_API_KEY": "sk-conifer-…" }
    }
  }
}

Cursor asks before each tool call by default. Keep that on for conifer_complete and conifer_compare — they spend money, and the prompt is where you see the ceiling you set.

VS Code

Create .vscode/mcp.json in the workspace. VS Code shows a Start hover over the server entry, and the tools appear in Copilot Chat’s agent mode:

.vscode/mcp.json
{
  "inputs": [
    {
      "type": "promptString",
      "id": "conifer-key",
      "description": "Conifer API key",
      "password": true
    }
  ],
  "servers": {
    "conifer": {
      "type": "stdio",
      "command": "node",
      "args": ["/path/to/use-conifer/bin/conifer-mcp.mjs"],
      "env": { "CONIFER_API_KEY": "${input:conifer-key}" }
    }
  }
}

The inputs block makes VS Code prompt for the key once and store it in its own secret storage, so it never sits in a file that gets committed.

What the agent gets

ToolWhat it does
conifer_completeAsk any model on the gateway a question, or hand it a whole conversation. The answer comes back with the exact settled cost of that call. Takes max_cost_nanousd, so the agent can bound its own spend before the call.
conifer_compareThe same prompt across two to five models in parallel. Each answer comes back beside its cost, cheapest first. A model that fails reports its error in place — one bad model never sinks the comparison.
conifer_list_modelsThe catalog this key can call, with declared capabilities, context windows, and the as-charged price for each entry's lane.
conifer_choose_modelThe cheapest model that declares the capabilities you name. A model with undeclared capabilities is skipped rather than assumed capable; an unpriced one is skipped rather than assumed free.
conifer_balanceRemaining credit on the account behind the key. A read; it never moves money.

Using it in a pipeline

The pattern that pays for itself: when a step’s model choice is a guess, make it an experiment. conifer_compare runs the real prompt from the real job across the candidates and returns the answers next to their costs. You read three answers, you see that the cheap one is fine for this step, and now the pipeline runs on evidence.

Each model’s call in a comparison is its own billed turn. max_cost_nanousd caps each turn, not the total — five models under a $0.01 ceiling can spend up to $0.05.

A gateway refusal comes back to the agent as readable text with the remedy in it — add credits, lower the ceiling, pick another model — rather than as a dropped connection. An agent that can read “insufficient allowance” can do something about it.

The same idea without MCP

If you are writing the bot yourself, the SDK does what the tools do. This picks a model from what the catalog actually declares, bounds the spend, and reports the cost in the reply:

bot.ts
import { Conifer } from "@conifer/sdk";

const conifer = new Conifer({ defaultHeaders: { "x-conifer-client": "slack-bot" } });

export async function onMention(text: string, isLongTask: boolean) {
  // Ranks what the catalog DECLARES: a model with undeclared capabilities is
  // skipped rather than assumed capable, an unpriced one rather than free.
  const model = await conifer.cheapestFor(isLongTask ? ["tools"] : []);
  if (!model) return "no model in the catalog fits that request";

  const answer = await conifer.chat({
    model: model.id,
    messages: [{ role: "user", content: text }],
    maxTokens: 800,
    maxCostNanoUsd: 20_000_000, // $0.02 per reply, refused above it
  });

  return `${answer.choices[0]?.message?.content}

_${model.id} · $${answer.receipt.costUsd}_`;
}

The capabilities and prices it reads are the same ones GET /v1/models returns:

terminal
curl -s https://api.conifer.build/v1/models \
  -H "Authorization: Bearer $CONIFER_API_KEY" \
  | jq '.data[] | {id, caps, context_window, pricing}'