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. Ship an MCP Server in Python That Exposes Your Internal API to LLMs 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 https://www.devclubhouse.com/u/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 or uv for package management- Node.js 18+ — mcp dev invokes npx @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 : python 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:Set API 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 an httpx.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. https://www.devclubhouse.com/go/ad/13 { "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 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, replace mcp.run with mcp.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: Replace dict 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/docs https://modelcontextprotocol.io/docs and the Python SDK on GitHub https://github.com/modelcontextprotocol/python-sdk . Mariana Souza https://www.devclubhouse.com/u/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.