Back to white papers
White Paper

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

Jake McCluskey

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."

READY TO IMPLEMENT

Want to talk through this in your business?

The paper above is the thinking. Let's spend 30 minutes on what it would actually look like to ship in your shop, no pitch, just a real scoping conversation.

Build Your Own MCP Server for Claude | Elite AI Advantage