Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 9

Custom FastAPI Dependencies for Input Validation

Recommended resources

Introduction

Welcome to the next stage of building our MCP server! In the previous lessons, we successfully exposed multiple tools and secured the entire toolset using a global authentication dependency with AuthConfig. This gave us a robust, top-level security layer.

Today, we'll zoom in from the server level to the individual tool level. Our goal is to create a pre-tool hook—a piece of code that runs before a tool's main logic to validate its specific inputs. This is a fundamental pattern for building reliable and predictable AI tools. Your experience with Rasa Pro, where you likely implemented logic to validate slot values before an action executes, will provide a strong conceptual parallel here.

By the end of this lesson, you will have created and applied a custom FastAPI dependency that validates the format of an order_id for the get_order_details tool, preventing invalid requests from ever being processed.


1. From Authentication to Validation: The Role of Dependencies

So far, we've used a dependency for a single purpose: authentication. But FastAPI's dependency injection system is far more versatile. It's a powerful mechanism for running any reusable logic before your main endpoint handler executes. Common use cases include logging, managing database connections, and, most importantly for us today, input validation.

Why is this so crucial for AI tools?

  • Fail Fast: If an LLM hallucinates an invalid input (like a malformed ID), we want to catch it immediately rather than letting it hit our database or internal services.
  • Clear Feedback: A specific validation error can, in more advanced scenarios, be used to give the LLM feedback on why its request failed, helping it to self-correct.
  • Robustness: It separates the validation logic from the core business logic of the tool, making the code cleaner and easier to maintain.

The key question is: how can a dependency inspect the parameters being sent to a specific tool?


2. The Magic of Automatic Parameter Injection

FastAPI has a powerful feature that makes this possible: it automatically inspects the function signature of your dependency and injects parameters from the request if the names match.

For example, our get_order_details tool is defined on the path /orders/{order_id}. If we create a dependency function that accepts an argument named order_id, FastAPI will automatically pass the value from the URL path into our dependency.

The following article explains this mechanism in detail. It's a short but critical read to understand how we can build context-aware dependencies.

Consuming Path Arguments Directly in FastAPI Dependency ...

The article 'Consuming Path Arguments Directly in FastAPI Dependency Functions' explains the core mechanism that allows our validation dependencies to access tool-specific parameters.

Please read the sections 'Automatic Parameter Resolution Mechanism' and 'Validation and Type Coercion'. Pay close attention to the code example showing how a user_id from the path is passed to the validate_user_id dependency.


3. Implementing a Custom Validation Dependency

Now let's apply this concept. We'll create a validation dependency for our get_order_details tool. Let's establish a business rule: a valid order_id must be a string that starts with the prefix "ORD" and is followed by only digits.

In your main.py file, add the following dependency function. A good place for it is right after your verify_auth function.




# main.py




# ... existing imports
from fastapi import FastAPI, Depends, HTTPException, Request




# ...




# --- Authentication ---
def verify_auth(request: Request):



    # ... your existing verify_auth function
    pass




# --- Custom Validation Dependencies ---
async def validate_order_id_format(order_id: str):
    """
    Dependency to validate that the order_id starts with 'ORD'
    and is followed by digits.
    """
    if not order_id.startswith("ORD") or not order_id[3:].isdigit():
        raise HTTPException(
            status_code=400,  # Bad Request
            detail="Invalid order_id format. Must start with 'ORD' followed by digits."
        )



    # If validation passes, the function simply returns.
    # We could also return a value, but it's not needed here.

Let's break this down:

  1. async def validate_order_id_format(order_id: str):: We define an async function that accepts one argument, order_id. FastAPI will supply this value from the request path.
  2. if not order_id.startswith("ORD") ...: This is our validation logic.
  3. raise HTTPException(...): If the validation fails, we raise an HTTPException with a 400 Bad Request status code and a descriptive error message. This immediately stops the request processing and sends the error back to the client (the MCP-enabled AI).

4. Applying the Validator to a Specific Tool

We don't want this validation to run for every tool—it's specific to get_order_details. In our last lesson, we applied authentication globally using AuthConfig. To apply a dependency to a single endpoint, we add it to the @app.get() decorator for that specific route.

The following video clip demonstrates this exact syntax.

Fast API Tutorial, Part 25: Dependencies in path operation decorators, global dependencies

This clip from the 'Fast API Tutorial' shows how to apply dependencies to a specific path operation decorator, which is exactly what we need to do for our tool.

Watch the segment from 05:11 to 06:15. Focus on how the dependencies parameter is added to the @app.get decorator, taking a list containing Depends(...).

Now, modify the get_order_details endpoint in your main.py to include our new validation dependency.




# main.py




# ... your other code ...




# --- Tool Endpoints ---

@app.get(
    "/orders/{order_id}",
    operation_id="get_order_details",
    description="Fetches detailed information for a specific order by its ID.",
    tags=["orders"],
    response_model=Order,



    # Apply our custom validator to this endpoint ONLY
    dependencies=[Depends(validate_order_id_format)],
)
async def get_order_details(order_id: str) -> Dict[str, Any]:
    """
    Example endpoint to simulate fetching order data.
    In a real application, this would query a database.
    """



    # Placeholder data - this code will only run if validation passes
    if order_id == "ORD12345":
        return {
            "order_id": order_id,
            "customer_id": 101,
            "status": "shipped",
            "total_amount": 199.99,
            "currency": "USD",
        }
    raise HTTPException(status_code=404, detail="Order not found")




# ... the rest of your code (get_customer_profile, MCP setup, etc.) remains unchanged

Notice that we add dependencies=[Depends(validate_order_id_format)] to the decorator. Now, any request to /orders/{order_id} will first run verify_auth (from our global AuthConfig), then validate_order_id_format, and only if both pass will it execute the get_order_details function body.


5. Testing the Pre-Tool Hook

Let's verify our pre-tool hook is working as expected using the MCP Inspector.

  1. Make sure your FastAPI server is running with the latest changes (uv run uvicorn main:app --reload).
  2. Open the MCP Inspector and connect to your server at http://localhost:8000/mcp, remembering to provide your Authorization header.
  3. You should see your tools listed. Find get_order_details.

Test 1: Valid Input

  • In the get_order_details tool panel, enter ORD12345 for the order_id.
  • Execute the tool.
  • Expected Result: The call succeeds, and you receive the JSON object for the order. This confirms that valid data passes through the dependency.

Test 2: Invalid Input

  • Now, enter ORDER-123 for the order_id. This format violates our rule.
  • Execute the tool.
  • Expected Result: The call fails. The Inspector should show an error response with a status code of 400 and the detail message: "Invalid order_id format. Must start with 'ORD' followed by digits.". This proves our pre-tool hook is successfully catching invalid input and stopping execution.

Conclusion

Congratulations! You have successfully implemented your first pre-tool hook. You've moved beyond general authentication and are now applying fine-grained, tool-specific business rules.

Key Takeaways:

  • FastAPI dependencies serve as powerful "pre-hooks" for running logic like input validation before a tool's main code.
  • The framework's automatic parameter injection is the key mechanism that allows a dependency to access and validate parameters from the request path (e.g., order_id).
  • By adding dependencies=[Depends(...)] to a specific route's decorator, you can apply validation logic to individual tools, giving you granular control.

In the next lesson, we will expand on this concept by creating a more advanced "gating" dependency. Instead of just validating the format of an input, this hook will use business logic to decide whether a tool should be allowed to run at all, adding another layer of intelligence to your MCP server.