abhishekkumarsengar opened a new issue, #241:
URL: https://github.com/apache/doris-mcp-server/issues/241
When multiple tokens with different database_config bindings send requests
concurrently, queries are silently executed using the wrong token's database
connection pool. This causes queries to run as the wrong database user,
resulting in timeouts or incorrect data access.
**Root Cause**
The server sets the authenticated token in mcp_auth_context_var (a
contextvars.ContextVar) inside the ASGI HTTP handler:
```
# doris_mcp_server/main.py
mcp_auth_context_var.set(auth_context)
await session_manager.handle_request(scope, receive, send)
```
However, the MCP framework (mcp/server/lowlevel/server.py) spawns a new
asyncio task to handle each tool call via anyio's tg.start_soon():
```
# mcp/server/lowlevel/server.py
async with anyio.create_task_group() as tg:
async for message in session.incoming_messages:
tg.start_soon(self._handle_message, message, ...)
```
anyio's tg.start_soon() does not copy the current contextvars context into
the spawned task (unlike asyncio.create_task() which does copy context since
Python 3.7). As a result, mcp_auth_context_var.get() returns None inside the
tool execution task.
With no token available, execute_query falls back to the hardcoded
session_id="mcp_session" as the pool cache key. Since this key is shared across
all tokens on the same worker process, whichever token first creates a pool
entry under "mcp_session" permanently contaminates it for all subsequent
requests on that worker — even requests from completely different tokens with
different credentials.
**Observed Behaviour**
- Token A (with valid database_config) sends a query
- Token B (with no database_config, falls back to global-env) runs first and
creates token_pools["mcp_session"] using global env credentials
- All subsequent requests from Token A get Token B's pool — wrong DB user,
wrong host
- Queries time out with the misleading error: "Failed to get connection for
authenticated token. This is a security measure to prevent using default
high-privilege credentials. Error: " (the trailing Error: is empty because it's
a silent asyncio.TimeoutError)
- The contaminated pool is never evicted — the worker is permanently broken
until restarted
**Expected Behaviour**
Each token's query should always use that token's own database_config
credentials, regardless of request ordering or which other tokens have
previously made requests on the same worker.
What the server is supposed to do
Every token in tokens.json has its own database_config binding — its own
user, password and host. When a request comes in, the server is supposed to:
1.Read the bearer token from the HTTP header
2.Look up its database_config from tokens.json
3.Open a connection pool for that specific token's credentials
4.Run the query as that token's DB user
What actually happens — the ContextVar bug
Python's contextvars.ContextVar is the mechanism the server uses to pass the
authenticated token from the HTTP layer down to the database layer. The problem
is it only works within the same asyncio task. The MCP framework spawns a new
task to handle tool calls, and that new task starts with an empty context.
┌─────────────────────────────────────────────────────────────────┐
│ TASK A (HTTP handler — one per incoming HTTP request) │
│ │
│ 1. Reads: Authorization: Bearer eXXXXX… │
│ 2. Looks up token → test_user credentials │
│ 3. mcp_auth_context_var.SET(test_user_context) ✓ │
│ │
│ 4. MCP framework: asyncio.create_task(run_tool(...)) │
│ │ │
│ │ ← no copy_context() here │
└──────────────────────────────┼──────────────────────────────────┘
│
┌──────────────────────────────▼──────────────────────────────────┐
│ TASK B (Tool execution — spawned by MCP framework) │
│ │
│ mcp_auth_context_var.GET() → None ✗ (empty context) │
│ │
│ No token → falls back to session_id = "mcp_session" │
│ Looks up pool cache: token_pools["mcp_session"] │
│ Gets → WHOEVER created this entry first │
└─────────────────────────────────────────────────────────────────┘
```
### The cache poisoning — from your real logs
Your logs show exactly when this happened:
```
08:26:53 — load_testing_token_id created (no database_config → uses
global-env)
08:33:44 — Creating dedicated pool (hash: 8f400c25) using global-env config:
Admin_user @ xx.xxx.xx.xxx
↑ this is load_testing_token_id's pool, stored under key
"mcp_session"
```
Because `session_id` is hardcoded as `"mcp_session"` for every caller, **the
first token to execute a query wins the cache entry permanently**. In your case
`load_testing_token_id` ran first, had no DB credentials (used global-env →
admin user), and poisoned the cache:
```
token_pools = {
"mcp_session" → pool(admin @ xx.xxx.xx.xxx) ← set by
load_testing_token_id
}
```
From that moment, **every token on that pod** — including your `Admin_user`
— gets the admin pool regardless of their own credentials:
```
08:33:44 onwards:
Your request: token = eXXXXX... (Admin_user)
→ Task B: ContextVar = None → session_id = "mcp_session"
→ pool cache hit → admin @ xx.xxx.xx.xxx
→ query runs as admin, times out after 30s
```
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]