a digital entity named phi that roams bsky phi.zzstoatzz.io
2
fork

Configure Feed

Select the types of activity you want to include in your feed.

fix MCP cancel scope error by creating fresh servers per run

shared MCPServerStreamableHTTP instances get entered/exited across
different async tasks when concurrent notifications arrive, triggering
"Attempted to exit cancel scope in a different task than it was entered in".

fix: create fresh MCP server instances per agent.run() call via the
toolsets parameter instead of sharing singletons on the agent.

ref: https://github.com/pydantic/pydantic-ai/issues/2818

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

zzstoatzz 947094d5 902cde20

+22 -20
+22 -20
src/bot/agent.py
··· 131 131 self.memory = None 132 132 logger.warning("no memory - missing turbopuffer or openai key") 133 133 134 - # Generic atproto record CRUD via hosted pdsx MCP 135 - pdsx_mcp = MCPServerStreamableHTTP( 136 - url="https://pdsx-by-zzstoatzz.fastmcp.app/mcp", 137 - timeout=30, 138 - headers={ 139 - "x-atproto-handle": settings.bluesky_handle, 140 - "x-atproto-password": settings.bluesky_password, 141 - }, 142 - ) 143 - 144 - # ATProto publication search via hosted pub-search MCP 145 - pub_search_mcp = MCPServerStreamableHTTP( 146 - url="https://pub-search-by-zzstoatzz.fastmcp.app/mcp", 147 - timeout=30, 148 - tool_prefix="pub", 149 - ) 150 - 151 - # Create PydanticAI agent with MCP tools 134 + # Create PydanticAI agent without MCP toolsets — they're created 135 + # fresh per agent.run() call to avoid the cancel scope bug: 136 + # https://github.com/pydantic/pydantic-ai/issues/2818 152 137 self.agent = Agent[PhiDeps, Response]( 153 138 name="phi", 154 139 model="anthropic:claude-sonnet-4-6", 155 140 system_prompt=f"{self.base_personality}\n\n{OPERATIONAL_INSTRUCTIONS}", 156 141 output_type=Response, 157 142 deps_type=PhiDeps, 158 - toolsets=[pdsx_mcp, pub_search_mcp], 159 143 ) 160 144 161 145 # --- memory tools --- ··· 349 333 350 334 logger.info("phi agent initialized with pdsx + pub-search mcp tools") 351 335 336 + def _mcp_toolsets(self) -> list[MCPServerStreamableHTTP]: 337 + """Create fresh MCP server instances for a single agent run.""" 338 + return [ 339 + MCPServerStreamableHTTP( 340 + url="https://pdsx-by-zzstoatzz.fastmcp.app/mcp", 341 + timeout=30, 342 + headers={ 343 + "x-atproto-handle": settings.bluesky_handle, 344 + "x-atproto-password": settings.bluesky_password, 345 + }, 346 + ), 347 + MCPServerStreamableHTTP( 348 + url="https://pub-search-by-zzstoatzz.fastmcp.app/mcp", 349 + timeout=30, 350 + tool_prefix="pub", 351 + ), 352 + ] 353 + 352 354 async def process_mention( 353 355 self, 354 356 mention_text: str, ··· 406 408 memory=self.memory, 407 409 thread_uri=thread_uri, 408 410 ) 409 - result = await self.agent.run(user_prompt, deps=deps) 411 + result = await self.agent.run(user_prompt, deps=deps, toolsets=self._mcp_toolsets()) 410 412 logger.info(f"agent decided: {result.output.action}" + (f" - {result.output.text[:80]}" if result.output.text else "") + (f" ({result.output.reason})" if result.output.reason else "")) 411 413 412 414 # Store interaction and extract observations