Chapter 6: Talking Back - FastMCP Context (Context
)
In Chapter 5: Reusable Chat Starters - FastMCP Prompts (Prompt
, PromptManager
), we learned how to create reusable message templates for interacting with AI models. We’ve seen how to build servers with data resources (Chapter 3) and action tools (Chapter 4).
But imagine you have a tool that takes a while to run, like processing a large file or making a complex calculation. How does your tool communicate back to the user while it’s running? How can it say “I’m 50% done!” or log important steps? Or what if a tool needs to read some data from one of the server’s resources to do its job?
This is where the Context
object comes in. It’s like giving your tool function a temporary backstage pass for the specific request it’s handling. This pass grants it access to special features like sending logs, reporting progress, or accessing other parts of the server environment related to that request.
What is Context
?
The Context
object is a special helper object provided by the FastMCP
framework. If you define a tool function (or a resource function) that includes a parameter specifically typed as Context
, FastMCP
will automatically create and pass this object to your function when it’s called.
Think of it this way:
- Each client request (like
callTool
orreadResource
) is like a separate event. - For that specific event,
FastMCP
can provide aContext
object. - This
Context
object holds information about that specific request (like its unique ID). - It also provides methods (functions) to interact with the ongoing session, such as:
- Sending log messages back to the client (
ctx.info
,ctx.debug
, etc.). - Reporting progress updates (
ctx.report_progress
). - Reading data from other resources defined on the server (
ctx.read_resource
).
- Sending log messages back to the client (
It’s your function’s way of communicating out or accessing shared server capabilities during its execution for a particular request.
Getting Access: Asking for the Context
How do you tell FastMCP
that your function needs this backstage pass? You simply add a parameter to your function definition and use a type hint to mark it as Context
.
Let’s create a tool that simulates a long-running task and uses Context
to report progress and log messages.
File: long_task_server.py
import anyio # For simulating delay with sleep
from mcp.server.fastmcp import FastMCP
# 1. Import the Context type
from mcp.server.fastmcp.server import Context
# Create the server instance
server = FastMCP(name="LongTaskServer")
# Define our tool function
# 2. Add a parameter (e.g., 'ctx') and type hint it as 'Context'
@server.tool(name="long_task", description="Simulates a task that takes time.")
async def run_long_task(duration_seconds: int, ctx: Context) -> str:
"""
Simulates a task, reporting progress and logging using Context.
"""
# 3. Use the context object!
await ctx.info(f"Starting long task for {duration_seconds} seconds.")
total_steps = 5
for i in range(total_steps):
step = i + 1
await ctx.debug(f"Working on step {step}/{total_steps}...")
# Simulate work
await anyio.sleep(duration_seconds / total_steps)
# Report progress (current step, total steps)
await ctx.report_progress(step, total_steps)
await ctx.info("Long task completed!")
return f"Finished simulated task of {duration_seconds} seconds."
# Standard run block
if __name__ == "__main__":
print(f"Starting {server.name}...")
server.run()
print(f"{server.name} finished.")
Explanation:
from mcp.server.fastmcp.server import Context
: We import the necessaryContext
class.async def run_long_task(duration_seconds: int, ctx: Context)
:- We define our tool function as usual.
- Crucially, we add a parameter named
ctx
. You can name it anything (likecontext
,req_ctx
), butctx
is common. - We add the type hint
: Context
after the parameter name. This is the signal toFastMCP
to inject the context object here.
- Using
ctx
: Inside the function, we can now use the methods provided by thectx
object:await ctx.info(...)
: Sends an informational log message back to the client connected to this session.await ctx.debug(...)
: Sends a debug-level log message. There are alsowarning
anderror
methods.await ctx.report_progress(step, total_steps)
: Sends a progress update to the client. The client application might display this in a progress bar.
When a client calls the long_task
tool, FastMCP
will:
- See the
ctx: Context
parameter. - Create a
Context
object specific to this request. - Call your
run_long_task
function, passing the duration and the newly createdctx
object. - Your function runs, and calls like
ctx.info
orctx.report_progress
send messages back to the client during the execution of the tool.
Using Context
to Access Resources
The Context
object isn’t just for sending information out; it can also be used to access other parts of the server, like reading resources defined using @server.resource
.
Let’s modify our example. Imagine our long task needs some configuration data stored in a resource.
File: long_task_server_with_resource.py
import anyio
from mcp.server.fastmcp import FastMCP
from mcp.server.fastmcp.server import Context
# Create the server instance
server = FastMCP(name="LongTaskServer")
# Define a simple resource that holds some config data
@server.resource(uri="config://task_settings", description="Settings for the long task.")
def get_task_settings() -> str:
"""Returns task settings as a simple string."""
# In a real app, this might load from a file or database
print("Resource 'config://task_settings' was read!")
return "Default speed: Normal" # Simple example setting
# Define our tool function
@server.tool(name="long_task", description="Simulates a task using config resource.")
async def run_long_task(duration_seconds: int, ctx: Context) -> str:
"""
Simulates a task, reads config via Context, reports progress.
"""
await ctx.info(f"Starting long task for {duration_seconds} seconds.")
# 1. Use context to read the resource
try:
# read_resource returns a list of content chunks
resource_contents = await ctx.read_resource("config://task_settings")
# Assuming simple text content for this example
settings = ""
for content_part in resource_contents:
if hasattr(content_part, 'content') and isinstance(content_part.content, str):
settings = content_part.content
break
await ctx.info(f"Loaded settings: {settings}")
except Exception as e:
await ctx.warning(f"Could not read task settings: {e}")
total_steps = 5
for i in range(total_steps):
step = i + 1
await ctx.debug(f"Working on step {step}/{total_steps}...")
await anyio.sleep(duration_seconds / total_steps)
await ctx.report_progress(step, total_steps)
await ctx.info("Long task completed!")
return f"Finished simulated task of {duration_seconds} seconds using settings."
# Standard run block
if __name__ == "__main__":
print(f"Starting {server.name}...")
server.run()
print(f"{server.name} finished.")
Explanation:
@server.resource(...)
: We added a simple resource namedconfig://task_settings
that just returns a string.resource_contents = await ctx.read_resource("config://task_settings")
: Inside ourrun_long_task
tool, we now usectx.read_resource()
to fetch the content of our configuration resource. This allows the tool to dynamically access data managed by the server without having direct access to the resource’s implementation function (get_task_settings
).- Processing Content: The
read_resource
method returns an iterable ofReadResourceContents
objects (often just one). We extracted the string content to use it.
Now, our tool can both communicate outwards (logs, progress) and interact inwards (read resources) using the same Context
object, all within the scope of the single request it’s handling.
How Context
Works Under the Hood
It feels like magic that just adding : Context
gives your function these powers, but it’s a well-defined process within FastMCP
.
- Request Arrives: A client sends a request, for example,
callTool
for ourlong_task
. - Low-Level Handling: The underlying
MCPServer
receives the request and creates aRequestContext
object. This low-level context holds the raw request details, a reference to the currentServerSession
, and the request ID. FastMCP
Takes Over: The request is routed to the appropriateFastMCP
handler method (e.g.,FastMCP.call_tool
).- Context Creation: Before calling the actual tool function,
FastMCP
calls its internalget_context()
method. This method creates the high-levelContext
object we use. It wraps the low-levelRequestContext
and also adds a reference to theFastMCP
server instance itself. - Function Inspection: The
ToolManager
(when asked to run the tool) inspects the signature of your target function (run_long_task
). It sees thectx: Context
parameter. - Injection: The
ToolManager
(specifically theTool.run
method which usesFuncMetadata.call_fn_with_arg_validation
) knows it needs to provide aContext
object. It takes theContext
created in step 4 and passes it as the argument for thectx
parameter when calling yourrun_long_task
function. - Execution: Your function runs. When you call
ctx.info("...")
, theContext
object uses its reference to the underlyingRequestContext
andServerSession
to send the appropriate log message back to the client via the session. Similarly,ctx.report_progress
uses the session, andctx.read_resource
uses the reference to theFastMCP
instance to call itsread_resource
method.
Simplified Sequence Diagram (callTool
with Context
):
sequenceDiagram
participant Client
participant FastMCPServer as FastMCP (server.py)
participant ToolMgr as ToolManager (_tool_manager)
participant ToolRunner as Tool.run / FuncMetadata
participant YourToolFunc as run_long_task(ctx: Context)
participant ContextObj as Context
Client->>+FastMCPServer: callTool(name="long_task", args={...})
FastMCPServer->>FastMCPServer: Create low-level RequestContext
FastMCPServer->>+ContextObj: Create Context (wraps RequestContext, FastMCP)
FastMCPServer->>+ToolMgr: call_tool(name="long_task", args={...})
ToolMgr->>+ToolRunner: run(arguments={...}, context=ContextObj)
ToolRunner->>ToolRunner: Inspect run_long_task, see 'ctx: Context'
ToolRunner->>+YourToolFunc: Call run_long_task(duration=..., ctx=ContextObj)
YourToolFunc->>ContextObj: ctx.info("Starting...")
ContextObj->>FastMCPServer: Use session.send_log_message(...)
YourToolFunc->>ContextObj: ctx.report_progress(...)
ContextObj->>FastMCPServer: Use session.send_progress_notification(...)
YourToolFunc->>ContextObj: ctx.read_resource("config://...")
ContextObj->>FastMCPServer: Call fastmcp.read_resource("config://...")
FastMCPServer-->>ContextObj: Return resource content
ContextObj-->>YourToolFunc: Return resource content
YourToolFunc-->>-ToolRunner: Return "Finished..."
ToolRunner-->>-ToolMgr: Return "Finished..."
ToolMgr-->>-FastMCPServer: Return "Finished..."
FastMCPServer->>-Client: Send Response: result="Finished..."
Looking at the Code (Briefly):
-
Context Creation (
server/fastmcp/server.py
): TheFastMCP.get_context
method is responsible for creating theContext
object when needed, typically just before calling a tool or resource handler. It grabs the low-level context and wraps it.# Inside server/fastmcp/server.py (Simplified FastMCP.get_context) from mcp.shared.context import RequestContext # Low-level context class FastMCP: # ... (other methods) ... def get_context(self) -> Context[ServerSession, object]: """Returns a Context object.""" try: # Get the low-level context for the current request request_context: RequestContext | None = self._mcp_server.request_context except LookupError: request_context = None # Not available outside a request # Create our high-level Context, passing the low-level one # and a reference to this FastMCP instance ('self') return Context(request_context=request_context, fastmcp=self)
-
Context Injection (
server/fastmcp/tools/base.py
): TheTool.from_function
method inspects the function signature to see if aContext
parameter exists and stores its name (context_kwarg
). Later,Tool.run
uses this information (viaFuncMetadata
) to pass the context object when calling your function.# Inside server/fastmcp/tools/base.py (Simplified Tool.from_function) class Tool(BaseModel): # ... fields ... context_kwarg: str | None = Field(...) @classmethod def from_function(cls, fn, ...) -> Tool: # ... other inspection ... context_param_name = None sig = inspect.signature(fn) for param_name, param in sig.parameters.items(): # Check if the type hint is Context if param.annotation is Context: context_param_name = param_name break # ... create FuncMetadata, skipping context arg ... return cls( # ..., context_kwarg=context_param_name, # ... ) # Inside Tool.run (simplified concept) async def run(self, arguments, context=None): # ... validate args ... kwargs_for_fn = validated_args if self.context_kwarg and context: # Add the context object to the arguments passed to the function kwargs_for_fn[self.context_kwarg] = context # Call the original function (self.fn) result = await self.fn(**kwargs_for_fn) # Or sync call return result
-
Context Implementation (
server/fastmcp/server.py
): TheContext
class itself implements methods likeinfo
,report_progress
,read_resource
by calling methods on the stored_request_context.session
or_fastmcp
instance.# Inside server/fastmcp/server.py (Simplified Context methods) class Context(BaseModel, Generic[ServerSessionT, LifespanContextT]): _request_context: RequestContext[...] | None _fastmcp: FastMCP | None # ... (init, properties) ... async def report_progress(self, progress, total=None): # Get progress token from low-level context meta if available progress_token = self.request_context.meta.progressToken if self.request_context.meta else None if progress_token: # Use the session object from the low-level context await self.request_context.session.send_progress_notification(...) async def read_resource(self, uri): # Use the stored FastMCP instance assert self._fastmcp is not None return await self._fastmcp.read_resource(uri) async def log(self, level, message, ...): # Use the session object from the low-level context await self.request_context.session.send_log_message(...) async def info(self, message, **extra): await self.log("info", message, **extra) # ... (debug, warning, error methods) ...
Conclusion
You’ve learned about the Context
object in FastMCP
– your function’s essential backstage pass during a request.
Context
provides access to request-specific information and server capabilities.- You gain access by adding a parameter type-hinted as
Context
to your tool or resource function definition. - It allows your functions to:
- Send log messages (
ctx.info
,ctx.debug
, etc.). - Report progress (
ctx.report_progress
). - Read server resources (
ctx.read_resource
). - Access request details (
ctx.request_id
).
- Send log messages (
FastMCP
automatically creates and injects theContext
object when your function is called for a specific request.
The Context
object is key to building more interactive and communicative tools and resources that can provide feedback to the user and interact with their environment during execution.
So far, we’ve focused on the high-level abstractions FastMCP
provides (Tool
, Resource
, Prompt
, Context
). In the next chapter, we’ll take a step back and look at the fundamental data structures defined by the MCP specification itself: Chapter 7: MCP Protocol Types. Understanding these types helps clarify the data being exchanged between clients and servers under the hood.
Generated by AI Codebase Knowledge Builder