Skip to content

Commit bb1f7c1

Browse files
RequestContext not captured in QueryBand on FastMCP 3.x
When the dependency was upgraded from FastMCP 2.x to 3.x, set_state and get_state became async and required JSON-serializable values by default. This introduced two silent bugs that caused request_context to always be None (or an unawaited coroutine) when building the Teradata QueryBand, so no per-request fields (REQUEST_ID, SESSION_ID, TENANT, etc.) were ever set. Bug 1 — serialization failure (middleware.py): set_state was called with serializable=True (default), but RequestContext is a plain Python dataclass, not JSON-serializable. FastMCP raised a TypeError that was silently swallowed by the except-at-debug-level block, so the state was never stored. Fixed by passing serializable=False to both set_state calls, routing the value into the request-scoped in-memory dict (_request_state) instead of the persistent store. Bug 2 — async/thread boundary (factory.py, app.py): execute_db_tool is a sync function dispatched via asyncio.to_thread. From inside a worker thread, calling the async get_state without await returns a coroutine object rather than the actual value. Fixed by capturing request_context in the async _mcp_tool (before thread dispatch) using a new _fetch_request_context() helper that properly awaits get_state, then injecting it as _request_context into the executor kwargs. execute_db_tool now pops _request_context from kwargs directly instead of calling get_context()/get_state() from the thread. The now-unused get_context import is removed from app.py.
1 parent 893d176 commit bb1f7c1

3 files changed

Lines changed: 18 additions & 9 deletions

File tree

src/teradata_mcp_server/app.py

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,6 @@
2727
import yaml
2828
from fastmcp import FastMCP
2929
from fastmcp.prompts.prompt import Message, TextContent
30-
from fastmcp.server.dependencies import get_context
3130
from pydantic import BaseModel, Field
3231
from sqlalchemy.engine import Connection
3332

@@ -318,6 +317,7 @@ def execute_db_tool(tool, *args, **kwargs):
318317
context for easier debugging.
319318
"""
320319
tool_name = kwargs.pop("tool_name", getattr(tool, "__name__", "unknown_tool"))
320+
request_context = kwargs.pop("_request_context", None)
321321
tdconn_local = get_tdconn()
322322

323323
if not getattr(tdconn_local, "engine", None):
@@ -334,8 +334,6 @@ def execute_db_tool(tool, *args, **kwargs):
334334
from sqlalchemy import text
335335

336336
with tdconn_local.engine.connect() as conn:
337-
ctx = get_context()
338-
request_context = ctx.get_state("request_context") if ctx else None
339337
qb = build_queryband(
340338
application=mcp.name,
341339
profile=profile_name,
@@ -359,8 +357,6 @@ def execute_db_tool(tool, *args, **kwargs):
359357
else:
360358
raw = tdconn_local.engine.raw_connection()
361359
try:
362-
ctx = get_context()
363-
request_context = ctx.get_state("request_context") if ctx else None
364360
qb = build_queryband(
365361
application=mcp.name,
366362
profile=profile_name,

src/teradata_mcp_server/middleware.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ async def on_request(self, context: MiddlewareContext, call_next):
7474
),
7575
)
7676
if context.fastmcp_context:
77-
await context.fastmcp_context.set_state("request_context", rc)
77+
await context.fastmcp_context.set_state("request_context", rc, serializable=False)
7878
else:
7979
self.logger.warning("No FastMCP context available - RequestContext not stored")
8080
except Exception as e:
@@ -197,7 +197,7 @@ async def on_request(self, context: MiddlewareContext, call_next):
197197
user_id=assume_user,
198198
)
199199
if context.fastmcp_context:
200-
await context.fastmcp_context.set_state("request_context", rc)
200+
await context.fastmcp_context.set_state("request_context", rc, serializable=False)
201201
else:
202202
self.logger.warning("No FastMCP context available - RequestContext not stored")
203203
except Exception as e:

src/teradata_mcp_server/tools/utils/factory.py

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,17 @@
22
import inspect
33

44

5+
async def _fetch_request_context():
6+
"""Fetch RequestContext from FastMCP state in the async layer before thread dispatch."""
7+
try:
8+
from fastmcp.server.dependencies import get_context
9+
10+
ctx = get_context()
11+
return await ctx.get_state("request_context")
12+
except Exception:
13+
return None
14+
15+
516
def create_mcp_tool(
617
*,
718
executor_func=None,
@@ -48,12 +59,14 @@ async def _mcp_tool(**kwargs):
4859
missing = [n for n in required_params if n not in kwargs]
4960
if missing:
5061
raise ValueError(f"Missing required parameters: {missing}")
51-
merged_kwargs = {**inject_kwargs, **kwargs}
62+
request_context = await _fetch_request_context()
63+
merged_kwargs = {**inject_kwargs, **kwargs, "_request_context": request_context}
5264
return await asyncio.to_thread(executor_func, **merged_kwargs)
5365
else:
5466

5567
async def _mcp_tool(**kwargs):
56-
merged_kwargs = {**inject_kwargs, **kwargs}
68+
request_context = await _fetch_request_context()
69+
merged_kwargs = {**inject_kwargs, **kwargs, "_request_context": request_context}
5770
return await asyncio.to_thread(executor_func, **merged_kwargs)
5871

5972
_mcp_tool.__name__ = tool_name

0 commit comments

Comments
 (0)