Repository: lastmile-ai/mcp-agent

Issue:
Possible stuck-age using claude and melisearch
This issue is more of understanding exact working conditions and debug output, rather than a software issue. I write it in the hope that it provides documentation when me (in the future) and others encounter such problems: maybe it will help debug from the copius debug output that mcp_agent provides.

I have a multi-turn agent doing agentic RAG, using the meilisearch mcp tool to go back and back to the meilisearch database. I am able to run it from my laptop fine, but running it within our company's firewall tends to brick the agent in the sense of it just stopping. And i wonder if this is timeouts causing some issues or soething else. (it's the same meilisearch server endpoint strangely enough, so it might be a communication error not handled by mcp_agent. What i am trying to understand is where in the process do things get bricked.

First the successful Turn 12:

```
2025-06-27T15:54:40.239669583Z app[web.1]: {
2025-06-27T15:54:40.239672199Z app[web.1]:   "data": {
2025-06-27T15:54:40.239674859Z app[web.1]:     "progress_action": "Chatting",
2025-06-27T15:54:40.239677576Z app[web.1]:     "model": "claude-3-7-sonnet-20250219",
2025-06-27T15:54:40.239680303Z app[web.1]:     "agent_name": "finder",
2025-06-27T15:54:40.239682906Z app[web.1]:     "chat_turn": 12
2025-06-27T15:54:40.239685520Z app[web.1]:   }
2025-06-27T15:54:40.239688080Z app[web.1]: }
2025-06-27T15:54:40.239690672Z app[web.1]: [DEBUG] 2025-06-27T15:54:39
2025-06-27T15:54:40.239693262Z app[web.1]: mcp_agent.workflows.llm.augmented_llm_anthropic.finder -
2025-06-27T15:54:40.239695796Z app[web.1]: claude-3-7-sonnet-20250219 response:
2025-06-27T15:54:40.239698292Z app[web.1]: {
2025-06-27T15:54:40.239700862Z app[web.1]:   "data": {
2025-06-27T15:54:40.239703515Z app[web.1]:     "id": "msg_01Xnjobi4Kz1TxMiPrKJ4Zi1",
2025-06-27T15:54:40.239706148Z app[web.1]:     "content": [
2025-06-27T15:54:40.239708686Z app[web.1]:       {
2025-06-27T15:54:40.239711319Z app[web.1]:         "citations": null,
2025-06-27T15:54:40.239713987Z app[web.1]:         "text": "Let me search specifically for the complete text of the Section
2025-06-27T15:54:40.239716719Z app[web.1]: 7.03(s) provision:",
2025-06-27T15:54:40.239719285Z app[web.1]:         "type": "text"
2025-06-27T15:54:40.239721925Z app[web.1]:       },
2025-06-27T15:54:40.239724486Z app[web.1]:       {
2025-06-27T15:54:40.239727019Z app[web.1]:         "id": "toolu_01GiJAK5p7SL7GuY4pJs46Zj",
2025-06-27T15:54:40.239729605Z app[web.1]:         "input": {
2025-06-27T15:54:40.239732300Z app[web.1]:           "query": "7.03(s) Indebtedness Non-Loan Party",
2025-06-27T15:54:40.239735065Z app[web.1]:           "indexUid": "ftx_paras",
2025-06-27T15:54:40.239737715Z app[web.1]:           "filter": "doc_id = 1072"
2025-06-27T15:54:40.239740338Z app[web.1]:         },
2025-06-27T15:54:40.239765985Z app[web.1]:         "name": "meilisearch_search",
2025-06-27T15:54:40.239769523Z app[web.1]:         "type": "tool_use"
2025-06-27T15:54:40.239772221Z app[web.1]:       }
2025-06-27T15:54:40.239774758Z app[web.1]:     ],
2025-06-27T15:54:40.239778674Z app[web.1]:     "model": "claude-3-7-sonnet-20250219",
2025-06-27T15:54:40.239781497Z app[web.1]:     "role": "assistant",
2025-06-27T15:54:40.239784147Z app[web.1]:     "stop_reason": "tool_use",
2025-06-27T15:54:40.239786750Z app[web.1]:     "stop_sequence": null,
2025-06-27T15:54:40.239789418Z app[web.1]:     "type": "message",
2025-06-27T15:54:40.239792124Z app[web.1]:     "usage": {
2025-06-27T15:54:40.239794711Z app[web.1]:       "cache_creation_input_tokens": 0,
2025-06-27T15:54:40.239797429Z app[web.1]:       "cache_read_input_tokens": 0,
2025-06-27T15:54:40.239800198Z app[web.1]:       "input_tokens": 139747,
2025-06-27T15:54:40.239802900Z app[web.1]:       "output_tokens": 139,
2025-06-27T15:54:40.239805564Z app[web.1]:       "server_tool_use": null,
2025-06-27T15:54:40.239808258Z app[web.1]:       "service_tier": "standard"
2025-06-27T15:54:40.239810951Z app[web.1]:     },
2025-06-27T15:54:40.239813436Z app[web.1]:     "_request_id": "req_011CQYzkuqFVYnLf2tFRpqrz"
2025-06-27T15:54:40.239825197Z app[web.1]:   }
2025-06-27T15:54:40.239827750Z app[web.1]: }
2025-06-27T15:54:40.239832374Z app[web.1]: [INFO] 2025-06-27T15:54:39 mcp_agent.mcp.mcp_aggregator.finder - Requesting tool
2025-06-27T15:54:40.239835037Z app[web.1]: call
2025-06-27T15:54:40.239837526Z app[web.1]: {
2025-06-27T15:54:40.239839974Z app[web.1]:   "data": {
2025-06-27T15:54:40.239842464Z app[web.1]:     "progress_action": "Calling Tool",
2025-06-27T15:54:40.239845048Z app[web.1]:     "tool_name": "search",
2025-06-27T15:54:40.239847692Z app[web.1]:     "server_name": "meilisearch",
2025-06-27T15:54:40.239850244Z app[web.1]:     "agent_name": "finder"
2025-06-27T15:54:40.239852905Z app[web.1]:   }
2025-06-27T15:54:40.239855404Z app[web.1]: }
2025-06-27T15:54:40.239857853Z app[web.1]: [DEBUG] 2025-06-27T15:54:39 mcp_agent.mcp.mcp_agent_client_session -
2025-06-27T15:54:40.239860425Z app[web.1]: send_request: request=
2025-06-27T15:54:40.239863000Z app[web.1]: {
2025-06-27T15:54:40.239865469Z app[web.1]:   "data": {
2025-06-27T15:54:40.239869480Z app[web.1]:     "method": "tools/call",
2025-06-27T15:54:40.239873342Z app[web.1]:     "params": {
2025-06-27T15:54:40.239878583Z app[web.1]:       "meta": null,
2025-06-27T15:54:40.239882748Z app[web.1]:       "name": "search",
2025-06-27T15:54:40.239885313Z app[web.1]:       "arguments": {
2025-06-27T15:54:40.239887872Z app[web.1]:         "query": "7.03(s) Indebtedness Non-Loan Party",
2025-06-27T15:54:40.239890659Z app[web.1]:         "indexUid": "ftx_paras",
2025-06-27T15:54:40.239893278Z app[web.1]:         "filter": "doc_id = 1072"
2025-06-27T15:54:40.239898208Z app[web.1]:       }
2025-06-27T15:54:40.239900731Z app[web.1]:     }
2025-06-27T15:54:40.239903307Z app[web.1]:   }
2025-06-27T15:54:40.239905828Z app[web.1]: }
2025-06-27T15:55:21.704255155Z app[web.1]: [DEBUG] 2025-06-27T15:54:40 mcp_agent.mcp.mcp_agent_client_session -
2025-06-27T15:55:21.704294565Z app[web.1]: send_request: response=
2025-06-27T15:55:21.724374954Z app[web.1]: {
2025-06-27T15:55:21.724413668Z app[web.1]:   "data": {
2025-06-27T15:55:21.724418174Z app[web.1]:     "meta": null,
2025-06-27T15:55:21.724421805Z app[web.1]:     "content": [
2025-06-27T15:55:21.724425241Z app[web.1]:       {
2025-06-27T15:55:21.724428488Z app[web.1]:         "type": "text",
2025-06-27T15:55:21.724432470Z app[web.1]:         "text": "Search results for '7.03(s) Indebtedness Non-Loan Party':\n{\n
2025-06-27T15:55:21.724436507Z app[web.1]: \"hits\": [\n    {\n      \"File_Para_ID\":
2025-06-27T15:55:21.724441614Z app[web.1]: \"hlly-37-20--37-20amendment-37-20no-37-201_1043\",\n      \"Content\": \"Liens

```

