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

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

Jake McCluskeyUpdated
Back to white papers

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

Common questions

Frequently asked

What is the difference between MCP tools, resources, and prompts?

Tools are functions the LLM can call, such as query_database or send_email. Resources are readable content like files or API responses that the LLM can pull. Prompts are pre-baked prompt templates the user can invoke. All three are exposed through a single MCP server.

How do I connect my MCP server to Claude Desktop?

Edit the claude_desktop_config.json file (in ~/Library/Application Support/Claude/ on macOS or %APPDATA%\Claude\ on Windows) to add your server under mcpServers with the command, args, and any required env variables. Restart Claude Desktop and you should see a plug icon showing your server is connected.

Can Claude write to my wiki through an MCP server?

Yes. You can add a write tool like create_page that accepts path, title, and content parameters. Claude will show the user the proposed write and ask for approval before executing, as the MCP security model has the client gate destructive actions, not the server.

What is the difference between stdio and SSE transport in MCP?

Stdio transport runs the server locally as a subprocess connected through standard input/output, suitable for single-user desktop use. SSE transport (Server-Sent Events) runs over HTTP and allows multiple people to share one remote MCP server, which is useful for team deployments.

Where do I find logs when my MCP server fails to start in Claude Desktop?

Check the logs at ~/Library/Logs/Claude/mcp-server-{servername}.log on macOS or %APPDATA%\Claude\logs\ on Windows. Common issues include missing docstrings on tool functions, incorrect type hints, or accidentally printing to stdout instead of logging to stderr.

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