A Guide to Nico's MCP Servers
how I give Claude persistent memory of my brand, design system, and teaching work
- renamed servers to match actual deployments: writer-for-human, designer-for-human, writer-for-designerer
- builder-of-decks now targets Slidev (was Reveal.js) with a custom Apple HIG theme
- updated tool names and descriptions throughout to match current specs
What Are MCPs?
Model Context Protocol — an open standard from Anthropic. It lets Claude call tools on external servers you control and pull back structured data.
- A server exposes tools (functions Claude can call) and returns data (JSON, text, whatever you need).
- Claude discovers what’s available, picks the right tool, calls it, uses the result.
- Without MCP, you’d paste your brand rules into every conversation. With it, Claude just pulls what it needs on the fly.
Why build custom servers?
- consistency — same rules, every conversation, no drift
- single source of truth — change a colour hex once; it propagates everywhere
- token efficiency — Claude grabs the slice it needs, not your whole style guide
- version control — rules live in Git, not copy-pasted prompt fragments
Figma Console MCP
Before getting into my custom servers — one off-the-shelf MCP worth knowing about. Figma Console MCP connects Claude to Figma so it can read your design files, inspect components, and pull tokens directly.
- runs via
npx— nothing to deploy or host - needs a Figma access token set as an environment variable
- works in Claude Desktop, Claude.ai, and Claude Code
My Custom Servers
Six servers. Each owns one domain of knowledge, nothing more. I built them for my day-to-day as a bilingual educator and freelance UI/UX designer.
| Server | Role |
|---|---|
| builder-of-mcps | Scaffolds new MCP servers (the “meta” server) |
| builder-of-decks | Slidev presentation toolkit with custom Apple HIG theme |
| writer-for-human | Linguistic style — tone, word choice, bilingual glossary |
| designer-for-human | Visual design system — colours, type, spacing, components |
| humanizer | Psycholinguistic rules for human-sounding writing |
| writer-for-designer | Client communication guide for freelance design |
What Each Does
- builder-of-mcps — Infrastructure config, file templates, deployment checklists, lessons learned. It keeps a port registry so servers don’t collide. When I need a new server, Claude calls this one and gets a fully resolved scaffold — file names, directory structure, config values, the lot.
-
builder-of-decks — CSS
class reference, composition patterns, design workflow, speaker
notes rules, quality checklists. One
get_scaffoldcall gives Claude a working Slidev deck with the brand theme baked in. -
writer-for-human — English
writing rules, Traditional Chinese writing rules, tone calibration,
a bilingual glossary, word-choice guidance, formatting rules.
get_style_for_taskreturns only what applies to a given task type, language, and audience — so Claude doesn’t burn tokens loading rules it won’t use. -
designer-for-human — Colour
palette (hex/RGB/HSL + WCAG contrast ratios), typography (5 fonts,
11-step fluid type scale), spacing, elevation, motion, icons, 12 UI
components, 11 behaviour patterns, imagery direction, dark-mode
overrides, accessibility standards. The composite
tool
get_visual_for_taskpulls all of that together for a given design task in a single call. - humanizer — Lexical, structural, sentiment, discourse, and psycholinguistic texture rules, plus content profiles and anti-patterns. The goal: AI-written text that doesn’t read like AI wrote it. Works across any content type, not just brand copy.
- writer-for-designer — Communication principles, audience-aware guidance (design-literate vs. non-designer clients), context-specific rules for proposals, feedback sessions, scope conversations, and the awkward ones too. Includes vocabulary with plain-language equivalents and patterns to avoid.
Architecture
Stack
Same pattern for every server:
- language — Python 3.12
-
framework —
FastMCP (
mcppackage) - transport — Streamable HTTP
-
data — JSON files, Git-tracked, in a
/datadirectory - container — Docker, managed through Portainer
- hosting — NAS (always on, low power draw)
- public access — Cloudflare Tunnel
- auth — Cloudflare Access — no app-level auth code needed
Project Structure
mcp-[name]/
├── CLAUDE.md # Context file for Claude Code
├── docs/
│ ├── PRD.md # Product requirements
│ ├── DATA_SCHEMA.md # JSON data file schemas
│ ├── API_SPEC.md # Tool specs
│ └── DEPLOYMENT.md # Deployment runbook
├── src/
│ ├── __init__.py
│ ├── server.py # FastMCP entry point
│ ├── tools/
│ │ ├── __init__.py
│ │ ├── read.py # Read-only tools
│ │ └── updates.py # Write tools (auto-commits to Git)
│ └── storage/
│ ├── __init__.py
│ └── json_store.py # JSON I/O + Git commit helper
├── data/
│ └── *.json # Domain data
├── Dockerfile
├── docker-compose.yml
├── entrypoint.sh # Startup script — seeds data, initialises Git
├── requirements.txt
└── .gitignore
Data Flow
┌───────────┐ HTTPS ┌──────────────────┐ Docker ┌────────────┐
│ Claude.ai │───────>│ Cloudflare Tunnel │────────>│ MCP Server │
│ or Claude │ MCP │ + Access (auth) │ local │ on NAS │
│ Code │ calls │ │ │ │
│ │<───────│ │<────────│ │
└───────────┘ JSON └──────────────────┘ └─────┬──────┘
│
┌─────┴──────┐
│/data/*.json│
│(Git-tracked)│
└─────┬──────┘
│
┌─────┴──────┐
│ GitHub │
│ (sync) │
└────────────┘
- Claude fires an MCP tool call over HTTPS
- Cloudflare Tunnel routes it to the NAS
- Cloudflare Access checks authorization
- the server reads its JSON data, returns a response
- write operations save to JSON, commit to Git, and push to GitHub automatically
Data Sync
Each container runs an entrypoint.sh script on startup:
-
seeds the
/datadirectory from bundled defaults if files are missing - initialises a local Git repo so write operations can auto-commit
- starts the server
For remote sync, add a git pull step to
entrypoint.sh that fetches from your GitHub repo before
starting the server. A daily scheduled container restart keeps data
fresh without manual intervention.
In Practice
Teaching
“Create a 12-slide deck on narrative structure for Grade 8.”
-
builder-of-decks →
get_scaffold— a working Slidev deck with the brand theme -
builder-of-decks →
get_compositions— picks a composition pattern for each slide -
designer-for-human →
get_visual_for_task— colours, fonts, spacing -
writer-for-human →
get_style_for_task— tone and language rules - humanizer — keeps the speaker notes from sounding robotic
What comes back is a deck that actually looks and reads like the brand. Not a generic template with my logo slapped on top.
Design
“Draft a project proposal email about the website redesign.”
-
writer-for-designer →
get_communication_guide— proposal rules matched to client literacy - humanizer — psycholinguistic rules so it reads like a person wrote it
-
designer-for-human →
get_brand_foundation— if the proposal references the practice itself
The output adjusts automatically. Non-designer client? No jargon. Design-literate? Precise terminology. Same principles either way.
Build Your Own
Prerequisites
- always-on machine — NAS, VPS, Raspberry Pi, or just your Mac if you leave it running
- Docker — keeps deployments reproducible
- a tunnel — Cloudflare Tunnel (free), ngrok, or Tailscale Funnel for a public URL; without one, only Claude Code on your local network can reach the server
- Git hosting — GitHub, GitLab, whatever you use
Steps
1. Start with one server.
Pick the single domain that eats the most time. Brand guidelines, writing style, component library — whichever hurts most to re-explain. Don’t try to build six at once.
2. Pick a framework.
- FastMCP (Python) — what I use
-
@modelcontextprotocol/sdk(TypeScript) — the official SDK - anything that can serve HTTP and return JSON works
3. Put your knowledge in JSON
One /data directory. One file per concern. This is your
source of truth.
{
"brand_colours": {
"primary": "#2D5A3D",
"secondary": "#8B6F47"
},
"tone": {
"default": "warm, direct, encouraging",
"formal": "respectful, precise, understated"
}
}
4. Serve slices, not the whole file.
This is the design choice that matters most. Tools should return the minimum data Claude actually needs. Add filters so it can ask for just the piece it wants:
@mcp.tool()
async def get_colours(role: str | None = None) -> dict:
"""Get brand colours, optionally filtered by role."""
colours = load_json("colours.json")
if role:
return {k: v for k, v in colours.items()
if v.get("role") == role}
return colours
Keeps token usage low. Also makes responses noticeably faster.
5. Containerise and deploy.
FROM python:3.12-slim
RUN apt-get update && apt-get install -y git \
&& rm -rf /var/lib/apt/lists/*
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY src/ ./src/
COPY data/ ./data-defaults/
COPY entrypoint.sh .
RUN chmod +x entrypoint.sh
ENV PYTHONPATH=/app
ENTRYPOINT ["./entrypoint.sh"]
6. Connect to Claude.
- Claude.ai / Desktop — Settings → Connectors → Add custom connector with your server’s public URL
- Claude Code — Add it to your project settings with the streamable HTTP transport
Lessons Learned
Things I figured out the hard way:
-
never use
mcp.run()in production — useuvicorn.run()withstreamable_http_app()instead -
never use
BaseHTTPMiddlewarewith FastMCP — it corrupts the MCP stream silently, and you’ll waste hours debugging it -
always disable DNS rebinding protection
when behind a reverse proxy — FastMCP’s
TransportSecuritySettingshandles this -
always set
PYTHONPATH=/appin your Dockerfile, or Python won’t find your modules and the error message won’t tell you why -
always Git-track your
/datadirectory — you get version history and a sync mechanism for free -
always use
ensure_ascii=Falsewhen writing JSON with non-ASCII content, otherwise your Chinese characters turn into\uescape sequences and become unreadable in the file - always keep it to one server per domain — a monolith sounds easier until you need to update one thing without breaking everything else
-
always prefix tool names when running
multiple servers —
humanizer_get_rulesvsstyle_get_rules— or they’ll collide and only one will register