Building an AI-Ready Product Catalog with FastAPI and MCP


Objective: In this lab, you will create a product catalog API using FastAPI and transform it into an AI-accessible service using the Model Context Protocol (MCP) with FastMCP. The lab is divided into two parts: Part 1 focuses on building and testing a FastAPI-based product catalog API, and Part 2 guides you through creating an MCP server to expose your API as AI-callable tools.

Prerequisites:

  • Python 3.10+ installed.
  • Basic understanding of Python, REST APIs, and JSON.
  • Familiarity with terminal commands and virtual environments.
  • Installations: fastapi[all], fastmcp, and uvicorn (via pip or uv).
  • Optional: Claude Desktop for testing AI tool calls (free tier sufficient).
  • Code editor (e.g., VS Code).
  • A project directory (e.g., product-catalog-lab).

Part 1: Building the FastAPI Product Catalog API

Objective: Create a FastAPI application with endpoints to list all products and retrieve a product by ID, using Pydantic models for data validation and a mock in-memory database.

Step 1.1: Set Up Your Environment

  1. Create a project directory:

    mkdir product-catalog-lab
    cd product-catalog-lab
  2. Set up a virtual environment:

    python -m venv venv
    source venv/bin/activate  # On Windows: venv\Scripts\activate
  3. Install required packages:

    pip install fastapi[all] uvicorn

    Alternatively, use uv:

    uv add fastapi[all] uvicorn

Step 1.2: Create the FastAPI Application

  1. Create a file named main.py in your project directory.

  2. Add the following code to define the product catalog API:

    # main.py
    from fastapi import FastAPI, HTTPException
    from pydantic import BaseModel
    from typing import List, Optional
    
    # Initialize FastAPI app
    app = FastAPI(title="Product Catalog API", description="A simple API for managing a product catalog")
    
    # Define Pydantic model for Product
    class Product(BaseModel):
        id: int
        name: str
        price: float
        description: Optional[str] = None
    
    # Mock in-memory database
    products_db = [
        Product(id=1, name="Laptop", price=999.99, description="High-end gaming laptop"),
        Product(id=2, name="Wireless Mouse", price=29.99, description="Ergonomic wireless mouse"),
        Product(id=3, name="Keyboard", price=59.99),
    ]
    
    @app.get("/products", response_model=List[Product])
    async def list_products():
        """Retrieve a list of all products in the catalog."""
        return products_db
    
    @app.get("/products/{product_id}", response_model=Product)
    async def get_product(product_id: int):
        """Retrieve a specific product by its ID."""
        for product in products_db:
            if product.id == product_id:
                return product
        raise HTTPException(status_code=404, detail="Product not found")

    Explanation:

    • Pydantic Model: Product enforces data structure with id, name, price, and optional description.
    • Mock Database: products_db is a simple list for demonstration. In a real app, you’d use a database like SQLite or PostgreSQL.
    • Endpoints:
      • GET /products: Returns all products.
      • GET /products/{product_id}: Returns a single product or a 404 error if not found.
    • Docstrings: These improve API documentation and will be used by MCP for AI-friendly tool descriptions.

Step 1.3: Test the FastAPI Application

  1. Run the FastAPI server:

    uvicorn main:app --host localhost --port 8000 --reload
  2. Open a browser or use curl to test the endpoints:

    • List Products:

      curl http://localhost:8000/products

      Expected output:

      [
        {
          "id": 1,
          "name": "Laptop",
          "price": 999.99,
          "description": "High-end gaming laptop"
        },
        {
          "id": 2,
          "name": "Wireless Mouse",
          "price": 29.99,
          "description": "Ergonomic wireless mouse"
        },
        { "id": 3, "name": "Keyboard", "price": 59.99, "description": null }
      ]
    • Get Product by ID:

      curl http://localhost:8000/products/1

      Expected output:

      {
        "id": 1,
        "name": "Laptop",
        "price": 999.99,
        "description": "High-end gaming laptop"
      }

      Test a non-existent ID:

      curl http://localhost:8000/products/999

      Expected output:

      { "detail": "Product not found" }
  3. Visit http://localhost:8000/docs in a browser to explore the interactive Swagger UI. Test both endpoints there.

Step 1.4: Verify OpenAPI Schema

  1. Access the OpenAPI schema at http://localhost:8000/openapi.json.

  2. Save this schema to a file (openapi.json) for reference:

    curl http://localhost:8000/openapi.json > openapi.json

    This schema will be used by FastMCP to generate AI tools.

Step 1.5: Reflection Questions

  • How does Pydantic ensure data validation in the API responses?
  • What happens if you send an invalid product_id (e.g., a string instead of an integer)?
  • How could you extend this API to include a POST /products endpoint for adding new products?