You can see here that it is sucessfully able to make a request. And that request returns. Now the llm determines it needs to make one more search, but that search gets stuck: 

```
2025-06-27T15:55:21.971853968Z app[web.1]: {
2025-06-27T15:55:21.971856627Z app[web.1]:   "data": {
2025-06-27T15:55:21.971859246Z app[web.1]:     "progress_action": "Chatting",
2025-06-27T15:55:21.971861917Z app[web.1]:     "model": "claude-3-7-sonnet-20250219",
2025-06-27T15:55:21.971864875Z app[web.1]:     "agent_name": "finder",
2025-06-27T15:55:21.971867596Z app[web.1]:     "chat_turn": 13
2025-06-27T15:55:21.971870170Z app[web.1]:   }
2025-06-27T15:55:21.971872632Z app[web.1]: }
2025-06-27T15:55:21.971875058Z app[web.1]: [DEBUG] 2025-06-27T15:55:21
2025-06-27T15:55:21.971877618Z app[web.1]: mcp_agent.workflows.llm.augmented_llm_anthropic.finder -
2025-06-27T15:55:21.971880152Z app[web.1]: claude-3-7-sonnet-20250219 response:
2025-06-27T15:55:21.971882690Z app[web.1]: {
2025-06-27T15:55:21.971885153Z app[web.1]:   "data": {
2025-06-27T15:55:21.971887660Z app[web.1]:     "id": "msg_01UcuTKeBVEkAMcajbzbXCfp",
2025-06-27T15:55:21.971890376Z app[web.1]:     "content": [
2025-06-27T15:55:21.971892955Z app[web.1]:       {
2025-06-27T15:55:21.971895498Z app[web.1]:         "citations": null,
2025-06-27T15:55:21.971898245Z app[web.1]:         "text": "I need to find exactly the text of 7.03(s). Let me make one
2025-06-27T15:55:21.971901016Z app[web.1]: more search:",
2025-06-27T15:55:21.971903528Z app[web.1]:         "type": "text"
2025-06-27T15:55:21.971906143Z app[web.1]:       },
2025-06-27T15:55:21.971908646Z app[web.1]:       {
2025-06-27T15:55:21.971911213Z app[web.1]:         "id": "toolu_019kea44zQQPJwkZWyoZtmvN",
2025-06-27T15:55:21.971913983Z app[web.1]:         "input": {
2025-06-27T15:55:21.971916704Z app[web.1]:           "query": "Indebtedness incurred by a Non-Loan Party",
2025-06-27T15:55:21.971919467Z app[web.1]:           "indexUid": "ftx_paras",
2025-06-27T15:55:21.971922087Z app[web.1]:           "filter": "doc_id = 1072"
2025-06-27T15:55:21.971926401Z app[web.1]:         },
2025-06-27T15:55:21.971929481Z app[web.1]:         "name": "meilisearch_search",
2025-06-27T15:55:21.971932150Z app[web.1]:         "type": "tool_use"
2025-06-27T15:55:21.971934808Z app[web.1]:       }
2025-06-27T15:55:21.971937390Z app[web.1]:     ],
2025-06-27T15:55:21.971939976Z app[web.1]:     "model": "claude-3-7-sonnet-20250219",
2025-06-27T15:55:21.971942725Z app[web.1]:     "role": "assistant",
2025-06-27T15:55:21.971945350Z app[web.1]:     "stop_reason": "tool_use",
2025-06-27T15:55:21.971947978Z app[web.1]:     "stop_sequence": null,
2025-06-27T15:55:21.971950581Z app[web.1]:     "type": "message",
2025-06-27T15:55:21.971953206Z app[web.1]:     "usage": {
2025-06-27T15:55:21.971955788Z app[web.1]:       "cache_creation_input_tokens": 0,
2025-06-27T15:55:21.971958421Z app[web.1]:       "cache_read_input_tokens": 0,
2025-06-27T15:55:21.971961023Z app[web.1]:       "input_tokens": 151214,
2025-06-27T15:55:21.971963704Z app[web.1]:       "output_tokens": 139,
2025-06-27T15:55:21.971966277Z app[web.1]:       "server_tool_use": null,
2025-06-27T15:55:21.971969026Z app[web.1]:       "service_tier": "standard"
2025-06-27T15:55:21.971971717Z app[web.1]:     },
2025-06-27T15:55:21.971974235Z app[web.1]:     "_request_id": "req_011CQYzp1XdjoCeekn33fJo1"
2025-06-27T15:55:21.971976849Z app[web.1]:   }
2025-06-27T15:55:21.971979404Z app[web.1]: }
2025-06-27T15:55:21.971981933Z app[web.1]: [INFO] 2025-06-27T15:55:21 mcp_agent.mcp.mcp_aggregator.finder - Requesting tool
2025-06-27T15:55:21.971984613Z app[web.1]: call
2025-06-27T15:55:21.971987082Z app[web.1]: {
2025-06-27T15:55:21.971989492Z app[web.1]:   "data": {
2025-06-27T15:55:21.971992061Z app[web.1]:     "progress_action": "Calling Tool",
2025-06-27T15:55:21.971994808Z app[web.1]:     "tool_name": "search",
2025-06-27T15:55:21.971997461Z app[web.1]:     "server_name": "meilisearch",
2025-06-27T15:55:21.972000090Z app[web.1]:     "agent_name": "finder"
2025-06-27T15:55:21.972002772Z app[web.1]:   }
2025-06-27T15:55:21.972005370Z app[web.1]: }
[DEBUG] 2025-06-27T15:55:21 mcp_agent.mcp.mcp_agent_client_session -
2025-06-27T15:55:21.972010590Z app[web.1]: send_request: request=
2025-06-27T15:55:21.972013245Z app[web.1]: {
2025-06-27T15:55:21.972015754Z app[web.1]:   "data": {
2025-06-27T15:55:21.972018332Z app[web.1]:     "method": "tools/call",
2025-06-27T15:55:21.972021055Z app[web.1]:     "params": {
2025-06-27T15:55:21.972023599Z app[web.1]:       "meta": null,
2025-06-27T15:55:21.972026279Z app[web.1]:       "name": "search",
2025-06-27T15:55:21.972028866Z app[web.1]:       "arguments": {
2025-06-27T15:55:21.972031404Z app[web.1]:         "query": "Indebtedness incurred by a Non-Loan Party",
2025-06-27T15:55:21.972034164Z app[web.1]:         "indexUid": "ftx_paras",
2025-06-27T15:55:21.972043632Z app[web.1]:         "filter": "doc_id = 1072"
2025-06-27T15:55:21.972046403Z app[web.1]:       }
2025-06-27T15:55:21.972050292Z app[web.1]:     }
2025-06-27T15:55:21.972052827Z app[web.1]:   }
2025-06-27T15:55:21.972055343Z app[web.1]: }
2025-06-27T15:55:21.972057830Z app[web.1]: [DEBUG] 2025-06-27T15:55:21 mcp_agent.agents.agent - Shutting down agent
2025-06-27T15:55:21.972060507Z app[web.1]: finder...
2025-06-27T15:55:21.972063181Z app[web.1]: [DEBUG] 2025-06-27T15:55:21 mcp_agent.mcp.mcp_aggregator.finder - Decremented
2025-06-27T15:55:21.972065797Z app[web.1]: connection ref count to 0
2025-06-27T15:55:21.972068317Z app[web.1]: [INFO] 2025-06-27T15:55:21 mcp_agent.mcp.mcp_aggregator.finder - Last aggregator
2025-06-27T15:55:21.972070962Z app[web.1]: closing, shutting down all persistent connections...
2025-06-27T15:55:21.972075711Z app[web.1]: [INFO] 2025-06-27T15:55:21 mcp_agent.mcp.mcp_connection_manager - Disconnecting
2025-06-27T15:55:21.972078491Z app[web.1]: all persistent server connections...
2025-06-27T15:55:21.972081032Z app[web.1]: [INFO] 2025-06-27T15:55:21 mcp_agent.mcp.mcp_connection_manager - meilisearch:
2025-06-27T15:55:21.972083665Z app[web.1]: Requesting shutdown...
2025-06-27T15:55:21.972086239Z app[web.1]: [INFO] 2025-06-27T15:55:21 mcp_agent.mcp.mcp_connection_manager - All persistent
2025-06-27T15:55:21.972088942Z app[web.1]: server connections signaled to disconnect.
2025-06-27T15:55:21.972091534Z app[web.1]: [DEBUG] 2025-06-27T15:55:21 mcp_agent.mcp.mcp_connection_manager -
2025-06-27T15:55:21.972094100Z app[web.1]: MCPConnectionManager: shutting down all server tasks...
2025-06-27T15:55:21.972096756Z app[web.1]: [INFO] 2025-06-27T15:55:21 mcp_agent.mcp.mcp_connection_manager - Disconnecting
2025-06-27T15:55:21.972099443Z app[web.1]: all persistent server connections...
2025-06-27T15:55:22.457596731Z app[web.1]: [INFO] 2025-06-27T15:55:22 mcp_agent.mcp.mcp_aggregator.finder - Connection
2025-06-27T15:55:22.457643613Z app[web.1]: manager successfully closed and removed from context
2025-06-27T15:55:22.458946108Z app[web.1]: [DEBUG] 2025-06-27T15:55:22 mcp_agent.agents.agent - Agent finder shutdown.
2025-06-27T15:55:22.459406152Z app[web.1]: [INFO] 2025-06-27T15:55:22 mcp_agent.mcp_basic_agent - MCPApp cleanup
2025-06-27T15:55:22.460007756Z app[web.1]: {
2025-06-27T15:55:22.460024742Z app[web.1]:   "data": {
2025-06-27T15:55:22.460027996Z app[web.1]:     "progress_action": "Finished",
2025-06-27T15:55:22.460030913Z app[web.1]:     "target": "mcp_basic_agent",
2025-06-27T15:55:22.460033762Z app[web.1]:     "agent_name": "mcp_application_loop"
2025-06-27T15:55:22.460036559Z app[web.1]:   }
2025-06-27T15:55:22.460039189Z app[web.1]: }
2025-06-27T15:55:22.461593325Z app[web.1]:     Calling Tool   | finder           meilisearch (search)

```

