cd /news/developer-tools/tutorial-python-mcp-internal-api-llm · home topics developer-tools article
[ARTICLE · art-33453] src=devclubhouse.com ↗ pub= topic=developer-tools verified=true sentiment=· neutral

Tutorial: Python MCP Internal API > LLM

A tutorial demonstrates how to build a Python MCP server using FastMCP that wraps a corporate REST API into three typed tools—search_customers, get_order, and create_support_ticket—enabling LLMs like Claude Desktop to call the API with type safety without exposing credentials.

read6 min views4 publishedJun 19, 2026

Wrap a corporate REST API in three typed tools using FastMCP, inspect them locally, and connect them to Claude Desktop—without ever exposing credentials to the model.

Mariana Souza

What You'll Build #

A Python MCP server using FastMCP

that wraps a corporate REST API as three structured tools—search_customers

, get_order

, and create_support_ticket

. Any MCP-compatible client (Claude Desktop, Cursor, custom agents) can call your API with full type safety, without the model ever seeing credentials or constructing raw URLs.

Prerequisites #

  • Python 3.10+ (required for built-in generic types like list[dict]

) pip

oruv

for package management- Node.js 18+ — mcp dev

invokesnpx @modelcontextprotocol/inspector

under the hood - Latest Claude Desktop (for end-to-end testing; optional if using only the inspector)

  • A REST API with a bearer token — a mock URL works fine to follow along
  • Comfortable with async

/await

Python

1. Set Up the Project #

mkdir mcp-internal-api && cd mcp-internal-api
python -m venv .venv
source .venv/bin/activate        # Windows: .venv\Scripts\activate
pip install "mcp[cli]" httpx python-dotenv

mcp[cli]

installs the mcp

CLI used for local inspection. httpx

handles async HTTP to your backend.

Create .env

for local credentials — add it to .gitignore now:

API_BASE_URL=https://api.corp.example.com
API_KEY=sk-your-real-token-here

2. Write the Server #

Create server.py

:

import os
import httpx
from dotenv import load_dotenv
from mcp.server.fastmcp import FastMCP

load_dotenv()

mcp = FastMCP("internal-api")

_BASE = os.environ["API_BASE_URL"]
_KEY  = os.environ["API_KEY"]

def _auth_headers() -> dict[str, str]:
    return {"Authorization": f"Bearer {_KEY}", "Accept": "application/json"}

@mcp.tool()
async def search_customers(query: str, limit: int = 10) -> list[dict]:
    """Search customers by name or email. Returns a list of customer records."""
    async with httpx.AsyncClient() as client:
        r = await client.get(
            f"{_BASE}/customers",
            headers=_auth_headers(),
            params={"q": query, "limit": limit},
            timeout=10.0,
        )
        r.raise_for_status()
        return r.json()

@mcp.tool()
async def get_order(order_id: str) -> dict:
    """Fetch a single order by its ID."""
    async with httpx.AsyncClient() as client:
        r = await client.get(
            f"{_BASE}/orders/{order_id}",
            headers=_auth_headers(),
            timeout=10.0,
        )
        r.raise_for_status()
        return r.json()

@mcp.tool()
async def create_support_ticket(
    customer_id: str,
    subject: str,
    body: str,
    priority: str = "normal",
) -> dict:
    """Open a support ticket for a customer.

    Args:
        customer_id: The customer's UUID.
        subject: One-line summary (max 120 chars).
        body: Full description of the issue.
        priority: 'low', 'normal', or 'high'.
    """
    if priority not in {"low", "normal", "high"}:
        raise ValueError(f"priority must be low/normal/high, got '{priority}'")

    async with httpx.AsyncClient() as client:
        r = await client.post(
            f"{_BASE}/tickets",
            headers=_auth_headers(),
            json={
                "customer_id": customer_id,
                "subject": subject,
                "body": body,
                "priority": priority,
            },
            timeout=10.0,
        )
        r.raise_for_status()
        return r.json()

if __name__ == "__main__":
    mcp.run()

Why each decision matters:

Detail Reason
Type annotations FastMCP auto-generates JSON Schema from them — the LLM receives exact parameter types, not free-form text
Docstrings Become the tool description the model reads before calling; write them like an API spec
raise_for_status() + ValueError
Exceptions surface to the LLM as structured tool errors rather than crashing the server process
Credentials in env vars Never passed as tool arguments, never echoed in responses, never in source control

mcp.run()

defaults to stdio transport, which is what Claude Desktop and most local clients expect — the client spawns your server as a subprocess and talks JSON-RPC over stdin/stdout.

3. Inspect Locally with mcp dev #

Before touching any LLM, validate the wiring in a browser UI:

mcp dev server.py

