Context Injection for MCP Tools¶
Zap automatically passes context (user data, session info, tenant IDs) to your MCP tools without exposing it to the LLM. This enables secure multi-tenancy, user-scoped operations, and personalized tool behavior while keeping sensitive identifiers hidden from the AI model.
Basic Usage¶
Pass context when executing a task, then access it in your tools using FastMCP's dependency injection system:
Client code:
from zap_ai import Zap, ZapAgent
from fastmcp import Client
# Define your agent
agent = ZapAgent(
name="DocumentAgent",
prompt="You help users find their documents.",
mcp_clients=[Client("./document_tools.py")],
)
zap = Zap(agents=[agent])
await zap.start()
# Pass context with user/tenant information
result = await zap.execute_task(
agent_name="DocumentAgent",
task="Show me my recent documents",
context={"user_id": "user_123", "tenant": "acme_corp"}
)
Tool code (MCP server):
from fastmcp import FastMCP
from fastmcp.dependencies import Depends
from zap_ai.mcp.context import ZapContext
mcp = FastMCP("Document Tools")
@mcp.tool()
async def search_documents(
query: str,
limit: int = 10,
zap_ctx: dict = Depends(ZapContext)
) -> str:
"""Search for documents in the user's tenant."""
# Extract context values
user_id = zap_ctx.get("user_id")
tenant = zap_ctx.get("tenant")
if not user_id or not tenant:
return "Error: No user context provided"
# Query with tenant/user scoping
results = await db.search_documents(
query=query,
user_id=user_id,
tenant=tenant,
limit=limit
)
return format_results(results)
The zap_ctx: dict = Depends(ZapContext) parameter is hidden from the LLM via FastMCP's dependency injection system. The AI model only sees query and limit in the tool schema.
Typed Context with Automatic Deserialization¶
For better type safety and IDE support, use Pydantic models or dataclasses for your context. Zap automatically preserves type information, enabling type-safe access in tools.
Define a typed context:
from pydantic import BaseModel
class UserContext(BaseModel):
user_id: str
tenant: str
permissions: list[str] = []
preferences: dict[str, str] = {}
Client code with typed context:
from zap_ai import Zap, ZapAgent
agent = ZapAgent[UserContext](
name="DocumentAgent",
prompt="You help users manage their documents.",
mcp_clients=[Client("./document_tools.py")],
)
zap = Zap(agents=[agent])
await zap.start()
# Pass a Pydantic model - type metadata is automatically added
context = UserContext(
user_id="user_123",
tenant="acme_corp",
permissions=["read", "write", "delete"],
preferences={"view": "grid", "sort": "date"}
)
result = await zap.execute_task(
agent_name="DocumentAgent",
task="Delete the file named 'draft.txt'",
context=context
)
Tool with typed deserialization:
from fastmcp import FastMCP
from fastmcp.dependencies import Depends
from zap_ai.mcp.context import TypedZapContext
mcp = FastMCP("Document Tools")
# Create a reusable typed dependency
CurrentUser = TypedZapContext(UserContext)
@mcp.tool()
async def delete_document(
filename: str,
user_ctx: UserContext = Depends(CurrentUser)
) -> str:
"""Delete a document."""
# user_ctx is fully typed with IDE autocomplete
if "delete" not in user_ctx.permissions:
return "Error: You don't have permission to delete documents"
await db.delete_document(
filename=filename,
user_id=user_ctx.user_id,
tenant=user_ctx.tenant
)
return f"Deleted '{filename}'"
Benefits of typed context: - ✅ IDE Support: Full autocomplete and type checking - ✅ Refactoring Safety: Rename fields with confidence - ✅ Runtime Validation: Pydantic validates types automatically - ✅ Zero Configuration: Works automatically for Pydantic models and dataclasses - ✅ Backward Compatible: Falls back to dict if type unavailable
Context Access Patterns¶
Zap provides three dependency injection helpers for accessing context. Choose based on your needs:
ZapContext - Full Context Dict¶
Extract the complete context as a dictionary.
Use when: You need the complete context or working with plain dict contexts.
from fastmcp import FastMCP
from fastmcp.dependencies import Depends
from zap_ai.mcp.context import ZapContext
mcp = FastMCP("My Service")
@mcp.tool()
async def my_tool(
query: str,
zap_ctx: dict = Depends(ZapContext)
) -> str:
"""A tool that needs full context."""
user_id = zap_ctx.get("user_id")
tenant = zap_ctx.get("tenant")
# ... use values
Note: If the context was created with a Pydantic model or dataclass, ZapContext will automatically deserialize it back to the typed object (not a dict). To always get a dict, access individual fields using ZapContextValue instead.
ZapContextValue(key, default) - Extract Specific Values¶
Create a dependency that extracts a specific value from context with a default.
Use when: You only need one or two specific values.
from fastmcp import FastMCP
from fastmcp.dependencies import Depends
from zap_ai.mcp.context import ZapContextValue
mcp = FastMCP("Search Service")
# Create reusable dependencies
Tenant = ZapContextValue("tenant", "default")
UserId = ZapContextValue("user_id")
@mcp.tool()
async def tenant_search(
query: str,
tenant: str = Depends(Tenant),
user_id: str | None = Depends(UserId)
) -> str:
"""Search within tenant's data scope."""
if not user_id:
return "Error: User not authenticated"
return await search_engine.search(query, tenant_filter=tenant)
Benefits: - Cleaner function signatures - Reusable dependencies across tools - Explicit defaults in one place
TypedZapContext(context_type) - Type-Safe Extraction¶
Create a dependency that deserializes context to a specific typed object with validation.
Use when: You want guaranteed type safety for critical operations.
from dataclasses import dataclass
from fastmcp import FastMCP
from fastmcp.dependencies import Depends
from zap_ai.mcp.context import TypedZapContext
@dataclass
class SessionContext:
user_id: str
authenticated: bool
role: str
mcp = FastMCP("Payment Service")
# Create typed dependency
CurrentSession = TypedZapContext(SessionContext)
@mcp.tool()
async def charge_payment(
amount: float,
session: SessionContext = Depends(CurrentSession)
) -> str:
"""Process a payment."""
# session is guaranteed to be SessionContext or raises TypeError
if not session.authenticated:
return "Error: Not authenticated"
return await payment_service.charge(session.user_id, amount)
Error handling: Raises TypeError if context cannot be deserialized to the expected type.
Security Best Practices¶
1. Context is hidden from the LLM¶
The LLM cannot see or manipulate context parameters. FastMCP's dependency injection system automatically filters these parameters from the tool schema shown to the model.
2. Use for identifiers, not secrets¶
Context travels through the Temporal workflow and MCP protocol. Pass user IDs, tenant identifiers, and session metadata—not API keys, passwords, or credentials. Use environment variables or tool-specific configuration for secrets.
# Good
context = {"user_id": "user_123", "tenant": "acme_corp", "role": "admin"}
# Bad - don't put secrets in context
context = {"user_id": "user_123", "api_key": "sk_live_123..."}
3. Always validate context in tools¶
Check that required context values exist and have valid types:
from fastmcp import FastMCP
from fastmcp.dependencies import Depends
from zap_ai.mcp.context import TypedZapContext
mcp = FastMCP("Protected Service")
CurrentUser = TypedZapContext(UserContext)
@mcp.tool()
async def protected_action(
action: str,
user_ctx: UserContext = Depends(CurrentUser)
) -> str:
"""Perform a protected action."""
# TypedZapContext raises TypeError if context is invalid
# At this point, user_ctx is guaranteed to be UserContext
if not user_ctx.user_id:
return "Error: User ID required"
# Safe to proceed
return await perform_action(user_ctx, action)
4. Type safety reduces bugs¶
Using Pydantic models or dataclasses provides compile-time checking and reduces runtime errors:
# With types: IDE catches errors immediately
user_ctx.usr_id # TypeError: 'UserContext' has no attribute 'usr_id'
# Without types: Runtime error only when code executes
zap_ctx.get("usr_id") # Returns None, silent bug
5. Keep context minimal¶
Only include what tools actually need. Smaller contexts are faster to serialize and easier to debug.
How It Works¶
Context flows from your Zap workflow to MCP tools through FastMCP's meta parameter:
┌─────────────────────────────────────────────────────────────────┐
│ Zap Workflow │
│ │
│ context (Pydantic/dataclass/dict) → serialize with metadata │
└─────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ Temporal Workflow Activity │
│ │
│ client.call_tool(name, args, meta={"zap_context": context}) │
└─────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ FastMCP Server │
│ │
│ @mcp.tool() │
│ async def my_tool(query: str, ctx = Depends(ZapContext)): │
│ # ZapContext extracts from meta, deserializes if typed │
│ # Dependency parameter hidden from LLM schema │
└─────────────────────────────────────────────────────────────────┘
Context Serialization¶
When you pass a Pydantic model or dataclass, Zap automatically adds type metadata:
# Your context
UserContext(user_id="123", tenant="acme")
# Serialized with metadata
{
"user_id": "123",
"tenant": "acme",
"__zap_context_type__": "myapp.models.UserContext",
"__zap_context_version__": "1"
}
The ZapContext and TypedZapContext dependencies use this metadata to reconstruct the original type:
1. Dynamically imports the type using importlib
2. Validates the type if using TypedZapContext
3. Reconstructs using model_validate() (Pydantic) or constructor (dataclass)
4. Falls back to dict with warning if type cannot be imported
Plain dicts pass through unchanged without metadata.
Why LLM Can't See Context¶
FastMCP's dependency injection system uses Depends() to inject dependencies. When FastMCP generates the tool schema for the LLM, it filters out all dependency parameters. The LLM only sees the "regular" parameters (like query and limit), never the context.
Advanced: Dependency Injection Pattern¶
The Zap context injection system follows FastMCP's standard dependency injection pattern:
# These are dependency factories - they return functions that extract context
ZapContext # Returns: (ctx: Context) -> dict | typed_object
ZapContextValue(k) # Returns: (ctx: Context) -> Any
TypedZapContext(T) # Returns: (ctx: Context) -> T
# Use with Depends() in your tools
@mcp.tool()
async def tool(
param: str,
# Depends() tells FastMCP to call the dependency function
ctx_value: dict = Depends(ZapContext)
):
...
Important: The context class must be importable from the MCP tool's Python environment. If you define UserContext in your client code but the MCP server can't import it, deserialization will fall back to dict.
Custom Context Types¶
Any class with these methods works:
- Pydantic v2: model_validate(data) for reconstruction
- Dataclass: Constructor accepts **kwargs
Custom classes without these methods will return as dict.
Troubleshooting¶
"Could not import context type" warning¶
Cause: The context class isn't available in the MCP tool's Python environment.
Solution:
- Define shared context types in a common module both client and server can import
- Or use plain dicts for simple cases
- The tool will still work with dict access via ZapContext
Context is empty ({}) in tools¶
Cause: Either no context was passed, or context didn't flow through the workflow.
Solution:
- Verify you're passing context= parameter to execute_task()
- Check that FastMCP version is >= 2.13.1 (meta parameter support)
- Ensure you're using Depends(ZapContext) or similar in your tool signature
TypeError: Expected context of type X¶
Cause: Using TypedZapContext but the actual context doesn't match the expected type.
Solution:
- Verify you're passing the correct context type in execute_task()
- Check that type metadata is present (context was created with Pydantic/dataclass)
- Use plain ZapContext if you want automatic fallback to dict
Context returns {} but I passed context¶
Cause: The dependency returns {} instead of None when no context exists.
Solution: Check for specific required fields instead:
from fastmcp.dependencies import Depends
from zap_ai.mcp.context import ZapContext
@mcp.tool()
async def tool(zap_ctx: dict = Depends(ZapContext)) -> str:
if not zap_ctx.get("user_id"):
return "Error: user_id required"
...
API Reference¶
ZapContext¶
A FastMCP dependency that deserializes the full ZapContext. Use with Depends().
Returns: Deserialized context dict, or the typed object if context was created with Pydantic/dataclass. Returns {} if no context provided.
Example:
from fastmcp import FastMCP
from fastmcp.dependencies import Depends
from zap_ai.mcp.context import ZapContext
@mcp.tool()
async def my_tool(zap_ctx: dict = Depends(ZapContext)) -> str:
user_id = zap_ctx.get("user_id")
...
ZapContextValue(key: str, default: Any = None)¶
Factory that creates a FastMCP dependency for extracting a specific value from context.
Parameters:
- key: The key to extract from context
- default: Default value if key not present (default: None)
Returns: A dependency function suitable for use with Depends()
Example:
from fastmcp import FastMCP
from fastmcp.dependencies import Depends
from zap_ai.mcp.context import ZapContextValue
# Create reusable dependencies
Tenant = ZapContextValue("tenant", "default")
UserId = ZapContextValue("user_id")
@mcp.tool()
async def tool(
tenant: str = Depends(Tenant),
user_id: str | None = Depends(UserId)
) -> str:
...
TypedZapContext(context_type: type[T])¶
Factory that creates a FastMCP dependency for type-safe context deserialization with validation.
Parameters:
- context_type: The expected context class (Pydantic model or dataclass)
Returns: A dependency function that deserializes to the specified type
Raises: TypeError if context cannot be deserialized to the expected type
Example:
from dataclasses import dataclass
from fastmcp import FastMCP
from fastmcp.dependencies import Depends
from zap_ai.mcp.context import TypedZapContext
@dataclass
class SessionContext:
user_id: str
role: str
# Create typed dependency
CurrentSession = TypedZapContext(SessionContext)
@mcp.tool()
async def tool(session: SessionContext = Depends(CurrentSession)) -> str:
# session is guaranteed to be SessionContext
...
Requirements¶
- FastMCP >= 2.13.1 for
metaparameter support - Pydantic v2 if using Pydantic models for context
- Context classes must be importable in the MCP tool's Python environment