Why build one?
Your company has things no public server covers: an internal API, a proprietary database or a home-grown deploy system. A small MCP server makes them available to Claude Code (and any other MCP client) in about 30 lines.
Set up
uv init docs-server && cd docs-server
uv add "mcp[cli]"Define tools with decorators
The Python SDK's FastMCP class turns type-hinted functions into MCP tools. The docstring and type hints generate the JSON schema for you, so you never hand-write schemas.
from mcp.server.fastmcp import FastMCP
mcp = FastMCP("docs")
DOCS = {
"deploy": "Deploys run from main via `make deploy`. Staging deploys on every merge.",
"oncall": "On-call rotates Mondays. Page via #oncall-urgent.",
}
@mcp.tool()
def list_docs() -> list[str]:
"""List the IDs of all internal docs."""
return sorted(DOCS)
@mcp.tool()
def read_doc(doc_id: str) -> str:
"""Read an internal doc by its ID."""
if doc_id not in DOCS:
raise ValueError(f"Unknown doc '{doc_id}'. Call list_docs first.")
return DOCS[doc_id]
@mcp.tool()
def edit_doc(doc_id: str, new_content: str) -> str:
"""Replace the content of an existing doc."""
if doc_id not in DOCS:
raise ValueError(f"Unknown doc '{doc_id}'.")
DOCS[doc_id] = new_content
return f"Updated {doc_id}"
if __name__ == "__main__":
mcp.run() # stdio transport by defaultError messages are read by Claude. "Unknown doc 'deploy-v2'. Call list_docs first." tells it exactly how to recover, while a bare KeyError doesn't.
Add resources
Resources expose data the app or user can attach. They can be static or templated:
@mcp.resource("docs://index")
def docs_index() -> str:
"""All doc IDs, one per line."""
return "\n".join(sorted(DOCS))
@mcp.resource("docs://{doc_id}")
def doc_resource(doc_id: str) -> str:
"""A single doc as a resource."""
return DOCS[doc_id]Add a prompt
@mcp.prompt()
def summarize_doc(doc_id: str) -> str:
"""Summarize a doc for a new teammate."""
return f"Read the doc '{doc_id}' and summarize it in 5 bullets for someone in their first week."Test with the Inspector
Before wiring it into Claude, test it in the browser-based MCP Inspector:
uv run mcp dev server.pyYou can list tools, call them with test inputs, read resources and see the exact JSON messages. That makes it much easier to debug a server before an AI is involved.
Connect it to Claude Code
claude mcp add --transport stdio docs -- uv run --directory /path/to/docs-server server.py- ⋮⋮Create the project and add the mcp[cli] package
- ⋮⋮Define tools with @mcp.tool() and clear docstrings
- ⋮⋮Add resources and prompts if they're useful
- ⋮⋮Test every tool in the MCP Inspector
- ⋮⋮Register it with claude mcp add and try it in a session
Check your understanding
What generates a FastMCP tool's input schema?
Why write descriptive error messages in tools?
What does uv run mcp dev server.py do?
Which decorator defines a templated resource?