This starts your server and opens the MCP Inspector (the URL is printed in your terminal). Navigate to Tools — you'll see all three tools with auto-generated input forms matching your Python signatures. Call search_customers

with query = "alice"

and confirm a JSON response or a typed upstream error.

Tip:SetAPI_BASE_URL=https://httpbin.org

temporarily to exercise the async/auth plumbing without a live internal API. You'll get a 404 back, which correctly surfaces as anhttpx.HTTPStatusError

tool error.

4. Wire to Claude Desktop #

Locate the config file:

OS Path
macOS ~/Library/Application Support/Claude/claude_desktop_config.json
Windows %APPDATA%\Claude\claude_desktop_config.json

Add your server entry. Use absolute paths — Claude Desktop spawns a clean, non-login shell that won't activate your virtualenv:

Serverless Inference by DigitalOcean 55+ models, every modality. One API key, one bill.

{
  "mcpServers": {
    "internal-api": {
      "command": "/absolute/path/to/.venv/bin/python",
      "args": ["/absolute/path/to/server.py"],
      "env": {
        "API_BASE_URL": "https://api.corp.example.com",
        "API_KEY": "sk-your-real-token-here"
      }
    }
  }
}

Restart Claude Desktop, then in a new conversation:

"Search for customers named 'smith', then open a high-priority support ticket for the first result explaining their order is delayed."

Claude will call search_customers

, inspect the output, then call create_support_ticket

— tool calls appear inline in the UI with their arguments and responses visible.

Verify It Works #

Inspector: After mcp dev server.py

, the Tools tab lists all three tools with correct schemas and no import errors in the terminal.

Claude Desktop: Open Settings → Developer. Your server appears as internal-api

with a green connected indicator. If it shows an error state, restart Claude Desktop after editing the config.

Schema sanity-check — confirm FastMCP generated correct schemas without starting a full client:

python -c "
import os; os.environ['API_BASE_URL']='http://x'; os.environ['API_KEY']='x'
import asyncio
import server
async def main():
    for tool in await server.mcp.list_tools():
        print(tool.name, tool.inputSchema)
asyncio.run(main())
"

You should see each tool name alongside its JSON Schema inputSchema

dict.

Troubleshooting #

** ModuleNotFoundError: No module named 'mcp'** — Claude Desktop uses a clean shell; your virtualenv isn't activated. Confirm

"command"

points to the venv interpreter: /path/to/.venv/bin/python

, not the system python

.Tools don't appear in Claude Desktop — Run mcp dev server.py

first; import errors or missing env vars appear there immediately. Also check ~/Library/Logs/Claude/

on macOS — Claude Desktop writes a per-server log file named after your server key (internal-api

).

** KeyError: 'API_BASE_URL'** — The

env

block in claude_desktop_config.json

replaces the shell environment entirely; load_dotenv()

won't read your .env

from there. Set all required keys explicitly in the JSON config.** httpx.ReadTimeout** — Your backend is slow. Raise

timeout=30.0

, or restructure long-running operations to return an AsyncGenerator

and use yield

to stream partial results back to the client.## Next Steps

Resources: Expose read-only context (OpenAPI specs, internal wikis) via@mcp.resource()

so the LLM can pull reference material without consuming tool-call budget.HTTP/SSE transport: For multi-user or remote deployments, replacemcp.run()

withmcp.run(transport="sse")

and mount it behind a secured reverse proxy; validate per-request tokens in middleware rather than a static env var.Rate limiting: Wrap_auth_headers()

with a token-bucket limiter (aiolimiter

is async-native) to prevent an agentic loop from flooding your upstream API.Richer schemas: Replacedict

return types with Pydantic models —FastMCP

generates detailed JSON Schema from them, giving the model better guidance on what fields to expect and use.Spec & SDK:modelcontextprotocol.io/docsand thePython SDK on GitHub.

Mariana Souza· Senior Editor

Mariana covers the fast-moving world of machine learning and generative AI, with a particular focus on how these technologies are reshaping development workflows. When she isn't stress-testing the latest foundation models, she's usually at a local hackathon.

Discussion 0 #

No comments yet

Be the first to weigh in.

── more in #developer-tools 4 stories · sorted by recency
── more on @fastmcp 3 stories trending now
sponsored brought to you by zahid.host 4,200+ EU-deployed projects
reading about agents? ship yours in a single git push.

Run your AI side-project on zahid.host

EU-based hosting, git-push deploys, automatic HTTPS, no cold starts. Free tier with a custom domain — perfect for shipping the agent you just read about.

$git push zahid main
Live at https://your-agent.zahid.host
Get free account → Pricing
from €0/mo · no card required
LIVE [news/tutorial-python-mcp-…] indexed:0 read:6min 2026-06-19 ·