From 49e2db8331ce0f30e2f26a7f5150b53bf480ade9 Mon Sep 17 00:00:00 2001 From: huanghuoguoguo Date: Sun, 11 Oct 2026 02:34:44 +0800 Subject: [PATCH] fix(api): scope MCP tool child tasks --- src/langbot/pkg/api/mcp/server.py | 35 ++++++++++++++++++++++++++++++- 1 file changed, 34 insertions(+), 1 deletion(-) diff --git a/src/langbot/pkg/api/mcp/server.py b/src/langbot/pkg/api/mcp/server.py index bb0445bce..7fa1293b9 100644 --- a/src/langbot/pkg/api/mcp/server.py +++ b/src/langbot/pkg/api/mcp/server.py @@ -18,10 +18,13 @@ maintainable. Extend deliberately. from __future__ import annotations import json +import functools import typing from mcp.server.fastmcp import FastMCP +from ..application import ApplicationAPI + from ..http.authz import Permission, require_permission from .context import get_request_context @@ -55,6 +58,19 @@ def _authorized(permission: Permission): return context +def _application_api(ap) -> ApplicationAPI: + """Resolve the shared operation facade without an eager fallback. + + ``_application_api(ap)`` evaluates + ``ap.pipeline_service`` even when the facade exists and assumes the legacy + service has facade method names such as ``list_pipelines``. Both make + lightweight app graphs fail before the operation can run. + """ + + application_api = getattr(ap, 'application_api', None) + return application_api if application_api is not None else ApplicationAPI(ap) + + class LangBotMCPServer: """Builds and owns the FastMCP instance for LangBot.""" @@ -487,7 +503,24 @@ class LangBotMCPServer: """Register a tool boundary before its authorization and service call.""" def register(fn): - observed = fn + @functools.wraps(fn) + async def observed(*args, **kwargs): + # FastMCP executes tool handlers in a child task. The mount's + # request boundary is intentionally not inherited by child + # tasks, so establish a fresh short-lived scope for each tool + # invocation using the already authenticated ContextVar. + context = get_request_context() + persistence_mgr = getattr(self.ap, 'persistence_mgr', None) + tenant_scope = getattr(persistence_mgr, 'tenant_scope', None) + if not callable(tenant_scope): + # Keep lightweight in-process test doubles usable. The + # production Application always has a PersistenceManager; + # MCPMount itself still fails closed when that manager is + # absent on a real request. + return await fn(*args, **kwargs) + async with tenant_scope(context.workspace_uuid): + return await fn(*args, **kwargs) + return self.mcp.tool(**options)(observed) return register