Back to white papers
White Paper

Build Your Own MCP Server for Claude: Tools, Resources, Prompts

Jake McCluskey
Build Your Own MCP Server for Claude: Tools, Resources, Prompts

Source topic: MCP (Model Context Protocol), flagged in the "AI Engineering Roadmap 2026" as a 2026-specific skill.

Stack: mcp Python SDK (or TypeScript), Claude Desktop / Claude Code.

What MCP actually is (in one paragraph)

MCP is the plug standard between LLMs and external systems. Before MCP, every tool you wanted Claude to use needed a custom integration per LLM host. With MCP, you write a server once (exposing tools, resources, prompts) and any MCP-aware client (Claude Desktop, Claude Code, Cursor, etc.) can connect to it. Think "USB-C for LLMs."

Three things an MCP server can expose:

  1. Tools: functions the LLM can call (query_database, send_email)
  2. Resources: readable content (files, API responses) the LLM can pull
  3. Prompts: pre-baked prompt templates the user can invoke

Why this is a genuine resume skill

Every serious LLM deployment is moving to MCP. Writing your own MCP server shows you understand:

  • Tool schemas (JSON Schema)
  • Stateful session handling between LLM and server
  • The protocol contracts that make agents interoperable
  • How to expose internal company systems to LLMs safely

Build: a "Company Wiki" MCP server

Scenario: expose a local markdown wiki to Claude so it can search, read, and summarize pages.

1. Install

pip install mcp
# or for TypeScript:
# npm install @modelcontextprotocol/sdk

2. Full server: wiki_server.py

import os, re
from pathlib import Path
from mcp.server.fastmcp import FastMCP

WIKI_ROOT = Path(os.environ.get("WIKI_ROOT", "./wiki"))
mcp = FastMCP("company-wiki")

# ---------- TOOLS ----------

@mcp.tool()
def search_wiki(query: str, limit: int = 10) -> list[dict]:
    """Full-text search across all wiki pages. Returns hits with page title, path, and matched snippet."""
    hits = []
    pattern = re.compile(re.escape(query), re.IGNORECASE)
    for md in WIKI_ROOT.rglob("*.md"):
        text = md.read_text(encoding="utf-8", errors="ignore")
        if match := pattern.search(text):
            start = max(0, match.start() - 80)
            end = min(len(text), match.end() + 80)
            hits.append({
                "path": str(md.relative_to(WIKI_ROOT)),
                "title": md.stem.replace("-", " ").title(),
                "snippet": text[start:end].replace("\n", " "),
            })
            if len(hits) >= limit:
                break
    return hits

@mcp.tool()
def read_page(path: str) -> str:
    """Read a single wiki page by its relative path."""
    full = WIKI_ROOT / path
    if not full.exists() or WIKI_ROOT not in full.resolve().parents and full.resolve() != (WIKI_ROOT / path).resolve():
        return f"Page not found or outside wiki: {path}"
    return full.read_text(encoding="utf-8", errors="ignore")

@mcp.tool()
def list_pages(folder: str = "") -> list[str]:
    """List all wiki pages, optionally under a subfolder."""
    base = WIKI_ROOT / folder if folder else WIKI_ROOT
    return sorted(str(p.relative_to(WIKI_ROOT)) for p in base.rglob("*.md"))

# ---------- RESOURCES ----------

@mcp.resource("wiki://{path}")
def page_resource(path: str) -> str:
    """Expose individual pages as readable resources Claude can pull without invoking a tool."""
    return read_page(path)

# ---------- PROMPTS ----------

@mcp.prompt()
def onboarding_summary() -> str:
    """Prompt template: summarize the onboarding-tagged pages for a new hire."""
    return (
        "Search the wiki for pages tagged 'onboarding'. For each, pull the page and "
        "produce a concise summary grouped by theme (accounts, tooling, team norms, code review)."
    )

if __name__ == "__main__":
    mcp.run(transport="stdio")

3. Register with Claude Desktop

macOS: ~/Library/Application Support/Claude/claude_desktop_config.json
Windows: %APPDATA%\Claude\claude_desktop_config.json

{
  "mcpServers": {
    "company-wiki": {
      "command": "python",
      "args": ["/absolute/path/to/wiki_server.py"],
      "env": {
        "WIKI_ROOT": "/absolute/path/to/your/wiki"
      }
    }
  }
}

Restart Claude Desktop. You should see a plug icon with company-wiki connected. Claude can now call search_wiki, read_page, and list_pages.

4. Register with Claude Code (CLI)

claude mcp add company-wiki \
  --command python \
  --args /absolute/path/to/wiki_server.py \
  --env WIKI_ROOT=/absolute/path/to/your/wiki

Or edit .claude/settings.json:

{
  "mcpServers": {
    "company-wiki": {
      "command": "python",
      "args": ["./wiki_server.py"],
      "env": {"WIKI_ROOT": "./wiki"}
    }
  }
}

Level up: remote HTTP MCP server

For team use, run MCP over HTTP (SSE transport) so several people share one server.

# http_server.py
from mcp.server.fastmcp import FastMCP

mcp = FastMCP("company-wiki", host="0.0.0.0", port=8765)

# (same @mcp.tool definitions as above)

if __name__ == "__main__":
    mcp.run(transport="sse")  # Server-Sent Events

Client config:

{
  "mcpServers": {
    "company-wiki": {
      "transport": "sse",
      "url": "http://wiki-mcp.internal:8765/sse"
    }
  }
}

Add auth with a reverse proxy (nginx plus a token). MCP itself doesn't enforce it.

Level up #2: tools that write, not just read

The wiki example is read-only. For a more impressive demo, add a create_page tool:

@mcp.tool()
def create_page(path: str, title: str, content: str) -> str:
    """Create a new wiki page. Requires human approval — Claude Desktop will prompt."""
    full = WIKI_ROOT / path
    if full.exists():
        return f"Page already exists: {path}"
    full.parent.mkdir(parents=True, exist_ok=True)
    full.write_text(f"# {title}\n\n{content}\n", encoding="utf-8")
    return f"Created: {path}"

Claude will show the user the proposed write and ask for approval before executing. That's the MCP security model: the client gates destructive actions.

Debugging checklist

  1. Server won't start in Claude Desktop: check logs at ~/Library/Logs/Claude/mcp-server-company-wiki.log (macOS) or %APPDATA%\Claude\logs\ (Windows).
  2. Tool doesn't appear: confirm the @mcp.tool() decorator is applied and the function has a docstring (Claude uses it as the description).
  3. Type errors: MCP inspects Python type hints to generate JSON Schema. Use list[dict], not a bare list.
  4. Stdio hang: don't print to stdout inside tool functions. stdout is the transport. Use logging to stderr instead.

What makes this different from a REST API

MCP serverREST API
LLM discovers tools at connection timeYou hardcode endpoints
Schemas are strongly typed (JSON Schema)OpenAPI optional
Built-in tool-use / human-approval loopYou roll your own
Stateful session (can keep context)Usually stateless
Standard across all MCP-aware clientsPer-client integration

Resume angle

"Shipped an MCP server exposing internal knowledge-base search as first-class tools for Claude: search, read, and create pages over stdio and SSE transports. Demonstrated the agent-to-system integration pattern that replaces bespoke LLM tool wiring."