This is where things go bad:

```
2025-06-27T15:55:21.972057830Z app[web.1]: [DEBUG] 2025-06-27T15:55:21 mcp_agent.agents.agent - Shutting down agent
2025-06-27T15:55:21.972060507Z app[web.1]: finder...
2025-06-27T15:55:21.972063181Z app[web.1]: [DEBUG] 2025-06-27T15:55:21 mcp_agent.mcp.mcp_aggregator.finder - Decremented
```

It wont make the 14th call. To make my debugging more omplex, i can only reproduce this bug in prod on the company systems, not my laptop. leading me to think that some timeout or something similar is not being handled. What is giving `mcp_agent` the sign to shut things down rather than make the search?


Discussion:
I have now replicated this behaviour on a second machine. Something causes the mcp_basic_agent to shut down the agent finder.

On the second machine the process if shutting down was pretty similar:

```
[DEBUG] 2025-06-28T00:42:44 mcp_agent.workflows.llm.augmented_llm_anthropic.finder - Chat in progress
{
  "data": {
    "progress_action": "Chatting",
    "model": "claude-3-7-sonnet-20250219",
    "agent_name": "finder",
    "chat_turn": 13
  }
}
[DEBUG] 2025-06-28T00:43:24 mcp_agent.workflows.llm.augmented_llm_anthropic.finder -
claude-3-7-sonnet-20250219 response:
{
  "data": {
    "id": "msg_013czZt6L6f3S5Cdevx7dMkj",
    "content": [
      {
        "citations": null,
        "text": "Let me search specifically for section 7.03(s):",
        "type": "text"
      },
      {
        "id": "toolu_012THWC6ocw1r4FYVaekKg6h",
        "input": {
          "query": "Section 7.03(s) Non-Loan Party $25,500,000",
          "indexUid": "ftx_paras",
          "filter": "doc_id = 1072"
        },
        "name": "meilisearch_search",
        "type": "tool_use"
      }
    ],
    "model": "claude-3-7-sonnet-20250219",
    "role": "assistant",
    "stop_reason": "tool_use",
    "stop_sequence": null,
    "type": "message",
    "usage": {
      "cache_creation_input_tokens": 0,
      "cache_read_input_tokens": 0,
      "input_tokens": 142710,
      "output_tokens": 135,
      "server_tool_use": null,
      "service_tier": "standard"
    },
    "_request_id": "req_011CQZh5bdofgDQEpvy1wsBE"
  }
}
[INFO] 2025-06-28T00:43:24 mcp_agent.mcp.mcp_aggregator.finder - Requesting tool call
{
  "data": {
    "progress_action": "Calling Tool",
    "tool_name": "search",
    "server_name": "meilisearch",
    "agent_name": "finder"
  }
}
[DEBUG] 2025-06-28T00:43:24 mcp_agent.mcp.mcp_agent_client_session - send_request: request=
{
  "data": {
    "method": "tools/call",
    "params": {
      "meta": null,
      "name": "search",
      "arguments": {
        "query": "Section 7.03(s) Non-Loan Party $25,500,000",
        "indexUid": "ftx_paras",
        "filter": "doc_id = 1072"
      }
    }
  }
}
... Running        | mcp_basic_agent
[DEBUG] 2025-06-28T00:43:24 mcp_agent.agents.agent - Shutting down agent finder...
[DEBUG] 2025-06-28T00:43:24 mcp_agent.mcp.mcp_aggregator.finder - Decremented connection ref count to
0
[INFO] 2025-06-28T00:43:24 mcp_agent.mcp.mcp_aggregator.finder - Last aggregator closing, shutting
down all persistent connections...
[INFO] 2025-06-28T00:43:24 mcp_agent.mcp.mcp_connection_manager - Disconnecting all persistent server
connections...
[INFO] 2025-06-28T00:43:24 mcp_agent.mcp.mcp_connection_manager - meilisearch: Requesting shutdown...
[INFO] 2025-06-28T00:43:24 mcp_agent.mcp.mcp_connection_manager - All persistent server connections
signaled to disconnect.
[DEBUG] 2025-06-28T00:43:24 mcp_agent.mcp.mcp_connection_manager - MCPConnectionManager: shutting
down all server tasks...
[INFO] 2025-06-28T00:43:24 mcp_agent.mcp.mcp_connection_manager - Disconnecting all persistent server
connections...
[INFO] 2025-06-28T00:43:25 mcp_agent.mcp.mcp_aggregator.finder - Connection manager successfully
closed and removed from context
[DEBUG] 2025-06-28T00:43:25 mcp_agent.agents.agent - Agent finder shutdown.
[INFO] 2025-06-28T00:43:25 mcp_agent.mcp_basic_agent - MCPApp cleanup
{
  "data": {
    "progress_action": "Finished",
    "target": "mcp_basic_agent",
    "agent_name": "mcp_application_loop"
  }
}
    Running        | mcp_basic_agent
    Calling Tool   | finder           meilisearch (search)
```

