Introduction
Welcome back! In our previous lesson, we established our strategy for managing state in our conversational AI toolset. We compared global state with session state and concluded that for our purpose—mimicking the "slots" you're familiar with from Rasa Pro—the session state management provided by fastapi-mcp's Context object is the ideal solution. We decided our state manager would use the ctx.set_state() and ctx.get_state() methods.
Today, we move from design to implementation. The learning outcome for this lesson is to modify a tool's endpoint logic to set a 'slot' in the state manager after successfully executing. We will put our design into practice by updating the get_order_details tool. After it successfully fetches an order, it will save the customer_id associated with that order into the session state. This act of saving data after a successful tool call is a fundamental pattern for building multi-step, context-aware interactions.
Implementing the Post-Tool Hook
Let's modify our get_order_details endpoint. The goal is to inject the Context object, and upon successfully finding an order, use ctx.set_state() to store the customer_id. This acts as our "post-hook" logic.
Step 1: Import CurrentContext and Depends
First, you need to import CurrentContext from fastapi_mcp and ensure Depends is imported from fastapi. CurrentContext is the specific class we'll use for dependency injection.
In your main.py file, add CurrentContext to your imports:
# main.py
...
from fastapi import FastAPI, Depends, HTTPException, Request
from fastapi_mcp import FastApiMCP, AuthConfig, CurrentContext # Add CurrentContext here
...
Step 2: Inject the Context into the Tool Endpoint
Next, use FastAPI's dependency injection system to get an instance of the current session's context within your get_order_details function. You do this by adding a new parameter to the function signature with a Depends() default value.
Update the signature of get_order_details as follows:
# main.py (inside get_order_details function)
...
async def get_order_details(
order_id: str,
ctx: CurrentContext = Depends() # Inject the context
) -> Dict[str, Any]:
...
Now, within the body of this function, the variable ctx will hold the context object for the current MCP session, giving us access to our state manager.
Step 3: Set the State After Successful Execution
The final step is to call await ctx.set_state() at the correct moment. We only want to set the "slot" if the order was found. Therefore, the call should come just before the successful return statement.
Here is the complete, modified get_order_details function.
# main.py
# ... (other imports and Pydantic models)
@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,
)
async def get_order_details(
order_id: str,
ctx: CurrentContext = Depends() # 1. Inject the context
) -> Dict[str, Any]:
"""
Example endpoint to simulate fetching order data.
After fetching, it saves the customer_id to the session state.
"""
# Placeholder data
if order_id == "ORD12345":
order_data = {
"order_id": order_id,
"customer_id": 101,
"status": "shipped",
"total_amount": 199.99,
"currency": "USD",
}
# 2. Set the state ("slot") after a successful operation
# This is our post-tool hook logic.
print(f"Setting state: last_customer_id = {order_data['customer_id']}")
await ctx.set_state("last_customer_id", order_data["customer_id"])
return order_data # 3. Return the original response
raise HTTPException(status_code=404, detail="Order not found")
Notice the logic:
- We successfully retrieve the
order_data. - We use
await ctx.set_state("last_customer_id", order_data["customer_id"])to save the customer ID into the session state. The key"last_customer_id"is our slot name. - We then return the
order_dataas before. The tool's primary function is unchanged.
If an invalid order_id is passed, an HTTPException is raised, and the set_state line is never reached. This ensures our state remains clean and is only updated upon success.
Verifying the State Change
How can we be sure the state was set correctly? The set_state method doesn't return a confirmation. We need a way to inspect the session state.
The best way to do this is to create a temporary "debug" tool whose sole purpose is to read and display the entire state for the current session. This will make the abstract concept of "session state" tangible.
Create a Debug Tool to Read State
Let's add a new endpoint to our main.py that will act as this debug tool.
# main.py (add this new endpoint)
@app.get(
"/debug/session-state",
operation_id="get_session_state",
description="FOR DEBUGGING: Retrieves the entire current session state.",
tags=["debug"],
)
async def get_session_state(ctx: CurrentContext = Depends()) -> Dict[str, Any]:
"""
Returns all key-value pairs stored in the current MCP session's state.
"""
# The get_state() method with no arguments returns the whole state dictionary.
state = await ctx.get_state()
return state if state is not None else {}
This tool uses the same CurrentContext dependency. The ctx.get_state() method, when called without a key, conveniently returns the entire state dictionary for the session.
Expose the New Tool
Now, update your FastApiMCP instance to include this new tool.
# main.py (in the MCP Integration section)
mcp = FastApiMCP(
app,
name="Apxml API Services",
description="Tools for managing orders and customers.",
# ... other parameters
include_operations=[
"get_order_details",
"get_customer_profile",
"get_session_state", # Add the new debug tool
],
auth_config=AuthConfig(
dependencies=[Depends(verify_auth)],
),
)
Test the Workflow
With your server running (uv run uvicorn main:app --reload), you can now test the entire flow using curl or the MCP Inspector.
1. Initial State Check (Optional)
First, call the debug tool to see the initial empty state.
curl -X POST http://localhost:8000/mcp/get_session_state \
-H "Authorization: Bearer your-super-secret-token" \
-H "Content-Type: application/json" \
-d '{}'
Expected Output:
{}
2. Call get_order_details to Set the State
Now, call the tool that sets the slot.
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"}'
Expected Output:
{"order_id":"ORD12345","customer_id":101,"status":"shipped","total_amount":199.99,"currency":"USD"}
In your server logs, you should see our print statement: Setting state: last_customer_id = 101.
3. Verify the State was Set
Finally, call the debug tool again.
curl -X POST http://localhost:8000/mcp/get_session_state \
-H "Authorization: Bearer your-super-secret-token" \
-H "Content-Type: application/json" \
-d '{}'
Expected Output:
{"last_customer_id": 101}
Success! The last_customer_id has been persisted in the session state, just as a slot would be in a conversational framework.
Conclusion
In this lesson, you successfully implemented a post-tool hook to manage conversational state. You took the state manager we designed in the previous lesson and put it into practice.
Key Takeaways:
- You can access the session-specific
Contextobject in any tool endpoint by addingctx: CurrentContext = Depends()as a function parameter. - The
await ctx.set_state(key, value)method allows you to save data to the session state, effectively "setting a slot." - This state-setting logic should be placed to run only after the tool's primary task has succeeded, ensuring state integrity.
- Creating a simple debug tool to inspect the session state is an invaluable technique for verifying your logic.
Next Up:
We can now reliably set a slot. The next logical step is to use that slot. In the next lesson, we will create a second tool that reads from the state manager. We will modify get_customer_profile so it can use the last_customer_id we just stored, enabling more natural, chained interactions like, "What are the details for order ORD12345?" followed by "Now get me that customer's profile."