How to Wrap a REST API as an MCP Server for AI Agents

A hands-on Python tutorial for wrapping any REST API as an MCP server so AI agents like Claude can discover and call your tools at runtime.

MK

Mohammed Kafeel

Machine Learning Researcher

June 23, 202615 min read
On this page

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
  • httpx for async HTTP calls
  • python-dotenv for 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 dataResource
  • Action the agent decides to takeTool
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_weather and get_weather_forecast appear in the tools list
  • Input schema - the city parameter shows as required, days as 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: .env files, 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