Deliverable: A running FastAPI server with two functional endpoints, verified via curl or Swagger UI.

Part 2: Creating the MCP Server

Objective: Use FastMCP to expose your FastAPI endpoints as AI-callable tools via an MCP server, enabling integration with AI agents like Claude.

Step 2.1: Install FastMCP

  1. Install FastMCP in your virtual environment:

    pip install fastmcp

    Or with uv:

    uv add fastmcp

Step 2.2: Create the MCP Server

  1. Create a new file named mcp_server.py in your project directory.

  2. Add the following code to set up the MCP server:

    # mcp_server.py
    import sys
    from pathlib import Path
    import asyncio
    from contextlib import asynccontextmanager
    
    # Ensure the project root is in the Python path
    sys.path.append(str(Path(__file__).parent))
    
    from fastmcp import FastMCP
    from main import products_db, Product
    from typing import List
    
    # Initialize FastMCP
    mcp = FastMCP(
        name="Product Catalog MCP Server",
    )
    
    @mcp.tool()
    def list_products() -> List[dict]:
        """List all available products with their ID, name, price, and description."""
        return [product.model_dump() for product in products_db]
    
    @mcp.tool()
    def get_product(product_id: int) -> dict:
        """Retrieve details of a specific product by its ID.
    
        Args:
            product_id: The unique identifier of the product
        """
        for product in products_db:
            if product.id == product_id:
                return product.model_dump()
        return {"error": "Product not found"}
    
    if __name__ == "__main__":
        mcp.run()

    Explanation:

    • FastMCP Integration: Links your FastAPI app to MCP, auto-converting routes to tools.
    • Tool Decorators: @mcp.tool customizes how routes appear to AI agents, using your route’s logic and Pydantic schemas.
    • Separate Port: Runs on 8001 to avoid conflicting with your FastAPI server (8000).
    • Path Fix: sys.path.append ensures your main.py is importable.

Step 2.3: Run the MCP Server

  1. Keep your FastAPI server running (uvicorn main:app --port 8000).

  2. In a new terminal, activate the virtual environment and start the MCP server:

    source venv/bin/activate  # On Windows: venv\Scripts\activate
    python mcp_server.py
  3. Look for console output showing the MCP server running on http://localhost:8001 and a JSON config snippet for AI clients.

Step 2.4: Test with an AI Client (Claude Desktop)

  1. Install Claude Desktop if not already installed.

  2. Open Claude Desktop and navigate to Settings > Developer > Edit config to edit claude-desktop-config.json.

  3. Add the MCP server configuration (adjust paths as needed):

    {
      "mcpServers": {
        "product-catalog": {
          "command": "\\full\\path\\to\\product-catalog-lab\\venv\\Scripts\\python.exe",
          "args": ["\\full\\path\\to\\product-catalog-lab\\mcp_server.py"]
        }
      }
    }
    • Replace /full/path/to/product-catalog-lab with your project directory path.
  4. Restart Claude Desktop.

  5. In a new chat, enable tools:

    • Click Search and tools > Select “product-catalog-mcp”.

    • Test queries like:

      • “List all products in the catalog.”
      • “What is the product with ID 2?”
    • Expected outputs:

      • For “List all products”:

        Tool Call: list_products_tool
        Response: [
          {"id": 1, "name": "Laptop", "price": 999.99, "description": "High-end gaming laptop"},
          {"id": 2, "name": "Wireless Mouse", "price": 29.99, "description": "Ergonomic wireless mouse"},
          {"id": 3, "name": "Keyboard", "price": 59.99, "description": null}
        ]
      • For “Product with ID 2”:

        Tool Call: get_product_tool
        Response: {"id": 2, "name": "Wireless Mouse", "price": 29.99, "description": "Ergonomic wireless mouse"}

Step 2.5: Reflection Questions

  • How does FastMCP use your FastAPI app’s OpenAPI schema to create tools?
  • What benefits do the @mcp.tool decorators provide for AI interactions?
  • How would you secure the MCP server for production use (e.g., adding authentication)?

Deliverable: A running MCP server exposing your FastAPI endpoints as AI-callable tools, verified via Claude Desktop or another MCP-compatible client.

Additional Notes

  • Extending the Lab:
    • Add a POST /products endpoint to your FastAPI app and test excluding it from MCP exposure.
    • Replace the mock products_db with a PostgreSQL database using SQLAlchemy.
    • Experiment with Speakeasy CLI to generate a standalone MCP server from openapi.json.
    • Deploy using Docker.

Submission: Submit your main.py and mcp_server.py files, along with screenshots of:

  1. Swagger UI showing both FastAPI endpoints.
  2. Claude Desktop tool call outputs for both list_products_tool and get_product_tool.

By Wahid Hamdi