Skip to main content

Welcome to a Grasp lesson

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

Lesson 16

State-Aware Tool Development

Recommended resources

Introduction

Welcome to our final lesson on building advanced tool patterns with fastapi-mcp. In the last session, you successfully implemented a "post-tool hook" by modifying the get_order_details tool to save the customer_id into the session state using ctx.set_state(). This was the first half of creating a "slot" as you know it from your work with Rasa Pro—the ability to set state.

Today, we complete the cycle. The learning outcome for this lesson is to create a second tool that reads a value from the state manager to influence its own execution logic. We will upgrade the get_customer_profile tool to make it context-aware. It will learn to use the last_customer_id that we stored in the previous step, allowing for more natural, chained conversational commands like: "What are the details for order ORD12345?" followed by, "Now get me that customer's profile."


Upgrading get_customer_profile to be Context-Aware

Our goal is to make get_customer_profile smart. It should still work when a user explicitly provides a customer_id, but it should also work if they don't, by intelligently looking for one in the session state.

Step 1: Modify the Tool's Signature and Inject Context

First, we need to change the function signature to make the customer_id parameter optional. This is the key that unlocks the possibility of calling the tool without an explicit ID. We'll also inject the CurrentContext just as we did before.

In main.py, find the get_customer_profile function and update it:




# main.py




# ... (make sure these are imported)
from typing import Optional, Dict, Any
from fastapi_mcp import CurrentContext
from fastapi import Depends, HTTPException




# ...

@app.get(
    "/customers/{customer_id}",



    # ...
)



# Original function signature:
# async def get_customer_profile(customer_id: int) -> Dict[str, Any]:




# NEW function signature:
async def get_customer_profile(
    ctx: CurrentContext = Depends(),       # 1. Inject the context
    customer_id: Optional[int] = None  # 2. Make customer_id optional
) -> Dict[str, Any]:



    # ... function body will go here

This change will cause a small issue: the path parameter /customers/{customer_id} now conflicts with our optional customer_id parameter. A common and clean way to resolve this in FastAPI is to have two endpoints that map to the same underlying logic. However, for simplicity and to keep our tool logic consolidated, we will change the endpoint to accept the customer_id as a query parameter.

Let's adjust the decorator and function signature together:




# main.py (updated version)




# ... (imports)

@app.get(
    "/customer", # Changed path from /customers/{customer_id}
    operation_id="get_customer_profile",
    description="Retrieves the profile of a customer. If no ID is provided, it uses the customer from the last viewed order.", # Updated description
    tags=["customers"],
    response_model=Customer,
)
async def get_customer_profile(
    ctx: CurrentContext = Depends(),
    customer_id: Optional[int] = None # Now a query parameter
) -> Dict[str, Any]:
    """
    Example endpoint to simulate fetching customer data.
    It can use an explicit customer_id or one from the session state.
    """



    # Logic to be added next

The updated description is critical. It's the instruction manual for the AI model, telling it how and when to use this tool's new context-aware capability.

Step 2: Implement the State-Reading Logic

Now, let's implement the core logic inside the function. The flow should be:

  1. Determine which customer ID to use. Prioritize the explicitly passed customer_id.
  2. If no ID was passed, try to get it from the session state using ctx.get_state().
  3. If there's still no ID, we can't proceed. Raise an error.
  4. If we have an ID, execute the original logic of the tool.

Here's the complete function body:




# main.py




# ... (Pydantic models and other endpoints)

