Field guides · MCP
Your AI is guessing our violet.
It isn't lying. It has no way to look it up. An MCP server is how you give it one — here's the whole idea in seven short beats.
01 · The guess
Ask for our violet. Watch.
—
What the AI picked
#8118e1
Ours
It picked the most common purple on the internet. Close enough to pass a glance, wrong enough to fail an accessibility check — and it'll do it again tomorrow, because corrections don't stick.
The wiring
A model doesn't retrieve facts; it predicts plausible ones. Our palette isn't in its training data, so the plausible answer is Tailwind's violet-500. Pasting the brand guide into every session "fixes" it at a per-turn token cost, goes stale on the next Figma edit, and still can't tell you whether the pair passes AA.
// The nearest Tailwind purple vs ours. Close enough to survive review,
// far enough to fail accessibility.
{ "guessed": "#8B5CF6", "white_text_contrast": "4.23:1", "AA_body": false },
{ "ours": "#8118e1", "white_text_contrast": "6.66:1", "AA_body": true }
02 · What an MCP server is
A small program the AI can phone.
It sits beside the AI and knows things the AI can't see. Plug it in and the AI gets a menu of what it can ask for. One plug, any AI app — think USB-C.
The host
Claude Code
or Claude Desktop, Cursor, VS Code…
The server
c2-brand
~200 lines. Knows our palette. Can do maths.
- get_brand_token — "what's our violet?"
- check_contrast — "is white readable on it?"
- lint_brand_colors — "did I use anything off-brand?"
- list_brand_tokens — "show me everything"
↑ the menu arrives the moment they connect. The AI decides when to order.
The wiring
Protocol-wise it's JSON-RPC over stdin/stdout (local) or HTTP (remote). The host asks tools/list, gets names + descriptions + a JSON Schema per argument, and forwards that to the model as its tool set. The description is the prompt — "use this instead of inferring" arrives exactly when it's relevant.
// What the model actually receives when it connects — tools/list, trimmed.
// The description is the only thing it reads before deciding to call. It's a prompt.
{ "tools": [
{ "name": "get_brand_token",
"description": "Resolve a colour name (violet, cyan, ink…) to its exact approved
hex in both themes. Use this instead of recalling or inferring.",
"inputSchema": { "type": "object", "properties": { "name": { "type": "string" } },
"required": ["name"] } },
{ "name": "check_contrast", "description": "WCAG ratio + AA/AAA verdict for a text/background pair." },
{ "name": "lint_brand_colors","description": "Scan a file for hex codes not in the palette; suggest the nearest token." },
{ "name": "list_brand_tokens","description": "Every approved token, font and house rule." }
]}
03 · Same question, with the server
Now it looks it up.
—
What the AI picked
Three phone calls: what's the hex, is white readable on it, did I slip anything off-brand in. Right first time — and the last call is the AI checking its own work.
The wiring
Two kinds of tool are doing the work. Lookup replaces interpolation with retrieval. Verification is the one people skip and the one that changes behaviour most: a tool that returns pass/fail turns a generator into an iterator — write, check, fix — without you in the loop. If you build one tool, build that one.
server.registerTool("get_brand_token", {
description: "Resolve a colour name to its exact approved hex. " +
"Use this instead of recalling or inferring a hex value.",
inputSchema: { name: z.string() },
annotations: { readOnlyHint: true },
}, async ({ name }) => {
const token = brand.tokens.find((t) => t.name === name.toLowerCase());
return token
? json(token)
: json({ error: `No token "${name}"`, available: brand.tokens.map((t) => t.name) });
});
// The linter's last line is what makes the agent iterate instead of stop:
// summary: "7 off-brand value(s). Fix each one and re-run."
04 · Change the truth
Rebrand. Ask again. Try it.
Pick a new violet below — that edits the file the server reads. Nothing is retrained, nothing re-pasted. The AI just asks the file.
brand.json
{
"name": "violet",
"hex": "#8118e1",
"role": "accent"
}
pick one
The button it builds next — already the new colour.
The wiring
The palette lives in a JSON file, not in the server code, so a designer owns it. This page simulates the round-trip; the real thing behaves identically — edit brand.json, ask again, new answer. Nothing you can put in a prompt, a memory file, or a model's weights does that.
{ "name": "violet", "hex": "#8118e1", "dark": "#b07df5", "role": "accent",
"use": "Primary accent and CTA. The one everybody guesses wrong." }
05 · Why it works
Three things a prompt can't do.
?→!
Looks up, doesn't guess
For anything private — your colours, your clients, today's numbers — the “plausible” answer is someone else's. A tool is the only line to yours.
✓✗
Checks its own work
Give it a tool that says pass or fail and it stops handing you drafts. It fixes, re-checks, then says done.
now
Always current
Nothing is copied into the AI. It asks the source every time, so the answer is as fresh as the file — and it works in every AI app, once.
06 · Where else this goes
Anything with a truth the AI can't see.
Same shape every time: a human question on the left, a tiny function that answers it on the right.
Harvest
“How many hours on Acme this week?”
→ list_time_entries(project)
Already running for some of us.
Figma
“Build this screen.”
→ get_design_context(node)
Reads the file instead of squinting at a screenshot.
Laravel Boost
“What columns does orders have?”
→ database_schema(table)
First-party. Schema, routes, version-aware docs.
Your CMS
“Which posts still mention the old pricing?”
→ search_content(query)
One afternoon to build.
Analytics
“Did yesterday's launch move signups?”
→ get_metric(name, range)
Real numbers, not vibes.
Deploys
“Ship it to staging.”
→ deploy(env) · with a confirm step
An action with guardrails beats improvised curl.
The wiring
Cheaper answers first: a file the agent can already read, a CLI it already drives well (gh, wrangler, aws), or a skill if you're teaching a process rather than exposing data. Build a server when the truth lives somewhere unreachable, when you want an action with a schema and guardrails, when you want the agent to check itself, or when more than one person or client needs it. Tools execute with your credentials — read-only scopes, dedicated tokens, readOnlyHint on anything that doesn't write.
07 · Build one
Three steps. An afternoon.
-
1
Put the truth in a file
Or point at where it already lives — a database, an API, a spreadsheet.
-
2
Write one function
With a one-sentence description of when to call it. That sentence is the whole art.
-
3
Plug it in
One command. Then ask the AI a question it couldn't answer five minutes ago.
The one from this page is real and lives in this repo: scripts/mcp-demo/. Engineers — for the code and the exercise brief.
The wiring
npm install @modelcontextprotocol/sdk zod
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";
const server = new McpServer({ name: "c2-brand", version: "1.0.0" });
server.registerTool("get_brand_token", {
description: "Resolve a colour name to its exact hex. Use instead of guessing.",
inputSchema: { name: z.string() },
}, async ({ name }) => ({
content: [{ type: "text", text: JSON.stringify(brand.tokens.find((t) => t.name === name)) }],
}));
await server.connect(new StdioServerTransport()); // that's a working server
# project scope writes .mcp.json → commit it, teammates get it on clone
claude mcp add c2-brand --scope project -- node scripts/mcp-demo/server.mjs
# no client needed to test — it's a program that reads JSON on stdin
printf '%s\n' \
'{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"d","version":"1"}}}' \
'{"jsonrpc":"2.0","method":"notifications/initialized"}' \
'{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"get_brand_token","arguments":{"name":"violet"}}}' \
| node scripts/mcp-demo/server.mjs
Exercise · 45 minutes, pairs
- Pick a truth your agent gets wrong today — a client list, a set of URLs, a price sheet, a schema. Put it in a JSON file.
- Copy scripts/mcp-demo/server.mjs, keep one lookup tool, rename it, write the description as the sentence you'd say to a new hire.
- claude mcp add it. Fresh session. Ask the question without naming the tool. If it doesn't call it, sharpen the description.
- Stretch: add a tool that returns { ok: false, fix: … } for something the agent produces. Watch it iterate.
- Gotcha: project-scoped servers show as pending approval on first open — approve once.