Skip to main content

Welcome to a Grasp lesson

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

Lesson 10

Implementing Custom Validation for MCP Server Endpoints

Recommended resources

Introduction

Hello! In our last lesson, we successfully created our first "pre-tool hook"—a custom FastAPI dependency named validate_order_id_format that checks if an order ID has the correct format. We applied it using the dependencies argument in the @app.get decorator, which is a great way to attach "fire-and-forget" validation that runs for its side effects (like raising an error).

Today, we're going to build on that by learning a more powerful and common way to apply the very same dependency. Instead of attaching it to the decorator, we'll inject it directly into our tool's function signature. This approach is not just an alternative; it's the foundation for more advanced patterns where dependencies need to pass data or results into your tool's logic.

Your experience with Rasa Pro actions, which receive a tracker and dispatcher object to work with, provides a good mental model here. Just as an action needs specific inputs to do its job, this new dependency pattern allows us to explicitly declare what our tool function needs to execute.


1. Two Ways to Apply a Dependency

FastAPI offers two primary ways to apply a dependency to a path operation, and choosing the right one depends on your goal:

  1. In the Decorator: dependencies=[Depends(my_dep)]

    • Use Case: Ideal for dependencies that don't return a value your endpoint function needs. Examples include logging a request, checking an API key, or—as we did last lesson—running validation that only raises an exception on failure.
    • Analogy: This is like a security guard at the door. They either let you in or they don't; they don't hand you anything on your way in.
  2. In the Function Signature: def my_endpoint(value: Annotated[T, Depends(my_dep)])

    • Use Case: Essential when the dependency performs a task and returns a result that your endpoint logic needs to use. This could be fetching a user object from a database, processing an input, or returning a validated and sanitized value.
    • Analogy: This is like a concierge. You give them a request (e.g., a ticket number), they validate it and process it, and then they hand you back something you need (e.g., your event pass).

Today, we will refactor our get_order_details tool to use the second pattern. It's a subtle but significant change that makes our code more explicit and extensible.


2. Injecting a Validation Dependency

To see this pattern in action, let's study a clear, practical example. The following article demonstrates exactly how to create a dependency that validates a path parameter and is then applied directly in the function signature.

Fastapi Depends with parameters and arguments - Hrekov

The article 'Fastapi Depends with parameters and arguments' from Hrekov provides excellent, concise examples of dependency injection patterns. We'll focus on the very first one.

Please read the section titled 'Request Input Parameters (Path/Query/Header)'. Pay close attention to how validate_item_id is defined and then used inside the read_item function's signature via the ValidItemID type alias. This is precisely the pattern we are about to implement.

As you saw in the article, the dependency validate_item_id doesn't just raise an error; it also returns the item_id on success. This allows the path operation function read_item to receive the validated value.


3. Refactoring Our Code

Let's apply this pattern to our own validate_order_id_format dependency and the get_order_details tool.

Step 1: Update the Dependency to Return a Value

First, modify validate_order_id_format in main.py so that it returns the order_id if validation succeeds.




# main.py




# ... inside your 'Custom Validation Dependencies' section

async def validate_order_id_format(order_id: str):
    """
    Dependency to validate that the order_id starts with 'ORD'
    and is followed by digits. Returns the validated order_id on success.
    """
    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, return the validated ID.
    return order_id

Step 2: Apply the Dependency in the Function Signature

Now, we'll change how the get_order_details endpoint uses this dependency. This involves two small changes: removing the dependency from the decorator and adding it to a function parameter's type annotation.

Make sure you have Annotated imported from the typing module at the top of your main.py.
from typing import Optional, Dict, Any, Annotated




# main.py




# ... in the 'Tool Endpoints' section

@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,



    # REMOVE this line: dependencies=[Depends(validate_order_id_format)],
)
async def get_order_details(



    # APPLY the dependency here instead
    order_id: Annotated[str, Depends(validate_order_id_format)]
) -> Dict[str, Any]:
    """
    Example endpoint to simulate fetching order data.
    This code will only run if the dependency validation passes.
    """



    # The rest of the function body remains the same.
    # It still uses the `order_id` parameter, which now holds the validated value.
    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")

Let's break down this refactoring:

  1. We removed dependencies=[Depends(validate_order_id_format)] from the @app.get decorator.
  2. We changed the order_id parameter from order_id: str to order_id: Annotated[str, Depends(validate_order_id_format)].

Now, when a request comes to /orders/{some_id}, FastAPI sees the Annotated type hint and does the following:

  • It runs the validate_order_id_format dependency, passing the {some_id} value to its order_id argument.
  • If the dependency raises an HTTPException, the request is stopped and the error is returned.
  • If the dependency succeeds, it returns the validated order_id. FastAPI then assigns this returned value to the get_order_details function's order_id parameter.
  • Finally, the body of get_order_details is executed.

This pattern is cleaner and more explicit about the data flow. The visual below reinforces this concept of injecting a dependency to secure a route.

FastAPI Authentication with Dependency Injection Example
Caption: This image illustrates the same dependency injection pattern, but for user authentication. The `require_user` dependency is run first, and the resulting `user` object is passed into the `do_secret_things` function, just as our `validate_order_id_format` dependency now provides the validated `order_id` to our `get_order_details` function.

4. Verifying the Refactored Logic

Even though we've changed the implementation, the user-facing behavior should be identical. Let's confirm this.

  1. Ensure 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 (http://localhost:8000/mcp) with the correct Authorization header.
  • Test 1 (Valid): Execute the get_order_details tool with order_id set to ORD12345.
    • Expected Result: A successful 200 OK response with the order details.
  • Test 2 (Invalid): Execute the tool with order_id set to invalid-id.
    • Expected Result: A 400 Bad Request error with the detail message: "Invalid order_id format. Must start with 'ORD' followed by digits."

Your pre-tool hook is working exactly as before, but it's now implemented using a more powerful and idiomatic FastAPI pattern.


Conclusion

In this lesson, you refactored your pre-tool hook to use a more versatile dependency injection pattern. This small change from the decorator to the function signature opens up a new world of possibilities for creating more intelligent tools.

Key Takeaways:

  • Dependencies can be applied in the decorator (for side-effects) or in the function signature (to provide values).
  • Applying a dependency in the function signature using Annotated[<type>, Depends(<dependency>)] is the standard way to run validation and pass the sanitized result into your endpoint logic.
  • For this pattern to work, the dependency function should return the validated value upon successful execution.

In our next lesson, we will build directly on this new pattern. We'll create a "gating" dependency that performs a more complex check using business logic—going beyond simple format validation to decide if a tool should be allowed to execute at all.