-
Notifications
You must be signed in to change notification settings - Fork 344
Description
🐛 Describe the bug
I'm building an AI agent that uses MCP tools via langchain-mcp-adapters and streams responses back to the client using LangGraph, LangServe, and FastAPI.
The agent is created with tools provided by MultiServerMCPClient, and the stream is handled using:
async for event in agent.astream_events(...):The problem occurs when the request finishes and the context manager for the MultiServerMCPClient attempts to close. I consistently get the following error:
RuntimeError: Attempted to exit cancel scope in a different task than it was entered in
💥Full Error Traceback
RuntimeError: Attempted to exit cancel scope in a different task than it was entered in
Traceback (most recent call last):
File "/usr/local/lib/python3.12/site-packages/anyio/_backends/_asyncio.py", line 457, in __exit__
raise RuntimeError(
RuntimeError: Attempted to exit cancel scope in a different task than it was entered in
During handling of the above exception, another exception occurred:
Traceback (most recent call last):
File "/usr/local/lib/python3.12/site-packages/mcp/client/sse.py", line 43, in sse_client
async with anyio.create_task_group() as tg:
File "/usr/local/lib/python3.12/site-packages/anyio/_backends/_asyncio.py", line 778, in __aexit__
if self.cancel_scope.__exit__(type(exc), exc, exc.__traceback__):
File "/usr/local/lib/python3.12/site-packages/anyio/_backends/_asyncio.py", line 457, in __exit__
raise RuntimeError(
RuntimeError: Attempted to exit cancel scope in a different task than it was entered in
This seems to stem from the sse_client method and the internal AnyIO TaskGroup / CancelScope handling.
To Reproduce
Steps to reproduce the behavior:
- Set up
MultiServerMCPClientwith an SSE-based tool like Zapier. - Use it within an
asynccontextmanager. - Call
agent.astream_events()from inside a FastAPI route. - Let the request finish, and observe the stack trace.
💻 Minimal Reproducible Code
from typing import Annotated, Sequence, TypedDict, List, Union
from pydantic import BaseModel, Field
from langchain_core.messages import AIMessage, HumanMessage, SystemMessage, BaseMessage
from langgraph.prebuilt import create_react_agent
from langchain_mcp_adapters.client import MultiServerMCPClient
from contextlib import asynccontextmanager
from langgraph.graph.message import add_messages
from app.utils.llm import AgentModel, get_llm_by_model
class TestRequest(BaseModel):
messages: List[Union[SystemMessage, HumanMessage, AIMessage]]
class State(TypedDict):
messages: Annotated[Sequence[BaseMessage], add_messages]
@asynccontextmanager
async def make_graph():
mcp_client = MultiServerMCPClient(
{
"zapier": {
"url": "https://actions.zapier.com/mcp/sk-ak-xxxxxx/sse",
"transport": "sse",
}
}
)
async with mcp_client:
mcp_tools = mcp_client.get_tools()
model = get_llm_by_model(AgentModel.GPT_4_1_MINI, 0.1)
agent = create_react_agent(model, mcp_tools)
yield agent
async def run(request: TestRequest):
async with make_graph() as agent:
async for event in agent.astream_events(request, version="v1"):
kind = event["event"]
if kind == "on_chat_model_stream":
chunk = event["data"]["chunk"]
if chunk.content and len(chunk.tool_calls) == 0:
yield chunk✅ Expected behavior
The MultiServerMCPClient should cleanly enter and exit its internal cancel_scope or task_group without triggering a RuntimeError, as long as it is used inside a single async task context.
In this case, the context manager (async with) is entered and exited within the same function, and no lifecycle is shared across tasks. The expectation is that the client and its associated SSE connections can gracefully shut down after use in an @asynccontextmanager or standard async function.
🧩 Environment
- OS: Ubuntu 22.04 (Docker)
- Python: 3.12
- Poetry: yes (
pyproject.tomlattached) - anyio: 4.3.0
- langchain: 0.3.x
- langgraph: 0.2.20+
- langchain-mcp-adapters: 0.0.8
- Run context: FastAPI + LangGraph agent streaming via
agent.astream_events(...)