@app.get(
    "/customer",
    operation_id="get_customer_profile",
    description="Retrieves the profile of a customer. If a customer ID is provided, it fetches that specific profile. If no ID is provided, it attempts to use the customer ID from the most recently viewed order.",
    tags=["customers"],
    response_model=Customer,
)
async def get_customer_profile(
    ctx: CurrentContext = Depends(),
    customer_id: Optional[int] = None
) -> Dict[str, Any]:
    """
    Example endpoint to simulate fetching customer data.
    It can use an explicit customer_id or one from the session state.
    """
    effective_customer_id = customer_id
    



    # If no customer_id was passed directly, try to get it from the state
    if effective_customer_id is None:
        print("No customer_id provided, checking session state...")
        id_from_state = await ctx.get_state("last_customer_id")
        if id_from_state:
            print(f"Found customer_id '{id_from_state}' in state.")
            effective_customer_id = id_from_state




    # If we still don't have an ID, we cannot proceed.
    if effective_customer_id is None:
        raise HTTPException(
            status_code=400,
            detail="Customer ID not provided and not found in session context."
        )




    # --- Original tool logic, now using effective_customer_id ---
    print(f"Fetching profile for customer_id: {effective_customer_id}")
    if effective_customer_id == 101:
        return {
            "customer_id": effective_customer_id,
            "name": "Jane Doe",
            "email": "jane.doe@example.com",
            "join_date": "2023-05-20",
        }
        
    raise HTTPException(status_code=404, detail="Customer not found")

This implementation perfectly mirrors the logic of a conversational slot-filling system. It prioritizes explicit information but falls back to stored context when needed.


Testing the Complete Conversational Flow

Your toolset is now capable of a two-step, context-driven conversation. Let's verify it. Make sure your FastApiMCP instance is still exposing both get_order_details and get_customer_profile, and that your server is running.

Test 1: Calling without Context (Failure Case)

First, in a new session, call get_customer_profile without providing an ID. It should fail gracefully.

curl -X POST http://localhost:8000/mcp/get_customer_profile \
-H "Authorization: Bearer your-super-secret-token" \
-H "Content-Type: application/json" \
-d '{}'

Expected Output (a 400 Bad Request):

{
  "detail": "Customer ID not provided and not found in session context."
}

This is correct. The tool requires an ID, and we haven't given it one, either directly or via context.

Test 2: The Full, Context-Aware Flow

Now, let's simulate the intended conversation.

Step A: Set the context by asking for order details.

curl -X POST http://localhost:8000/mcp/get_order_details \
-H "Authorization: Bearer your-super-secret-token" \
-H "Content-Type: application/json" \
-d '{"order_id": "ORD12345"}'

This call succeeds and, as we know from the last lesson, sets last_customer_id to 101 in the session state.

Step B: Use the context by asking for the customer profile without an ID.

curl -X POST http://localhost:8000/mcp/get_customer_profile \
-H "Authorization: Bearer your-super-secret-token" \
-H "Content-Type: application/json" \
-d '{}'

This time, the tool finds last_customer_id in the state and uses it.

Expected Output (Success):

{
  "customer_id": 101,
  "name": "Jane Doe",
  "email": "jane.doe@example.com",
  "join_date": "2023-05-20"
}

Your server logs should confirm this:

No customer_id provided, checking session state...
Found customer_id '101' in state.
Fetching profile for customer_id: 101

You have successfully built a chain of tool calls where the output of one implicitly provides the input for the next.


Conclusion

Congratulations on completing the course! Over these lessons, you've gone from a basic FastAPI application to a sophisticated, context-aware MCP server.

In this final lesson, you completed the state management cycle. You upgraded a tool to read from the session state, enabling it to act intelligently based on previous interactions. This is the foundation of building powerful, multi-step conversational AI agents.

Key Takeaways from this Lesson:

  • You can read from the session state using await ctx.get_state(key).
  • Making tool parameters optional (e.g., Optional[int] = None) is the entry point for implementing context-aware logic.
  • The logic flow—check for explicit input, then check context, then handle missing information—is a robust pattern for building flexible tools.
  • Updating a tool's description is just as important as updating its code. It allows the AI model to understand and leverage the tool's new capabilities.

Course Wrap-up:

You started with the goal of building pre- and post-hooks for your MCP tools. By leveraging FastAPI's powerful dependency injection system, you've done exactly that. You've implemented:

  • Pre-tool hooks for authentication (verify_auth) and input validation (by raising HTTPException based on logic before tool execution).
  • Post-tool hooks for state management (using ctx.set_state after a successful tool call).
  • Context-aware logic that reads from state to create chained, conversational flows.

You now possess the core patterns needed to transform any standard API into a powerful, context-aware toolset for large language models, bridging the gap between your internal services and the world of generative AI.