On this page
- What Is an MCP Server? (And Why Should You Care?)
- REST API vs MCP Server - What's the Difference?
- When Should You Wrap a REST API as an MCP Server?
- What You Need Before You Start (Prerequisites)
- Step-by-Step: How to Wrap a REST API as an MCP Server in Python
- Common Mistakes to Avoid
- Advanced Tips for Production-Ready MCP Servers
- Key Takeaways
- FAQ
- Useful Sources
By December 2025, the MCP ecosystem had crossed 10,000 active public MCP servers. Every major AI tool - Claude, Cursor, Windsurf, VS Code - now supports the Model Context Protocol natively. If your REST API isn't MCP-ready, AI agents simply can't use it. They won't even know it exists.
In this guide, you'll learn exactly how to wrap any REST API as an MCP server in under an hour - using Python, FastMCP, and a real working example with the OpenWeatherMap API.
TL;DR
- MCP (Model Context Protocol) is an open standard created by Anthropic (November 2024) that lets AI agents discover and call external tools at runtime.
- An MCP server wraps your existing API and exposes it to AI agents as Tools, Resources, or Prompts.
- REST APIs are stateless and designed for humans; MCP servers are stateful and designed for AI agents.
- You can build a working Python MCP server in ~50 lines of code using FastMCP and
httpx.- Test locally with MCP Inspector, then connect to Claude Desktop or any MCP-compatible host.
What Is an MCP Server? (And Why Should You Care?)
An MCP server is a lightweight program that exposes tools, data, and prompts to AI agents using the Model Context Protocol. Think of it as the adapter that makes your existing systems speak "AI agent."
Model Context Protocol (MCP) was created by Anthropic and released as an open standard on November 25, 2024. The goal: give every AI agent a single, consistent way to connect to any external system - databases, APIs, file systems, you name it.
The best analogy? MCP is the USB-C of AI integrations. Before USB-C, every device had its own proprietary connector. Before MCP, every AI tool needed its own custom integration for every API. MCP standardizes the connector.
How the Architecture Works
MCP follows a clean three-layer architecture:
- MCP Host - the AI application (e.g., Claude Desktop, Cursor, VS Code). It manages connections.
- MCP Client - a component inside the host that maintains a dedicated connection to one MCP server.
- MCP Server - your program. It exposes capabilities to the client.
- External Systems - your REST API, database, or any backend the server talks to.
The 3 MCP Primitives
Every MCP server exposes capabilities through three primitives:
- Tools - executable functions the AI agent can call to take action (e.g., "search the web", "send an email", "get the weather"). The agent decides when to call them.
- Resources - read-only data sources that provide context (e.g., a file, a database record, an API snapshot). Think of them as GET endpoints the agent reads passively.
- Prompts - reusable templates that structure how the agent interacts with a tool or resource.
Why It Matters: The N×M Problem
Before MCP, if you had N AI tools and M APIs, you needed N×M custom integrations. With MCP, you write one MCP server per API (M servers), and every AI tool connects to all of them. That's N+M instead of N×M - a massive reduction in complexity. (We dig into this in the MCP N×M integration problem.)
By December 2025, the ecosystem had grown from zero to over 10,000 active public MCP servers, with the GitHub mcp-server topic covering nearly 16,000 repositories. This isn't a niche experiment anymore.
REST API vs MCP Server - What's the Difference?
The core difference: REST APIs are designed for humans and apps; MCP servers are designed for AI agents. A REST API waits to be called. An MCP server actively tells the agent what it can do.
Here's the full comparison:
| Feature | REST API | MCP Server |
|---|---|---|
| Designed for | Humans / apps | AI agents |
| Communication | Stateless HTTP | Stateful JSON-RPC 2.0 |
| Tool discovery | Manual docs | Dynamic at runtime |
| Auth model | API keys / OAuth | Per-session credentials |
| State | Stateless | Stateful |
| Who decides what to call | The developer | The AI agent |
JSON-RPC 2.0 (a lightweight remote procedure call protocol that runs over HTTP or stdio) is the wire format MCP uses. When an agent connects to your MCP server, it sends a tools/list request and gets back a structured list of everything your server can do - with input schemas, descriptions, and all. No docs required.
That's the key insight: the agent discovers your tools at runtime, not at build time. This is what makes MCP fundamentally different from just calling a REST API from a prompt. (For a deeper side-by-side, see MCP vs REST API.)
When Should You Wrap a REST API as an MCP Server?
Wrap a REST API as an MCP server when you want AI agents to autonomously discover and call it without hardcoded integration code.
Good use cases
- Internal tools - your company's CRM, ticketing system, or data warehouse
- Third-party APIs - weather, payments (Stripe), maps, communication (Twilio), search
- Developer tools - GitHub, Jira, Linear, Sentry
- Any API you control - where you want AI agent API integration without rebuilding from scratch
When NOT to wrap
- ⚠️ The API already has an official MCP server (check the MCP servers registry first)
- ⚠️ The API involves irreversible, high-stakes actions (wire transfers, mass deletions) without human-in-the-loop confirmation
- ⚠️ The API is too complex or undocumented to map cleanly to tools
Quick decision checklist
- Do I want an AI agent to call this API autonomously?
- Does an official MCP server not already exist for it?
- Can I define clear, single-purpose actions from the API's endpoints?
- Do I have the API credentials and documentation?
- Is the risk of autonomous calls acceptable?
If you checked all five - you're ready to build.
What You Need Before You Start (Prerequisites)
You need a working REST API, Python 3.10+, and the MCP SDK. Here's the full list:
- ✅ A REST API with documentation (we'll use OpenWeatherMap)
- ✅ Python 3.10 or higher (or Node.js 18+ for TypeScript)
- ✅ The MCP Python SDK with FastMCP
- ✅
httpxfor async HTTP calls - ✅
python-dotenvfor secure credential management - ✅ An API key for your target API
- ✅ (Optional) An OpenAPI spec - speeds up endpoint mapping significantly
Install everything in one command
pip install "mcp[cli]" httpx python-dotenv
Project structure
weather-mcp/
├── server.py # Your MCP server
├── .env # API keys (never commit this)
├── .env.example # Template for teammates
└── requirements.txt # Pinned dependencies
Step-by-Step: How to Wrap a REST API as an MCP Server in Python
We're going to wrap the OpenWeatherMap REST API as a fully working python MCP server. OpenWeatherMap is a great example: it's a real public API, it's free to start, and the endpoints are clean and well-documented.
Step 1: Set Up Your Environment
Create your project folder and install dependencies:
mkdir weather-mcp && cd weather-mcp
pip install "mcp[cli]" httpx python-dotenv
Sign up at openweathermap.org and grab a free API key. Then create your .env file:
# .env
OPENWEATHER_API_KEY=your_api_key_here
💡 Tip: Add .env to your .gitignore immediately. One leaked API key can cost you real money.
Step 2: Map Your REST Endpoints to MCP Primitives
Before writing a single line of code, decide which endpoints become Tools and which become Resources.
The rule is simple:
- Read-only, context-providing data → Resource
- Action the agent decides to take → Tool
| REST Endpoint | HTTP Method | MCP Primitive | Reason |
|---|---|---|---|
/weather |
GET | Tool | Agent decides when to fetch current weather |
/forecast |
GET | Tool | Agent decides when to fetch a forecast |
/air_pollution |
GET | Resource | Background context data |
In practice, most REST API endpoints end up as Tools - because the agent needs to decide when to call them based on the conversation. Use Resources for data you want pre-loaded as context.
Step 3: Write the MCP Server Code
Here's the full, runnable MCP server Python code for our weather server. Every line is commented.
# server.py
import httpx
from mcp.server.fastmcp import FastMCP
from dotenv import load_dotenv
import os
# Load API key from .env file
load_dotenv()
# Initialize the FastMCP server with a name
# This name appears in MCP clients like Claude Desktop
mcp = FastMCP("weather-mcp")
API_KEY = os.getenv("OPENWEATHER_API_KEY")
BASE_URL = "https://api.openweathermap.org/data/2.5"
@mcp.tool()
async def get_current_weather(city: str) -> dict:
"""
Get current weather conditions for a city.
Args:
city: The city name (e.g., 'London', 'Tokyo', 'New York')
Returns:
A dict with temperature, description, humidity, and city name.
"""
async with httpx.AsyncClient() as client:
response = await client.get(
f"{BASE_URL}/weather",
params={
"q": city,
"appid": API_KEY,
"units": "metric" # Celsius
}
)
# Raise an exception for 4xx/5xx responses
# Agents need clear error signals - don't swallow exceptions
response.raise_for_status()
data = response.json()
# Always transform raw API responses into clean, agent-readable output
# Don't just return data["main"] - shape it for the agent's needs
return {
"city": data["name"],
"country": data["sys"]["country"],
"temperature_celsius": data["main"]["temp"],
"feels_like_celsius": data["main"]["feels_like"],
"description": data["weather"][0]["description"],
"humidity_percent": data["main"]["humidity"],
"wind_speed_ms": data["wind"]["speed"]
}
@mcp.tool()
async def get_weather_forecast(city: str, days: int = 3) -> dict:
"""
Get a multi-day weather forecast for a city.
Args:
city: The city name.
days: Number of days to forecast (1–5). Defaults to 3.
Returns:
A dict with daily forecast summaries.
"""
# Clamp days to valid range - input validation matters
days = max(1, min(days, 5))
async with httpx.AsyncClient() as client:
response = await client.get(
f"{BASE_URL}/forecast",
params={
"q": city,
"appid": API_KEY,
"units": "metric",
"cnt": days * 8 # API returns data in 3-hour intervals
}
)
response.raise_for_status()
data = response.json()
# Group by day and summarize - don't dump 40 raw entries at the agent
forecasts = {}
for entry in data["list"]:
date = entry["dt_txt"].split(" ")[0]
if date not in forecasts:
forecasts[date] = {
"date": date,
"temps": [],
"descriptions": []
}
forecasts[date]["temps"].append(entry["main"]["temp"])
forecasts[date]["descriptions"].append(
entry["weather"][0]["description"]
)
# Compute daily averages
daily = []
for date, info in list(forecasts.items())[:days]:
daily.append({
"date": info["date"],
"avg_temp_celsius": round(
sum(info["temps"]) / len(info["temps"]), 1
),
"conditions": list(set(info["descriptions"]))
})
return {"city": city, "forecast": daily}
if __name__ == "__main__":
# Run the server using stdio transport (default for local MCP clients)
mcp.run()
The @mcp.tool() decorator does the heavy lifting: it auto-generates the JSON Schema for the function's inputs, registers the tool with the MCP server, and makes it discoverable. That's the FastMCP magic.
Step 4: Handle Authentication Securely
Never hardcode API keys. This is the single most common mistake we see in MCP server tutorials.
Your .env file:
# .env - never commit this file
OPENWEATHER_API_KEY=abc123yourkeyhere
Your .env.example file (commit this one):
# .env.example - copy to .env and fill in your values
OPENWEATHER_API_KEY=
For APIs that use Bearer tokens instead of query params, inject the token in the request headers:
headers = {"Authorization": f"Bearer {os.getenv('MY_API_TOKEN')}"}
response = await client.get(url, headers=headers)
For OAuth 2.0 APIs, store the refresh token in .env and handle token refresh in a helper function - don't bake it into each tool.
Step 5: Test Your MCP Server
Use MCP Inspector - it's the Postman for MCP servers. It gives you a browser-based UI to test tool discovery, input schemas, and responses without connecting a full AI client.
Run it directly with npx:
npx @modelcontextprotocol/inspector python server.py
MCP Inspector will open in your browser. Here's what to check:
- ✅ Tool discovery - both
get_current_weatherandget_weather_forecastappear in the tools list - ✅ Input schema - the
cityparameter shows as required,daysas optional with a default - ✅ Response format - call the tool with a real city and verify the returned JSON is clean and structured
- ✅ Error handling - try an invalid city name and confirm you get a meaningful error, not a stack trace
💡 Tip: MCP Inspector also shows you the raw JSON-RPC 2.0 messages being exchanged. This is invaluable for debugging schema issues.
[Screenshot: MCP Inspector showing the weather-mcp tools list with get_current_weather and get_weather_forecast]
Step 6: Connect to Claude Desktop (or Any MCP Client)
Once your server passes MCP Inspector, connecting it to Claude Desktop takes about 60 seconds.
Open (or create) your Claude Desktop config file:
- macOS:
~/Library/Application Support/Claude/claude_desktop_config.json - Windows:
%APPDATA%\Claude\claude_desktop_config.json
Add your server:
{
"mcpServers": {
"weather-mcp": {
"command": "python",
"args": ["/absolute/path/to/weather-mcp/server.py"],
"env": {
"OPENWEATHER_API_KEY": "your_api_key_here"
}
}
}
}
Restart Claude Desktop. You'll see a 🔌 icon in the chat interface confirming the MCP server is connected. Now ask Claude: "What's the weather in Tokyo right now?" - it will call your get_current_weather tool automatically.
How does the agent discover and call the tool? On startup, Claude Desktop sends a tools/list request to your server. Your server responds with the tool names, descriptions, and input schemas. When the user's message matches a tool's purpose, Claude decides to call it - sends a tools/call request - and your server executes the REST API call and returns the result.
Common Mistakes to Avoid
Most MCP server bugs fall into five predictable categories. Here's how to spot and fix each one.
| ⚠️ Mistake | ✅ Fix |
|---|---|
| Mapping every GET endpoint as a Tool | Use Resources for read-only context data; Tools for agent-driven actions |
Hardcoding API keys in server.py |
Always use .env + python-dotenv or environment variables |
| Returning raw API responses | Transform to clean, minimal dicts the agent can actually use |
| Skipping error handling | Use response.raise_for_status() and wrap in try/except with clear messages |
| Making tools too broad ("do_everything") | One tool = one clear, specific action |
A few more worth calling out:
- ⚠️ Don't ignore the tool description. The agent reads your docstring to decide when to call the tool. A vague description means the agent calls the wrong tool - or none at all. (See MCP tool schema design for how to write descriptions agents actually understand.)
- ⚠️ Don't return nested API objects verbatim. A raw OpenWeatherMap response has 30+ fields. Return the 5–7 your agent actually needs.
- ⚠️ Don't forget to handle rate limits. If your REST API has rate limits, your MCP server should catch 429 errors and return a clear "rate limit reached, try again in X seconds" message.
Advanced Tips for Production-Ready MCP Servers
You've got a working MCP server. Here's how to make it production-grade.
Add Input Validation with Pydantic
FastMCP works natively with Pydantic models. Use them for complex inputs:
from pydantic import BaseModel, Field
class WeatherRequest(BaseModel):
city: str = Field(..., description="City name, e.g. 'London'")
units: str = Field("metric", pattern="^(metric|imperial|standard)$")
@mcp.tool()
async def get_weather(request: WeatherRequest) -> dict:
...
Pydantic validation runs before your tool executes - so the agent gets a clear schema error instead of a cryptic HTTP 400.
Implement Rate Limiting Awareness
import asyncio
async def call_with_retry(client, url, params, max_retries=3):
for attempt in range(max_retries):
response = await client.get(url, params=params)
if response.status_code == 429:
wait = int(response.headers.get("Retry-After", 5))
await asyncio.sleep(wait)
continue
response.raise_for_status()
return response
raise Exception("Rate limit exceeded after retries")
Use Streaming for Large Responses
For endpoints that return large datasets, use MCP's streaming support to send results progressively rather than buffering everything in memory.
Add Structured Logging
import logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger("weather-mcp")
@mcp.tool()
async def get_current_weather(city: str) -> dict:
logger.info(f"Fetching weather for city={city}")
...
Logs are your best friend when debugging why an agent called the wrong tool or got an unexpected response.
Version Your MCP Server
Name your server with a version: FastMCP("weather-mcp-v1"). When you make breaking changes to tool signatures, bump the version. MCP clients cache tool schemas - a version bump signals them to refresh.
Consider openapi-to-mcp for Large APIs
If you're wrapping a large API with 50+ endpoints, don't map them manually. FastMCP's FastMCP.from_openapi() can generate an MCP server directly from an OpenAPI spec:
import httpx
from fastmcp import FastMCP
# Load OpenAPI spec from URL or file
spec = httpx.get("https://api.example.com/openapi.json").json()
mcp = FastMCP.from_openapi(spec, client=httpx.AsyncClient())
This is a massive time-saver for REST API MCP server projects at scale.
Key Takeaways
- ✅ MCP is the standard for AI agent API integration - created by Anthropic in November 2024, now supported by Claude, Cursor, VS Code, and more.
- ✅ MCP servers expose three primitives: Tools (agent-driven actions), Resources (read-only context), and Prompts (reusable templates).
- ✅ The mapping rule: read-only endpoints → Resources; action endpoints → Tools.
- ✅ FastMCP + httpx is the fastest path to a working Python MCP server - ~50 lines of code for a real, functional server.
- ✅ Always transform API responses into clean, minimal output before returning to the agent.
- ✅ Test with MCP Inspector before connecting to any AI client.
- ✅ Security first:
.envfiles, never hardcoded credentials, always validate inputs.
FAQ
What is an MCP server in simple terms?
An MCP server is a program that exposes your tools and data to AI agents using the Model Context Protocol. Think of it as a plug adapter: your existing API is the appliance, the MCP server is the adapter, and the AI agent is the socket. The agent plugs in, discovers what your server can do, and calls the right tool when needed.
Can I wrap any REST API as an MCP server?
Almost any REST API can be wrapped. The main exceptions: APIs that already have an official MCP server (check the MCP servers registry first), and APIs where autonomous agent calls would be too risky (e.g., irreversible financial transactions without human approval). For everything else, if you have the docs and credentials, you can wrap it.
What's the difference between MCP Tools and MCP Resources?
Tools are functions the AI agent actively calls to take an action - like fetching live weather or sending a message. Resources are read-only data sources the agent uses as background context - like a database schema or a static config file. The practical rule: if the agent needs to decide when to fetch it, it's a Tool. If it's always useful context, it's a Resource.
Do I need to know Python to build an MCP server?
No. MCP has official SDKs for Python, TypeScript/JavaScript, Java, Kotlin, C#, and Swift. The Python MCP server path (using FastMCP) is the most beginner-friendly, but if you're more comfortable in TypeScript or Node.js 18+, the TypeScript SDK is equally capable. The concepts - Tools, Resources, Prompts - are identical across all SDKs. (If you'd rather start fresh rather than wrap an existing API, see our tutorial on building an MCP server from scratch.)
How do AI agents discover MCP tools at runtime?
When an MCP host (like Claude Desktop) connects to your server, it sends a tools/list JSON-RPC 2.0 request. Your server responds with a list of all registered tools, including their names, descriptions, and input schemas. The AI agent reads these schemas and uses them to decide which tool to call - and with what arguments - based on the user's message. No hardcoded routing required.
Is MCP secure for production use?
MCP itself is protocol-agnostic on security, but it supports standard auth patterns. For local stdio transport (your machine), security is handled by OS-level process isolation. For remote HTTP transport, MCP recommends OAuth 2.0 and supports Bearer tokens and API keys per session. The main risks are in your server code: hardcoded credentials, missing input validation, and overly broad tools. Follow the security practices in this guide - and our checklist for securing your MCP server - and you'll be in good shape.
What's the best MCP SDK for beginners?
FastMCP (Python) is the most beginner-friendly starting point. It handles schema generation, validation, and transport automatically - you just write Python functions and decorate them with @mcp.tool(). The official MCP Python SDK at github.com/modelcontextprotocol/python-sdk incorporates FastMCP as its high-level interface. For TypeScript developers, the @modelcontextprotocol/sdk npm package is the equivalent starting point.
Useful Sources
- Model Context Protocol - Official Documentation - Architecture overview, primitives, and protocol spec
- Anthropic MCP Announcement (November 25, 2024) - The original launch post
- FastMCP Python Library - Documentation - Official FastMCP docs and examples
- FastMCP on GitHub (PrefectHQ) - Source code and advanced usage
- MCP Inspector - Official Tool - Testing and debugging MCP servers
- MCP Inspector on GitHub - Source and usage examples
- OpenWeatherMap Current Weather API Docs - Endpoint reference for the tutorial example
- MCP Python SDK on GitHub - Official Python SDK
Keep reading
How to Build Your First MCP Server in Python: Step-by-Step Tutorial
Build your first MCP server in Python with FastMCP — tools, resources, prompts, and wiring it into Claude Desktop, with full working code.
MCP vs REST API: Why They're Complementary, Not Competing Standards
MCP and REST aren't competitors — they're layers of the same stack. How MCP wraps REST for AI agents, and when to use each.
MCP Integration for Salesforce, SAP, and NetSuite: A Practical Guide
A step-by-step guide to MCP integration for Salesforce, SAP, and NetSuite - setup, security, use cases, and connecting AI agents to your enterprise systems.