How might I debug this sudden shutdown...timeouts? limits on number of calls to same agent? (on the other machine it hits this one much faster..on this machine it happened on the 13th or14th turn too.
Hey @rahuldave, could you share a minimal reproducible example for me to look into? Have you tried increasing the max_iterations like so:
```python
result = await llm.generate_str(
    message="your query",
    request_params=RequestParams(max_iterations=50) 
)
```
The `max_iterations` is set to 20, which is more than the number of turns at which this happens. And on two different machines it happened on iteration 13 i think, which makes me wonder if there is a magic number 12 somewhere. I'm unsure how to create a reproducible example because this does not happen on my laptop, and there it happily goes to turn 14 without issue. (the laptop is a mac, the original example is in a docker container, the second one on a linux server on aws). But i will try. In the meanwhile i am trying to understand what and in which piece of code the shutdown is precipitated: like how we get from 

```
[DEBUG] 2025-06-28T00:43:24 mcp_agent.mcp.mcp_agent_client_session - send_request: request=
{
  "data": {
    "method": "tools/call",
    "params": {
      "meta": null,
      "name": "search",
      "arguments": {
        "query": "Section 7.03(s) Non-Loan Party $25,500,000",
        "indexUid": "ftx_paras",
        "filter": "doc_id = 1072"
      }
    }
  }
}
... Running        | mcp_basic_agent

```

to

```
[DEBUG] 2025-06-28T00:43:24 mcp_agent.agents.agent - Shutting down agent finder...
[DEBUG] 2025-06-28T00:43:24 mcp_agent.mcp.mcp_aggregator.finder - Decremented connection ref count to
0
```

The app itself runs under fastapi..a request comes in, and off to the loops we are..
Here is code which, removing the fastapi stuff, does whats happening. Our documents paras are indexed inside a meilisearch database, with multiple indexes for paras, filenames, and definitions. Clause starts with the filename and asks its own questions to answer the original question. Basically, agentic RAG. I am trying to see if I can get this version to fail...

```python
import asyncio
import os
import time
import logging
import sys
import uuid
import json
from typing import Dict, List, Optional, Any, Callable, TypeVar, Awaitable
from datetime import datetime
from pathlib import Path
from functools import partial
from mcp_agent.app import MCPApp
from mcp_agent.config import (
    Settings,
    LoggerSettings,
    MCPSettings,
    MCPServerSettings,
    OpenAISettings,
    AnthropicSettings,
)
from mcp_agent.agents.agent import Agent
from mcp_agent.workflows.llm.augmented_llm import RequestParams, SimpleMemory
from mcp_agent.workflows.llm.llm_selector import ModelPreferences
from mcp_agent.workflows.llm.augmented_llm_anthropic import AnthropicAugmentedLLM
from mcp_agent.workflows.llm.augmented_llm_google import GoogleAugmentedLLM
import meilisearch
import time


mcp_app = MCPApp(name="mcp_basic_agent")

async def runner():
    question = "How much non-guarantor loan capacity is there in this debt contract?"
    filename = "HLLY%20-%20Amendment%20no%201.pdf"
    prompt = """
    You are an expert on all kinds of credit loans. I am going to ask you questions about a debt document.

    Various indexes for this document are in a meilisearch database. You will use the meilisearch tool make a MCP full-text searches in this database to answer this question.

    The main content of this document, paragraph by paragraph, is represented by full-text index `ftx_paras`. These paras often refer to definitions, which can be found in the index `ftx_defs`. The `filenames_ftx_index` index identifies the individual documents with their `document_name` as the name of the document, the `Filename` which is used to construct a URL to fetch the document, and the `doc_id` which can be used to look up the other indexes for information. In the `ftx_paras` index, all the entries with a given dictionary key `Filename` or `doc_id` comprise this single debt document.

    Use the supplied document name in the user's query to lookup the `doc_id` or `Filename`, and then use the `doc_id` to query the `ftx_paras` index. The field you want to get from this index is the `Content` field: it contains the full text of the paragraph. The other fields are useful as well.

    Since your main workflow will be to expand your queries on these indices as you get back results from `ftx_paras`: for example if a particular retrieval from `ftx_paras` has text "Section 6.1" in the `Content` field, you may want to go recursively search for the words "Section 6.1" in the `ftx_paras` field `Content` in the index and find those paras that match it. You can also look in the fields `Definition_ref` for a retrieved paragraph: these will tell you the definitions that the current paragraphs refer to, and there will be useful information in them. You will need to look up definitions in the `ftx_defs` index, and can use the same `doc_id` keys to look the definitions up in the `ftx_defs` index.

    There are other keys in `ftx_paras` which are important to this workflow. The field `region_coordinates` is the region of the entry in the original pdf document. The field `Start_Page` gives us the page number of the pdf page where the entry is to be found. The field `Para_ID` is the paragraph id for the entry. The field `Filename` (which we talked about earlier) gives us the pdf file from which these indexes were built. All 4 of these MUST be carried along by you with the 'Content' entry in your computations and MUST  be used for citations (which I will describe below).

    How should your workflow proceed?

    Any question i ask you, use must first use its direct formulation to make a full text search. Keep track of the dictionaries that match this query. Now, using your expertise, use the meaning of the question to formulate synonymous questions. (This is called query expansion.) Ask these questions to the `ftx_paras` index as well and collect the dictionaries obtained in the response. Make a list of all these dictionaries and find all the definitions used using the `Definitions_Ref` field. Expand even further by searching for the `Content` of sections referred to (in full text `Content` field) and definitions used.

    Take your time. You MUST make atleast 4 followup search iterations on the database (so atleast 4 followup finder tool calls after the first one), using the results from each previous search to guide you on what to search next.

    Now, using all the dictionaries and definitions collected as your context, formulate an answer to the question. Formulate your answer with the citations using the url format below only after all the hits on meilisearch as part of your query expansion have been completed, as this will help give you good context for your answer. In your answer, any assertion you make MUST be supported by a citation.

    Construct the citation from each of the dictionaries you retrieved from the indices and used for the answer in the format:
    ```
    http://192.168.116.82:3004/adapter?pdf=https://api.staging.credcore.dev/content/documents/{doc_id}/source.pdf&page={page}&coords={coordinates}
    ```
    where the value of 'page' comes from `Start_Page`, the value of 'doc_id' from the `doc_id` field, and 'coordinates' from the `region_coordinates` in  `ftx_paras` to format your citations.
    You MUST use the `Para_ID` from the index as the markdown text for these urls, for example `[Para 114](http://192.168.116.82:3004/adapter?pdf=https://api.staging.credcore.dev/content/documents/1072/source.pdf&page=23&coords=116.7,671.2572,280.4811,662.3571999999999)`.

    Notice the format of the coordinates. The co-ordinates from the `region_coordinates` array in the index are taken and simply listed comma separated.

    DO NOT show me all the MCP searches you made.
    DO NOT ask for human input.

    Your answer must consist of 3 sections. The first one with header "Searches" summarizes the tree of searches you made: that is, it lists the first searches it made with question query expansion, and the subsequent searches that the returned answers made you do.

    When you return your answer to the question, write the primary provisions regarding the question under a proper header. List these provisions point by point, in a well-formatted markdown table. There must be a row for each provison. Each row has 3 columns: the first for the name of the provison, the second for its description, and the third for the citations to back up the description for that provison. There MUST be a citation for each point!

    Finally summarize your findings in a "Summary" section.
    """
    start = time.time()
    await process_question(
        question,
        filename,
        prompt,
        start
    )

async def process_question(message: str, filename: str = "HLLY - Amendment Number 1", custom_prompt: str = None, start_time: float = None):
    # Get a logger for this function
    logger = logging.getLogger("contract_ui.process_question")

    async with mcp_app.run() as agent_app:
        context = agent_app.context

        logger.info("Starting contract analysis")
        logger.info(f"Current config: {context.config.model_dump()}")



        # Create agent with the determined instruction
        # With recent changes, instruction is never None - we always have at least a minimal prompt

        finder_agent = Agent(
            name="finder",
            instruction=custom_prompt,
            server_names=["meilisearch"],
        )

        async with finder_agent:
            logger.info("Connecting to finder agent and loading tools...")
            result = await finder_agent.list_tools()
            logger.info(f"Tools available: {result.model_dump()}")

            llm = await finder_agent.attach_llm(AnthropicAugmentedLLM)
            # Format message differently based on mode

            formatted_message = f"""The question refers to the file "{filename}" and is:
            {message}"""
            logger.info(f"Using document analysis format with file: {filename}")

            logger.info(f"Sending question to agent: {message}")

            # Set up periodic time updates
            update_interval = 0.5  # seconds
            last_update = time.time()

            async def process_response():
                nonlocal last_update
                result = await llm.generate_str(
                    message=formatted_message,
                    request_params=RequestParams(
                        max_iterations=20
                    )
                )
                return result

            # Start the processing task
            task = asyncio.create_task(process_response())

            # While waiting for the response, send time updates
            while not task.done():
                current_time = time.time()
                if current_time - last_update >= update_interval:
                    elapsed = current_time - start_time if start_time else 0.0
                    logger.info(f"Elapsed: {elapsed}")
                    last_update = current_time
                await asyncio.sleep(20.0)

            # Get the result
            result = await task

            # Debug the response
            logger.info(f"Response received, length: {len(result) if result else 0}")
            if result:
                logger.debug(f"Response snippet: {result[:100]}")

            # Ensure result is a string and not None
            if result is None:
                result = "No response received from the agent."
                logger.warning("No response received from agent")


def configure_logging():
    """Configure the Python logging system for our application.

    Note: We don't add a console handler here because the mcp-agent library
    already configures one through the YAML configuration (transports: [console, file])
    """
    # Get the root logger
    root_logger = logging.getLogger()

    # Don't clear existing handlers since MCP agent sets them up

    # Just set the appropriate logging levels
    root_logger.setLevel(logging.INFO)

    # Set specific logger levels
    contract_logger = logging.getLogger("contract_ui")
    contract_logger.setLevel(logging.INFO)

    # Return our application logger
    return contract_logger

if __name__ == "__main__":
    # Configure logging
    logger = configure_logging()
    asyncio.run(runner())
```
The basic question for me is how may i debug what happens between the tool use request and the sudden shutdown: i thought i might be blocking some coroutines by poorly written async code, but should that shut down the search? If there a timeout beyond which the co-routines responsible for the mcp search just give up and we get into the shut stage below? If something like this is happening how can i instrument it?

```
[INFO] 2025-06-28T00:43:24 mcp_agent.mcp.mcp_aggregator.finder - Requesting tool call
{
  "data": {
    "progress_action": "Calling Tool",
    "tool_name": "search",
    "server_name": "meilisearch",
    "agent_name": "finder"
  }
}
[DEBUG] 2025-06-28T00:43:24 mcp_agent.mcp.mcp_agent_client_session - send_request: request=
{
  "data": {
    "method": "tools/call",
    "params": {
      "meta": null,
      "name": "search",
      "arguments": {
        "query": "Section 7.03(s) Non-Loan Party $25,500,000",
        "indexUid": "ftx_paras",
        "filter": "doc_id = 1072"
      }
    }
  }
}
... Running        | mcp_basic_agent
[DEBUG] 2025-06-28T00:43:24 mcp_agent.agents.agent - Shutting down agent finder...
[DEBUG] 2025-06-28T00:43:24 mcp_agent.mcp.mcp_aggregator.finder - Decremented connection ref count to
0
[INFO] 2025-06-28T00:43:24 mcp_agent.mcp.mcp_aggregator.finder - Last aggregator closing, shutting
down all persistent connections...
[INFO] 2025-06-28T00:43:24 mcp_agent.mcp.mcp_connection_manager - Disconnecting all persistent server
connections...
```


The MCP server tools are called in [mcp_aggregator](https://github.com/lastmile-ai/mcp-agent/blob/b2119ecf07c817c2bf481c91fa84194c3c9e7eab/src/mcp_agent/mcp/mcp_aggregator.py#L882), which triggers [MCPAgentClientSession.send_request](https://github.com/lastmile-ai/mcp-agent/blob/b2119ecf07c817c2bf481c91fa84194c3c9e7eab/src/mcp_agent/mcp/mcp_agent_client_session.py#L154). `send_request` adds tracing before forwarding the request to the MCP server.

Looking at your logs, the shutdown happens before a response from the MCP server is received. However,I don't see any error logged which is unexpected since errors should be logged if the request fails. Therefore, I am not sure if the problem is with a bad tool response or if something else is causing finder_agent to [close](https://github.com/lastmile-ai/mcp-agent/blob/b2119ecf07c817c2bf481c91fa84194c3c9e7eab/src/mcp_agent/agents/agent.py#L293).

There shouldn't be any timeout unless we set them in `mcp_agent.config.yaml`. We should also see an error in the logs if it timeout.

I'll look further, but hopefully this points you in the right direction to debug the error.


>Therefore, I am not sure if the problem is with a bad tool response or if something else is causing finder_agent to close.

This seems particularly important...are there particular errors/exceptions i should be on the lookout for? Clearly the context manager exit function is called? I think I must add a lot of exception handling in to debug
Have you tried placing the code inside the finder_agent context manager within a try-except block to capture any errors?
```python
async with finder_agent:
    try:
        logger.info("Connecting to finder agent and loading tools...")
        result = await finder_agent.list_tools()
        logger.info(f"Tools available: {result.model_dump()}")

        llm = await finder_agent.attach_llm(AnthropicAugmentedLLM)

        # ...rest of your code
    except Exception as e:
        logger.error(f"Exception in finder_agent context: {e}", exc_info=True)
```
Yes thats exactly what i am doing now! Reproduction is hard so i keep thinking coros blocked race conditions. One problem i has was uvicorn websocket timeouts: i communicate over websocket, which i fixed by increasing the uvicorn timeout to 2 minutes, as some of the agent calls, especially with claude 429 retrys, last a long time, and i think mcp_agent yields at very specific points in generate_str (like between turns but not between llm and mcp calls). I'm trying to reproduce the errors i showed you and have not succeeded, but i dont know how something like that in the fastapi context affects mcp_agent or if it even can, by sending some sort of exception which causes us to go to the finder agents context manager exit...
I wonder if this is related somehow to multithreading issues. @rahuldave is your app multi-threaded? Either way I'll prioritize reviewing @StreetLamb's fixes for multithreaded environments (https://github.com/lastmile-ai/mcp-agent/pull/265) today.
@rahuldave just submitted a few subtle fixes that should resolve these issues in multi-threaded environments. I'll publish a patch update and ping you shortly to see if you can try this again!



Patch:
diff --git a/Makefile b/Makefile
index 59e2861a7..25b78c8d0 100644
--- a/Makefile
+++ b/Makefile
@@ -18,13 +18,13 @@ tests:
 
 .PHONY: coverage
 coverage:
-	uv run coverage run -m pytest
+	uv run coverage run -m pytest tests
 	uv run coverage xml -o coverage.xml
 	uv run coverage report -m --fail-under=80
 
 .PHONY: coverage-report
 coverage-report:
-	uv run coverage run -m pytest
+	uv run coverage run -m pytest tests
 	uv run coverage html
 
 .PHONY: schema
diff --git a/examples/multithread/main.py b/examples/multithread/main.py
new file mode 100644
index 000000000..cdfba5d1b
--- /dev/null
+++ b/examples/multithread/main.py
@@ -0,0 +1,110 @@
+import argparse
+import asyncio
+import concurrent.futures
+import logging
+import traceback
+
+from mcp_agent.app import MCPApp
+from mcp_agent.agents.agent import Agent
+from mcp_agent.workflows.evaluator_optimizer.evaluator_optimizer import (
+    EvaluatorOptimizerLLM,
+    QualityRating,
+)
+from mcp_agent.workflows.llm.augmented_llm import RequestParams
+from mcp_agent.workflows.llm.augmented_llm_openai import OpenAIAugmentedLLM
+
+logger = logging.getLogger(__name__)
+
+
+async def run() -> str:
+    app = MCPApp(name="script_generation_fewshot_eval")
+    async with app.run():
+        optimizer = Agent(
+            name="optimizer",
+            instruction="""You are an expert script writer and optimizer. Your task is to generate a script based on the provided message.
+            The story must adhere to the following rules:
+            1. The story must be at least 100 words long.
+            2. The story can be no longer than 150 words.
+            """,
+            server_names=[],
+        )
+        evaluator = Agent(
+            name="evaluator",
+            instruction="""Evaluate the script based on the following criteria:
+            [Criteria]: Script Length (target is no less than 100 words and no more than 150 words)
+            [Coherence]: The script should be coherent and follow a logical structure.
+            [Creativity]: The script should be creative and engaging.
+            For each criterion,
+            - Provide a rating (EXCELLENT, GOOD, FAIR, or POOR)
+            - Offer specific feedback or suggestions for improvement.
+
+            Summarize your evaluation as a structured response with:
+            - Overall quality rating
+            - Specific feedback and areas for improvement.
+            - Include concrete feedback about script length expressed in number of words. This is very important!
+            """,
+            server_names=["word_count"],
+        )
+
+        evaluator_optimizer = EvaluatorOptimizerLLM(
+            optimizer=optimizer,
+            evaluator=evaluator,
+            llm_factory=OpenAIAugmentedLLM,
+            min_rating=QualityRating.GOOD,
+        )
+
+        result = await evaluator_optimizer.generate_str(
+            """
+            Please write a story about a goblin that is a master of disguise.
+            The goblin should be able to change its appearance and behavior to blend in with different environments and situations
+            """,
+            request_params=RequestParams(maxTokens=16384, max_iterations=3),
+        )
+
+        return result
+
+
+def generate_step():
+    loop = asyncio.new_event_loop()
+    try:
+        asyncio.set_event_loop(loop)
+        result = loop.run_until_complete(run())
+        return result
+    except Exception as e:
+        logger.exception("Error during script generation", exc_info=e)
+        return ""
+    finally:
+        # Close the loop
+        loop.close()
+        asyncio.set_event_loop(None)
+
+
+def main(concurrency: int) -> list[str]:
+    results = []
+    with concurrent.futures.ThreadPoolExecutor(max_workers=concurrency) as executor:
+        futures = {executor.submit(generate_step): idx for idx in range(concurrency)}
+        for future in concurrent.futures.as_completed(futures):
+            idx = futures[future]
+            try:
+                result = future.result()
+                print(f"[Thread {idx}] Result: {result}\n\n")
+                results.append(result)
+            except Exception as e:
+                print(f"[Thread {idx}] Generated an exception: {e}")
+                traceback.print_exc()
+    return results
+
+
+if __name__ == "__main__":
+    parser = argparse.ArgumentParser()
+    parser.add_argument(
+        "-c", "--concurrency", type=int, default=2, help="Number of concurrent requests"
+    )
+
+    args = parser.parse_args()
+
+    results = main(args.concurrency)
+
+    print("\n\n---\n\n")
+    for idx, result in enumerate(results):
+        print(f"Result {idx}: {result}\n")
diff --git a/examples/multithread/mcp_agent.config.yaml b/examples/multithread/mcp_agent.config.yaml
new file mode 100644
index 000000000..d6b9adb50
--- /dev/null
+++ b/examples/multithread/mcp_agent.config.yaml
@@ -0,0 +1,28 @@
+$schema: ../../../schema/mcp-agent.config.schema.json
+
+execution_engine: asyncio
+logger:
+  transports: [console, file]
+  level: debug
+  progress_display: false
+  path_settings:
+    path_pattern: "logs/mcp-agent-{unique_id}.jsonl"
+    unique_id: "timestamp" # Options: "timestamp" or "session_id"
+    timestamp_format: "%Y%m%d_%H%M%S"
+
+mcp:
+  servers:
+    fetch:
+      command: "uvx"
+      args: ["mcp-server-fetch"]
+    filesystem:
+      command: "npx"
+      args: ["-y", "@modelcontextprotocol/server-filesystem"]
+    word_count:
+      command: "uv"
+      args: ["run", "word_count.py"]
+
+openai:
+  # Secrets (API keys, etc.) are stored in an mcp_agent.secrets.yaml file which can be gitignored
+  #  default_model: "o3-mini"
+  default_model: "gpt-4o-mini"
diff --git a/examples/multithread/mcp_agent.secrets.yaml.example b/examples/multithread/mcp_agent.secrets.yaml.example
new file mode 100644
index 000000000..8f1ba4c9e
--- /dev/null
+++ b/examples/multithread/mcp_agent.secrets.yaml.example
@@ -0,0 +1,9 @@
+$schema: ../../../schema/mcp-agent.config.schema.json
+
+openai:
+  api_key: openai_api_key
+
+anthropic:
+  api_key: anthropic_api_key
+
+
diff --git a/examples/multithread/requirements.txt b/examples/multithread/requirements.txt
new file mode 100644
index 000000000..45c6a2a95
--- /dev/null
+++ b/examples/multithread/requirements.txt
@@ -0,0 +1,7 @@
+# Core framework dependency
+mcp-agent @ file://../../../  # Link to the local mcp-agent project root
+
+# Additional dependencies specific to this example
+anthropic
+openai
+
diff --git a/examples/multithread/word_count.py b/examples/multithread/word_count.py
new file mode 100644
index 000000000..6e0a0728d
--- /dev/null
+++ b/examples/multithread/word_count.py
@@ -0,0 +1,14 @@
+from mcp.server.fastmcp import FastMCP
+
+
+mcp = FastMCP("Script Duration Server")
+
+
+@mcp.tool()
+def get_script_word_count(script: str) -> int:
+    """Return the number of whitespace-separated tokens in *script*."""
+    return len(script.split())
+
+
+if __name__ == "__main__":
+    mcp.run()
diff --git a/src/mcp_agent/agents/agent.py b/src/mcp_agent/agents/agent.py
index 452113552..4e7e71ddc 100644
--- a/src/mcp_agent/agents/agent.py
+++ b/src/mcp_agent/agents/agent.py
@@ -201,6 +201,8 @@ async def initialize(self, force: bool = False):
             # Fall back to global context if available
             from mcp_agent.core.context import get_current_context
 
+            # Advisory: obtaining a global context can be unsafe in multithreaded runs
+            # Prefer explicitly setting agent.context = app.context when running per-thread apps
             self.context = get_current_context()
 
         tracer = get_tracer(self.context)
@@ -1050,16 +1052,14 @@ class AgentTasks:
     Agent tasks for executing agent-related activities.
     """
 
-    # --- global-per-worker state -------------------------------------------------
-    # Maps agent name to its corresponding MCPAggregator
-    server_aggregators_for_agent: Dict[str, MCPAggregator] = {}
-    server_aggregators_for_agent_lock: asyncio.Lock = asyncio.Lock()
-    # Maps agent name to its reference count
-    agent_refcounts: dict[str, int] = {}
-    # ---------------------------------------------------------------------------
-
     def __init__(self, context: "Context"):
         self.context = context
+        # --- instance-scoped state (thread-safe for Temporal worker event loop) ---
+        # Using instance attributes avoids cross-thread event loop affinity issues with asyncio.Lock
+        # when activities run concurrently in Temporal workers or multi-threaded environments.
+        self.server_aggregators_for_agent: Dict[str, MCPAggregator] = {}
+        self.server_aggregators_for_agent_lock: asyncio.Lock = asyncio.Lock()
+        self.agent_refcounts: dict[str, int] = {}
 
     async def initialize_aggregator_task(
         self, request: InitAggregatorRequest
diff --git a/src/mcp_agent/config.py b/src/mcp_agent/config.py
index 8e4e7c053..716224120 100644
--- a/src/mcp_agent/config.py
+++ b/src/mcp_agent/config.py
@@ -5,6 +5,8 @@
 
 from pathlib import Path
 from typing import Dict, List, Literal, Optional
+import threading
+import warnings
 
 from pydantic import BaseModel, ConfigDict, Field, field_validator
 from pydantic_settings import BaseSettings, SettingsConfigDict
@@ -517,4 +519,16 @@ def deep_merge(base: dict, update: dict) -> dict:
 
     # No valid config found anywhere
     _settings = Settings()
+
+    # Thread-safety advisory: warn when using global singleton from non-main thread
+    if (
+        threading.current_thread() is not threading.main_thread()
+        and config_path is None
+    ):
+        warnings.warn(
+            "get_settings() returned the global Settings singleton on a non-main thread. "
+            "In multithreaded environments, prefer passing a Settings instance explicitly to MCPApp("
+            "settings=...) or provide a per-thread config_path to avoid cross-thread coupling.",
+            stacklevel=2,
+        )
     return _settings
diff --git a/src/mcp_agent/core/context.py b/src/mcp_agent/core/context.py
index 8d321ce21..673fb300d 100644
--- a/src/mcp_agent/core/context.py
+++ b/src/mcp_agent/core/context.py
@@ -5,6 +5,7 @@
 import asyncio
 import concurrent.futures
 from typing import Any, Optional, TYPE_CHECKING
+import warnings
 
 from pydantic import BaseModel, ConfigDict
 
@@ -275,6 +276,13 @@ def run_async():
                 _global_context = loop.run_until_complete(initialize_context())
         except RuntimeError:
             _global_context = asyncio.run(initialize_context())
+
+        # Advisory: using a global context can cause cross-thread coupling
+        warnings.warn(
+            "get_current_context() created a global Context. "
+            "In multithreaded runs, instantiate an MCPApp per thread and use app.context instead.",
+            stacklevel=2,
+        )
     return _global_context
 
 
diff --git a/src/mcp_agent/mcp/mcp_aggregator.py b/src/mcp_agent/mcp/mcp_aggregator.py
index 27c681e32..2367d379f 100644
--- a/src/mcp_agent/mcp/mcp_aggregator.py
+++ b/src/mcp_agent/mcp/mcp_aggregator.py
@@ -248,14 +248,17 @@ async def close(self):
                                         "Timeout during disconnect_all(), forcing shutdown"
                                     )
 
-                                # Ensure the exit method is called regardless
+                                # Avoid calling __aexit__ directly across threads; mark inactive
                                 try:
-                                    await self._persistent_connection_manager.__aexit__(
-                                        None, None, None
-                                    )
+                                    if hasattr(
+                                        self._persistent_connection_manager,
+                                        "_tg_active",
+                                    ):
+                                        self._persistent_connection_manager._tg_active = False
+                                        self._persistent_connection_manager._tg = None
                                 except Exception as e:
                                     logger.warning(
-                                        f"Error during connection manager cleanup: {e}"
+                                        f"Error during connection manager state cleanup: {e}"
                                     )
 
                                 # Clean up the connection manager from the context
diff --git a/src/mcp_agent/mcp/mcp_connection_manager.py b/src/mcp_agent/mcp/mcp_connection_manager.py
index d7d2628e9..cae9c9ea0 100644
--- a/src/mcp_agent/mcp/mcp_connection_manager.py
+++ b/src/mcp_agent/mcp/mcp_connection_manager.py
@@ -3,6 +3,7 @@
 """
 
 from datetime import timedelta
+import threading
 from typing import (
     AsyncGenerator,
     Callable,
@@ -100,6 +101,11 @@ def request_shutdown(self) -> None:
         """
         self._shutdown_event.set()
 
+    # Back-compat helper to avoid tests reaching into Event internals across threads
+    def _is_shutdown_requested_flag(self) -> bool:
+        """Return True if a shutdown has been requested for this server connection."""
+        return self._shutdown_event.is_set()
+
     async def wait_for_shutdown_request(self) -> None:
         """
         Wait until the shutdown event is set.
@@ -229,6 +235,8 @@ def __init__(
         # Manage our own task group - independent of task context
         self._tg: TaskGroup | None = None
         self._tg_active = False
+        # Track the thread this manager was created in to ensure TaskGroup cleanup
+        self._thread_id = threading.get_ident()
 
     async def __aenter__(self):
         # We create a task group to manage all server lifecycle tasks
@@ -246,21 +254,28 @@ async def __aexit__(self, exc_type, exc_val, exc_tb):
             logger.debug("MCPConnectionManager: shutting down all server tasks...")
             await self.disconnect_all()
 
-            # TODO: saqadri (FA1) - Is this needed?
-            # Add a small delay to allow for clean shutdown
+            # Add a small delay to allow for clean shutdown of subprocess transports, etc.
             await anyio.sleep(0.5)
 
-            # Then close the task group if it's active
-            if self._tg_active:
-                try:
-                    await self._tg.__aexit__(exc_type, exc_val, exc_tb)
-                except Exception as e:
+            # Then close the task group if it's active and we're in the same thread
+            if self._tg_active and self._tg:
+                current_thread = threading.get_ident()
+                if current_thread == self._thread_id:
+                    try:
+                        await self._tg.__aexit__(exc_type, exc_val, exc_tb)
+                    except Exception as e:
+                        logger.warning(
+                            f"MCPConnectionManager: Error during task group cleanup: {e}"
+                        )
+                else:
+                    # Different thread – cannot safely cleanup anyio TaskGroup
                     logger.warning(
-                        f"MCPConnectionManager: Error during task group cleanup: {e}"
+                        f"MCPConnectionManager: Task group cleanup called from different thread "
+                        f"(created in {self._thread_id}, called from {current_thread}). Skipping cleanup."
                     )
-                finally:
-                    self._tg_active = False
-                    self._tg = None
+                # Always mark as inactive and drop reference
+                self._tg_active = False
+                self._tg = None
         except AttributeError:  # Handle missing `_exceptions`
             pass
         except Exception as e:
@@ -479,4 +494,8 @@ async def disconnect_all(self) -> None:
             logger.info(f"{name}: Requesting shutdown...")
             conn.request_shutdown()
 
+        # Allow some time for transports to clean up if we actually shut anything down
+        if servers_to_shutdown:
+            await anyio.sleep(0.2)
+
         logger.info("All persistent server connections signaled to disconnect.")
diff --git a/src/mcp_agent/workflows/llm/augmented_llm_ollama.py b/src/mcp_agent/workflows/llm/augmented_llm_ollama.py
index a681af176..111f1ca7a 100644
--- a/src/mcp_agent/workflows/llm/augmented_llm_ollama.py
+++ b/src/mcp_agent/workflows/llm/augmented_llm_ollama.py
@@ -1,6 +1,6 @@
 from typing import Type
 
-from openai import OpenAI
+from openai import AsyncOpenAI
 
 from mcp_agent.executor.workflow_task import workflow_task
 from mcp_agent.utils.pydantic_type_serializer import serialize_model, deserialize_model
@@ -100,22 +100,23 @@ async def request_structured_completion_task(
             )
 
         # Next we pass the text through instructor to extract structured data
-        client = instructor.from_openai(
-            OpenAI(
-                api_key=request.config.api_key,
-                base_url=request.config.base_url,
-                http_client=request.config.http_client,
-            ),
-            mode=instructor.Mode.JSON,
-        )
+        async with AsyncOpenAI(
+            api_key=request.config.api_key,
+            base_url=request.config.base_url,
+            http_client=request.config.http_client,
+        ) as async_client:
+            client = instructor.from_openai(
+                async_client,
+                mode=instructor.Mode.JSON,
+            )
 
-        # Extract structured data from natural language
-        structured_response = client.chat.completions.create(
-            model=request.model,
-            response_model=response_model,
-            messages=[
-                {"role": "user", "content": request.response_str},
-            ],
-        )
+            # Extract structured data from natural language
+            structured_response = await client.chat.completions.create(
+                model=request.model,
+                response_model=response_model,
+                messages=[
+                    {"role": "user", "content": request.response_str},
+                ],
+            )
 
-        return structured_response
+            return structured_response
diff --git a/src/mcp_agent/workflows/llm/augmented_llm_openai.py b/src/mcp_agent/workflows/llm/augmented_llm_openai.py
index 7010a32d5..d8a000cb3 100644
--- a/src/mcp_agent/workflows/llm/augmented_llm_openai.py
+++ b/src/mcp_agent/workflows/llm/augmented_llm_openai.py
@@ -864,7 +864,7 @@ async def request_completion_task(
         """
         Request a completion from OpenAI's API.
         """
-        async_openai_client = AsyncOpenAI(
+        async with AsyncOpenAI(
             api_key=request.config.api_key,
             base_url=request.config.base_url,
             http_client=request.config.http_client
@@ -873,12 +873,11 @@ async def request_completion_task(
             default_headers=request.config.default_headers
             if hasattr(request.config, "default_headers")
             else None,
-        )
-
-        payload = request.payload
-        response = await async_openai_client.chat.completions.create(**payload)
-        response = ensure_serializable(response)
-        return response
+        ) as async_openai_client:
+            payload = request.payload
+            response = await async_openai_client.chat.completions.create(**payload)
+            response = ensure_serializable(response)
+            return response
 
     @staticmethod
     @workflow_task
@@ -902,56 +901,48 @@ async def request_structured_completion_task(
             )
 
         # Next we pass the text through instructor to extract structured data
-        client = instructor.from_openai(
-            AsyncOpenAI(
-                api_key=request.config.api_key,
-                base_url=request.config.base_url,
-                http_client=request.config.http_client
-                if hasattr(request.config, "http_client")
-                else None,
-                default_headers=request.config.default_headers
-                if hasattr(request.config, "default_headers")
-                else None,
-            ),
-            mode=instructor.Mode.TOOLS_STRICT,
-        )
-
-        try:
-            # Extract structured data from natural language
-            structured_response = await client.chat.completions.create(
-                model=request.model,
-                response_model=response_model,
-                messages=[
-                    {"role": "user", "content": request.response_str},
-                ],
-                user=request.user,
-            )
-        except InstructorRetryException:
-            # Retry the request with JSON mode
+        async with AsyncOpenAI(
+            api_key=request.config.api_key,
+            base_url=request.config.base_url,
+            http_client=request.config.http_client
+            if hasattr(request.config, "http_client")
+            else None,
+            default_headers=request.config.default_headers
+            if hasattr(request.config, "default_headers")
+            else None,
+        ) as async_client:
             client = instructor.from_openai(
-                AsyncOpenAI(
-                    api_key=request.config.api_key,
-                    base_url=request.config.base_url,
-                    http_client=request.config.http_client
-                    if hasattr(request.config, "http_client")
-                    else None,
-                    default_headers=request.config.default_headers
-                    if hasattr(request.config, "default_headers")
-                    else None,
-                ),
-                mode=instructor.Mode.JSON,
+                async_client,
+                mode=instructor.Mode.TOOLS_STRICT,
             )
 
-            structured_response = await client.chat.completions.create(
-                model=request.model,
-                response_model=response_model,
-                messages=[
-                    {"role": "user", "content": request.response_str},
-                ],
-                user=request.user,
-            )
+            try:
+                # Extract structured data from natural language
+                structured_response = await client.chat.completions.create(
+                    model=request.model,
+                    response_model=response_model,
+                    messages=[
+                        {"role": "user", "content": request.response_str},
+                    ],
+                    user=request.user,
+                )
+            except InstructorRetryException:
+                # Retry the request with JSON mode
+                client = instructor.from_openai(
+                    async_client,
+                    mode=instructor.Mode.JSON,
+                )
 
-        return structured_response
+                structured_response = await client.chat.completions.create(
+                    model=request.model,
+                    response_model=response_model,
+                    messages=[
+                        {"role": "user", "content": request.response_str},
+                    ],
+                    user=request.user,
+                )
+
+            return structured_response
 
 
 class MCPOpenAITypeConverter(


Test patch:
diff --git a/tests/agents/test_agent_tasks_isolation.py b/tests/agents/test_agent_tasks_isolation.py
new file mode 100644
index 000000000..eeb63a01e
--- /dev/null
+++ b/tests/agents/test_agent_tasks_isolation.py
@@ -0,0 +1,22 @@
+import pytest
+
+from mcp_agent.core.context import initialize_context
+from mcp_agent.agents.agent import AgentTasks
+
+
+@pytest.mark.anyio
+async def test_agent_tasks_instance_scoped_state_isolation():
+    ctx = await initialize_context()
+
+    tasks_a = AgentTasks(context=ctx)
+    tasks_b = AgentTasks(context=ctx)
+
+    # They should not share aggregator dicts or locks
+    assert (
+        tasks_a.server_aggregators_for_agent is not tasks_b.server_aggregators_for_agent
+    )
+    assert (
+        tasks_a.server_aggregators_for_agent_lock
+        is not tasks_b.server_aggregators_for_agent_lock
+    )
+    assert tasks_a.agent_refcounts is not tasks_b.agent_refcounts
diff --git a/tests/integration/test_multithread_smoke.py b/tests/integration/test_multithread_smoke.py
new file mode 100644
index 000000000..08fd2d19b
--- /dev/null
+++ b/tests/integration/test_multithread_smoke.py
@@ -0,0 +1,74 @@
+import asyncio
+import concurrent.futures
+from unittest.mock import AsyncMock
+
+from mcp_agent.app import MCPApp
+from mcp_agent.agents.agent import Agent
+
+from mcp_agent.workflows.llm.augmented_llm import RequestParams, AugmentedLLM
+
+
+class _MockLLM(AugmentedLLM):
+    def __init__(self, agent=None, **kwargs):
+        super().__init__(**kwargs)
+        self.agent = agent
+        self.generate_mock = AsyncMock()
+        self.generate_str_mock = AsyncMock()
+        self.generate_structured_mock = AsyncMock()
+
+    async def generate(self, message, request_params=None):
+        return await self.generate_mock(message, request_params)
+
+    async def generate_str(self, message, request_params=None):
+        return await self.generate_str_mock(message, request_params)
+
+    async def generate_structured(self, message, response_model, request_params=None):
+        return await self.generate_structured_mock(
+            message, response_model, request_params
+        )
+
+
+class _MockLLMFactory:
+    def __call__(self, agent):
+        llm = _MockLLM(agent=agent)
+
+        async def _gen_str(message, request_params=None):
+            return "hello"
+
+        llm.generate_str_mock.side_effect = _gen_str
+        llm.generate_mock.side_effect = _gen_str
+        return llm
+
+
+def worker_once() -> str:
+    loop = asyncio.new_event_loop()
+    try:
+        asyncio.set_event_loop(loop)
+
+        async def run_once():
+            app = MCPApp(name="mt_smoke")
+            async with app.run():
+                agent = Agent(
+                    name="worker", instruction="You are concise.", server_names=[]
+                )
+                # Ensure agent uses this app's context (avoid global context across threads)
+                agent.context = app.context
+                await agent.attach_llm(llm_factory=_MockLLMFactory())
+                out = await agent.llm.generate_str(
+                    "Say hello",
+                    request_params=RequestParams(maxTokens=64, max_iterations=1),
+                )
+                return out
+
+        return loop.run_until_complete(run_once())
+    finally:
+        loop.close()
+        asyncio.set_event_loop(None)
+
+
+def test_multithread_smoke_two_workers():
+    # Run two workers concurrently; ensures independent event loops and app instances
+    with concurrent.futures.ThreadPoolExecutor(max_workers=2) as ex:
+        futures = [ex.submit(worker_once) for _ in range(2)]
+        results = [f.result(timeout=20) for f in futures]
+    assert all(isinstance(r, str) and len(r) > 0 for r in results)
diff --git a/tests/mcp/test_connection_manager_lifecycle.py b/tests/mcp/test_connection_manager_lifecycle.py
new file mode 100644
index 000000000..56df66a18
--- /dev/null
+++ b/tests/mcp/test_connection_manager_lifecycle.py
@@ -0,0 +1,21 @@
+import pytest
+
+from mcp_agent.mcp.mcp_connection_manager import MCPConnectionManager
+
+
+class DummyServerRegistry:
+    def __init__(self):
+        self.registry = {}
+        self.init_hooks = {}
+
+
+@pytest.mark.anyio
+async def test_connection_manager_lifecycle_single_loop():
+    mgr = MCPConnectionManager(server_registry=DummyServerRegistry())
+    # Enter context
+    await mgr.__aenter__()
+    # Disconnect (no servers) and exit
+    await mgr.disconnect_all()
+    await mgr.__aexit__(None, None, None)
+    # Should not raise and internal task group should be cleared
+    assert getattr(mgr, "_tg", None) is None
diff --git a/tests/mcp/test_mcp_connection_manager.py b/tests/mcp/test_mcp_connection_manager.py
index 72a0de7bb..8f0075ce2 100644
--- a/tests/mcp/test_mcp_connection_manager.py
+++ b/tests/mcp/test_mcp_connection_manager.py
@@ -158,7 +158,7 @@ async def test_disconnect_server(server_registry, dummy_client_session_factory):
         await server_conn.wait_for_initialized()
         await mgr.disconnect_server("srv1")
         await anyio.sleep(0)  # let event propagate
-        assert server_conn._shutdown_event.is_set()
+        assert server_conn._is_shutdown_requested_flag()
         assert "srv1" not in mgr.running_servers
 
 
@@ -175,8 +175,8 @@ async def test_disconnect_all(server_registry, dummy_client_session_factory):
         await conn2.wait_for_initialized()
         await mgr.disconnect_all()
         await anyio.sleep(0)
-        assert conn1._shutdown_event.is_set()
-        assert conn2._shutdown_event.is_set()
+        assert conn1._is_shutdown_requested_flag()
+        assert conn2._is_shutdown_requested_flag()
         assert mgr.running_servers == {}
 
 
diff --git a/tests/test_app.py b/tests/test_app.py
index 56f8eac01..677163457 100644
--- a/tests/test_app.py
+++ b/tests/test_app.py
@@ -72,8 +72,8 @@ async def run(self):
         return TestWorkflow
 
     @pytest.fixture
-    def test_task(self):
-        """Create a test task function."""
+    def test_task(self, request):
+        """Create a test task function with a unique name per test to avoid collisions."""
 
         async def task_function(param1: str, param2: int = 0):
             """A test task function.
@@ -87,6 +87,10 @@ async def task_function(param1: str, param2: int = 0):
             """
             return f"Task executed with {param1} and {param2}"
 
+        # Ensure a unique function identity to avoid activity name collisions across tests
+        task_function.__name__ = f"task_function_{request.node.name}"
+        task_function.__qualname__ = f"task_function_{request.node.name}"
+
         return task_function
 
     #