Affiliate Disclosure: This article contains affiliate links. We may earn a commission if you purchase through these links, at no additional cost to you. This helps us continue publishing free content. See our full disclosure. This report examines build mcp server tutorial.
Part 2 of 4 in the OpenClaw Saga series.
Every AI agent hits the same wall: Claude and ChatGPT can chat, but they can’t touch company internal tools. Notion databases stay out of reach. Slack channels remain silent. That internal API? Forget it.
Anthropic’s Model Context Protocol (MCP) fixes this. Think of it as a USB-C port for AI – plug in any tool once, and every MCP-compatible client can use it. Over 2,000 servers now exist in the official registry, with Microsoft, Google, AWS, and OpenAI all shipping MCP support in their platforms as of early 2026.
The detail most documentation skips: building an MCP server takes about 20 minutes. The official guides make it sound like a weekend project. It isn’t. This tutorial walks through building a working server that Claude Code can actually call – no theory, just code that runs.
Prerequisites
Based on available evidence, this tutorial requires Python 3.10 or higher, basic familiarity with async Python, and a working Claude Code or Claude Desktop installation for testing. The MCP Python SDK v1.2.0+ handles most of the heavy lifting.
What This Tutorial Builds
A custom MCP server that exposes two tools: one to fetch company data from an internal API, and one to post messages to a Slack channel. By the end, Claude Code will be able to query internal systems and send notifications – all through natural language commands. The same server works with any MCP-compatible client, not just Claude.
1. Set Up Your Environment
First, create a project directory and install the MCP SDK. Using uv (the modern Python package manager) keeps dependency management clean:
# Create project directory
mkdir company-mcp-server
cd company-mcp-server
# Initialize with uv
uv init
uv venv
source .venv/bin/activate # On Windows: .venv\Scripts\activate
# Install MCP SDK with CLI tools
uv add "mcp[cli]" httpx
The [cli] extra includes development tools that make testing significantly easier. The httpx library handles HTTP requests to your internal API – swap this for whatever HTTP client the team prefers.
Create the main server file:
touch server.py
2. Create the MCP Server Instance
Open server.py and initialize the FastMCP server. This class handles protocol compliance, connection management, and automatic schema generation:
from typing import Any
import httpx
from mcp.server.fastmcp import FastMCP
# Initialize FastMCP server with a descriptive name
mcp = FastMCP("company-tools")
# Configuration constants
INTERNAL_API_BASE = "https://api.yourcompany.internal/v1"
SLACK_WEBHOOK_URL = "https://hooks.slack.com/services/YOUR/WEBHOOK/HERE"
USER_AGENT = "company-mcp-server/1.0"
The server name appears in Claude Code’s tool list, so pick something the team will recognize. FastMCP automatically generates tool schemas from Python type hints and docstrings – no manual JSON schema required.
Why this matters: The USB-C analogy isn’t just marketing. Once a server implements the MCP protocol, any MCP client can discover and use it. Build once, run everywhere. The official registry lists 2,000+ servers built by teams who realized this math works in their favor.
3. Add Helper Functions
Before defining tools, set up helper functions for API communication. MCP servers run asynchronously, so use httpx.AsyncClient:
async def fetch_from_api(endpoint: str) -> dict[str, Any] | None:
"""Make authenticated request to internal API."""
headers = {
"User-Agent": USER_AGENT,
"Authorization": f"Bearer {get_api_token()}", # Implement token retrieval
"Accept": "application/json"
}
async with httpx.AsyncClient() as client:
try:
response = await client.get(
f"{INTERNAL_API_BASE}/{endpoint}",
headers=headers,
timeout=30.0
)
response.raise_for_status()
return response.json()
except Exception as e:
# Log to stderr, NOT stdout (breaks JSON-RPC)
import sys
print(f"API request failed: {e}", file=sys.stderr)
return None
def get_api_token() -> str:
"""Retrieve API token from environment or secrets manager."""
import os
return os.environ.get("INTERNAL_API_TOKEN", "")
Critical detail: Never use print() without file=sys.stderr in STDIO-based MCP servers. Writing to stdout corrupts the JSON-RPC message stream, causing the server to crash. This catches even experienced developers.
4. Define Your First Tool
Now, expose the internal API as an MCP tool. The @mcp.tool() decorator handles registration and schema generation automatically:

@mcp.tool()
async def get_customer(customer_id: str) -> str:
"""Fetch customer data from internal CRM.
Args:
customer_id: The unique customer identifier (e.g., "CUST-12345")
Returns:
Formatted customer information, including name, email, and account status
"""
data = await fetch_from_api(f"customers/{customer_id}")
if not data:
return f"Unable to fetch customer {customer_id}. Check the ID and try again."
# Format for LLM consumption
return f"""
Customer ID: {data.get('id', 'Unknown')}
Name: {data.get('name', 'Unknown')}
Email: {data.get('email', 'Unknown')}
Status: {data.get('status', 'Unknown')}
Account Balance: ${data.get('balance', 0):.2f}
Last Contact: {data.get('last_contact', 'Never')}
""".strip()
The docstring becomes part of the tool’s description that Claude sees. Write it as if explaining the tool to a colleague – because that’s exactly the audience. Clear descriptions reduce the number of confused “why didn’t that work?” debugging sessions.
The workflow shift: Before MCP, connecting Claude to internal tools meant building a custom integration for each client. Now? One server, any client. A developer on the team can spin up a new tool in the time it takes to drink coffee.
5. Add a Second Tool with Side Effects
Tools can perform actions, not just fetch data. Here’s a Slack notification tool:
@mcp.tool()
async def notify_slack(channel: str, message: str) -> str:
"""Post a message to a Slack channel.
Args:
channel: Channel name without # (e.g., "engineering", "alerts")
message: The message content to post
Returns:
Confirmation of successful delivery or error details
"""
import httpx
payload = {
"channel": f"#{channel}",
"text": message,
"username": "MCP Bot"
}
async with httpx.AsyncClient() as client:
try:
response = await client.post(
SLACK_WEBHOOK_URL,
json=payload,
timeout=10.0
)
response.raise_for_status()
return f"Message posted to #{channel} successfully"
except Exception as e:
return f"Failed to post to Slack: {str(e)}"
MCP clients automatically handle tool approval flows. When Claude wants to call notify_slack, the user sees a prompt asking for permission. No extra code required.
6. Run the Server
Add the entry point at the bottom of server.py:
def main():
"""Start the MCP server."""
mcp.run(transport="stdio")
if __name__ == "__main__":
main()
Test that it starts without errors:
uv run server.py
You won’t see any output – that’s correct. STDIO-based servers communicate via stdin/stdout, not terminal messages. Any logs should go to stderr or a file.
7. Connect to Claude Code
Claude Code connects to MCP servers through a configuration file. Run this command to add your server:
claude mcp add company-tools --command "uv" --args "--directory" "/ABSOLUTE/PATH/TO/company-mcp-server" "run" "server.py"
Replace /ABSOLUTE/PATH/TO/ with your actual path. Get it with pwd (macOS/Linux) or cd (Windows).
Verify the connection:
claude mcp list
Teams should see company-tools in the output. Restart Claude Code if the server doesn’t appear immediately.
8. Test the Tools
Now for the payoff. Open Claude Code and try:
“Look up customer CUST-12345 and tell me their account status.”
Claude will:
- Recognize that it has a
get_customertool. - Extract “CUST-12345” as the customer_id
- Ask for permission to call the tool.
- Display the formatted result.
Try the Slack tool:
“Send a message to the engineering channel saying the MCP server is working.”
Claude handles the natural language parsing and parameter extraction. The server just returns clean data.
Common Pitfalls
Writing to stdout
Using print() without file=sys.stderr corrupts the JSON-RPC stream. The server appears to start, then silently fails on the first request. This is the #1 cause of “my server doesn’t work” debugging sessions.
Fix: Always use print(..., file=sys.stderr) or a logging library configured for stderr/file output.
Relative Paths in Configuration
Claude Code launches servers from a different working directory than where development occurred. Relative paths in code (like ./config.json) will fail.
Fix: Use absolute paths or Path(__file__).parent to resolve paths relative to the server file.
Missing Type Hints
FastMCP generates tool schemas from function signatures. Missing type hints result in generic “any” schemas that confuse Claude and produce worse tool calls.
Fix: Add type hints to all function parameters and return values:
# Bad
def get_data(id):
...
# Good
async def get_data(id: str) -> dict[str, Any]:
...
Blocking Operations in Async Tools
Calling synchronous libraries (like requests instead of httpx) blocks the event loop. The server freezes until the request completes.
Fix: Use async libraries throughout, or wrap sync calls with asyncio.to_thread().
Hardcoded Credentials
Storing API tokens in source code works until someone commits it to GitHub. Then it becomes a security incident.
Fix: Use environment variables, a dedicated credential manager, or the MCP authorization flow for sensitive credentials.
What’s Next
Once a basic server works, consider these extensions:
Add Resources for read-only data: MCP Resources expose data without computation – perfect for configuration files, documentation, or cached API responses. Use @mcp.resource("template://{param}") decorators.
Implement Prompts for reusable workflows: MCP Prompts let users invoke predefined prompt templates. Create a “customer-summary” prompt that automatically calls your customer tool with the right formatting.
Deploy as HTTP transport: For remote servers or shared infrastructure, switch to transport="streamable-http" and deploy behind a reverse proxy. The same code works – just change the transport in mcp.run().
Add authentication: The MCP spec includes OAuth 2.1 support for servers that need authorization. Implement TokenVerifier to validate tokens before allowing tool calls.
Explore the registry: The MCP Registry lists 2,000+ community servers. Before building something from scratch, check if someone already solved the problem. Popular options include GitHub integration, database connectors, and web scraping tools.
From a practical standpoint, the verdict: MCP server development is straightforward enough that the 20-minute timeline isn’t marketing fluff – it’s realistic. The protocol handles the hard parts (discovery, schema generation, client compatibility), leaving you to focus on what your tools actually do. For teams standardizing on Claude Code or building internal AI workflows, MCP servers are the path of least resistance. Skip the custom integration work. Plug into the standard.
The strongest counterargument is that MCP’s “build once, run everywhere” promise depends entirely on broad, sustained adoption — and protocol standardization efforts in the developer tooling space have a long history of fragmenting under competitive pressure. Critics of this framing point out that with OpenAI, Google, and Microsoft all backing MCP today, the same vendors could just as easily diverge into proprietary extensions tomorrow, leaving teams with servers tightly coupled to a standard that has quietly forked. The 2,000-server registry is a real signal of momentum, but it is also a relatively low bar at this stage of the protocol’s lifecycle, and production teams should weigh the switching costs before treating MCP as permanent infrastructure.
What to Read Next
- JPMorgan’s AI Mandate Hides a 39-Point Perception Gap
- AI Coding Tools Cost $6,750/yr in Hidden Rework — 5 Ranked by True Price
- Shadow AI Costs $21K Per App: The 3:1 Ratio Nobody Tracks
References
- Model Context Protocol Introduction – Official documentation explaining MCP architecture and the USB-C analogy for AI tool integration
- Build an MCP Server – MCP Documentation – Complete walkthrough with Python and TypeScript examples
- MCP Python SDK Repository – Official Python SDK with FastMCP implementation, type hints, and extensive examples
- FastMCP Documentation – Production-ready Python framework for building MCP servers and clients
- FastMCP GitHub Repository – Source code, examples, and issue tracker for FastMCP
- MCP Inspector – Official debugging tool for MCP servers
