Repository: Azure/azure-mcp (2025-07-15T15:10:37Z)

Issue:
Error: ToolException('The tool azmcp-group-list was not found') when running Azure MCP server with LangGraph. It used to work in earlier versions.
**Note: It used to work in earlier versions.**

I am using LangGraph to run an Azure MCP server. It was working well previously, but suddenly it stopped working, and now I encounter the following error:

`Error: ToolException('The tool azmcp-group-list was not found') Please fix your mistakes. Please fix your mistakes`

![Image](https://github.com/user-attachments/assets/cf981870-3833-4071-a09f-02409d8ee2b2)

It appears that the system is attempting to use a tool named azmcp-group-list, but this tool does not exist or is not available in my environment. I have not made any changes to my code or configuration related to this tool.

Below is how I have configured the Azure MCP server with Langgraph:

```
client = MultiServerMCPClient(
    connections={
        "azure": {
            "command": "npx",
            "args": ["-y", "@azure/mcp@latest", "server", "start"],
            "env": None,
            "transport": "stdio"
        }
    }
)

tools = await client.get_tools()

agent = create_react_agent(
    llm,
    tools,
    prompt=system_message,
)
```

No issues found in my .env or codebase related to this tool. Please advise on how to resolve this error or update the automation to avoid referencing unavailable tools.

**Note: It used to work in earlier versions.**
Getting "The tool <tool-name> was not found" on first call to a fresh server instance
### Description

Starting with version `0.3.1` and introduced in [this commit](https://github.com/Azure/azure-mcp/commit/72be8a8163fb945ce1a6cd088b08c37b4e99beeb), there's an issue when making the first call to a fresh instance of the Azure MCP server on VSCode where the message "The tool \<tool-name> was not found" will be returned and the call will fail.

### Steps to reproduce

The issue will present itself when the server is in a "Stopped" state and a user asks an agent to perform a task that should call an `azmcp` tool (e.g. "Show me my Azure subscriptions" will make an agent attempt to call `azmcp-subscription-list`). This will cause the agent (or maybe VSCode?) to start the server and attempt the command right away. If the agent is unsuccessful at first, a second attempt to call a command will usually succeed:

<img width="427" height="601" alt="Image" src="https://github.com/user-attachments/assets/7c83fe28-36ce-440d-a18b-27cf4a0ce522" />

Using a new chat or a freshly opened VSCode window does not make a difference.

### Server configuration

The issue can be found using the standard Azure MCP server configuration:

```json
{
    "servers": {
        "Azure Server": {
            "command": "npx",
            "args": [
                "-y",
                "@azure/mcp@latest",
                "server",
                "start"
            ],
            "type": "stdio"
        },
    }
    "inputs": []
}
```

### Possible cause

Normally, when the server is started manually by clicking _Start_ in the **mcp.json** file

<img width="315" height="299" alt="Image" src="https://github.com/user-attachments/assets/1519ba75-181c-4ada-9204-eccf514c7997" />

or _Start Server_ from VSCode's top menu

<img width="908" height="202" alt="Image" src="https://github.com/user-attachments/assets/dc93081f-8a3c-451f-b9bb-7266d9ebbb03" />

Tool discovery will be the first thing to occur:

```bash
2025-07-14 19:35:51.800 [info] Starting server Azure Server Local
2025-07-14 19:35:51.801 [info] Connection state: Starting
2025-07-14 19:35:51.816 [info] Starting server from LocalProcess extension host
2025-07-14 19:35:51.825 [info] Connection state: Starting
2025-07-14 19:35:51.825 [info] Connection state: Running
2025-07-14 19:35:53.001 [info] Discovered 71 tools
```

I suspect this issue is related to the agent attempting to call the tool before the server's tools are even listed. This can happen if you've used the server at least one time allowing for VSCode to cache its tool list and the agent to be aware of which tools it can call. An example of this mismatched timing can be observed in the following video:

https://github.com/user-attachments/assets/79cd0860-c5a2-4fd6-8ee0-033df2e20d1b

A seemingly related issue with our live tests was fixed by adding a call to `McpClientExtensions.ListToolsAsync()` in [this PR](https://github.com/Azure/azure-mcp/pull/542).


Discussion:
Hi @iamchandanys, thanks for reaching out to us on GitHub and reporting this issue.

Just a note that the tool does exist:
https://github.com/Azure/azure-mcp/blob/main/docs/azmcp-commands.md#azure-resource-group-operations
```
# List resource groups in a subscription
azmcp group list --subscription <subscription>
```

@hallipr are there any logs or additional details you need to assist @iamchandanys?
@iamchandanys Can you try and ask the agent to perform the same task a second time and see if that works? I suspect the problem lies when performing the very first tool call.
@joshfree hi, thanks for your reply.

@joshfree @hallipr I'm getting a 'tool not found' error for every tool. The same code worked in earlier versions. Here are some logs:

**Sample 1:**

**Input:**
`[AIMessage(content='', additional_kwargs={'tool_calls': [{'id': 'call_zY4FLhsOE3Caz28Z8tIo2AmS', 'function': {'arguments': '{"subscription":"***","account-name":"***","database-name":"***","container-name":"claims","query":"SELECT * FROM claims c WHERE c.Name = \'xyz\'"}', 'name': 'azmcp-cosmos-database-container-item-query'}, 'type': 'function'}], 'refusal': None}, response_metadata={'token_usage': {'completion_tokens': 82, 'prompt_tokens': 22940, 'total_tokens': 23022, 'completion_tokens_details': {'accepted_prediction_tokens': 0, 'audio_tokens': 0, 'reasoning_tokens': 0, 'rejected_prediction_tokens': 0}, 'prompt_tokens_details': {'audio_tokens': 0, 'cached_tokens': 22784}}, 'model_name': 'gpt-4o-mini-2024-07-18', 'system_fingerprint': 'fp_efad92c60b', 'id': 'chatcmpl-Brw5LGWf5DD8FRkGAyZUOOzRr8DoT', 'service_tier': None, 'prompt_filter_results': [{'prompt_index': 0, 'content_filter_results': {'hate': {'filtered': False, 'severity': 'safe'}, 'jailbreak': {'filtered': False, 'detected': False}, 'self_harm': {'filtered': False, 'severity': 'safe'}, 'sexual': {'filtered': False, 'severity': 'safe'}, 'violence': {'filtered': False, 'severity': 'safe'}}}], 'finish_reason': 'tool_calls', 'logprobs': None, 'content_filter_results': {}}, id='run--9bfc08bf-7d75-428c-af48-58ada89066c2-0', tool_calls=[{'name': 'azmcp-cosmos-database-container-item-query', 'args': {'subscription': '***', 'account-name': '***', 'database-name': '***', 'container-name': 'claims', 'query': "SELECT * FROM claims c WHERE c.Name = 'xyz'"}, 'id': 'call_zY4FLhsOE3Caz28Z8tIo2AmS', 'type': 'tool_call'}], usage_metadata={'input_tokens': 22940, 'output_tokens': 82, 'total_tokens': 23022, 'input_token_details': {'audio': 0, 'cache_read': 22784}, 'output_token_details': {'audio': 0, 'reasoning': 0}})]`

**Output:**
`[ToolMessage(content="Error: ToolException('The tool azmcp-cosmos-database-container-item-query was not found')\n Please fix your mistakes.", name='azmcp-cosmos-database-container-item-query', id='f25b94a8-3680-41f5-8897-e1007f1ef88d', tool_call_id='call_zY4FLhsOE3Caz28Z8tIo2AmS', status='error')]`

**Sample 2:**
`[AIMessage(content='', additional_kwargs={'tool_calls': [{'id': 'call_WFPeqpMPC9vpgrnav7Psj63R', 'function': {'arguments': '{}', 'name': 'azmcp-subscription-list'}, 'type': 'function'}], 'refusal': None}, response_metadata={'token_usage': {'completion_tokens': 15, 'prompt_tokens': 22938, 'total_tokens': 22953, 'completion_tokens_details': {'accepted_prediction_tokens': 0, 'audio_tokens': 0, 'reasoning_tokens': 0, 'rejected_prediction_tokens': 0}, 'prompt_tokens_details': {'audio_tokens': 0, 'cached_tokens': 22784}}, 'model_name': 'gpt-4o-mini-2024-07-18', 'system_fingerprint': 'fp_efad92c60b', 'id': 'chatcmpl-Brw45R0GyiND9jKdb9sY3X6J6rZ9R', 'service_tier': None, 'prompt_filter_results': [{'prompt_index': 0, 'content_filter_results': {'hate': {'filtered': False, 'severity': 'safe'}, 'jailbreak': {'filtered': False, 'detected': False}, 'self_harm': {'filtered': False, 'severity': 'safe'}, 'sexual': {'filtered': False, 'severity': 'safe'}, 'violence': {'filtered': False, 'severity': 'safe'}}}], 'finish_reason': 'tool_calls', 'logprobs': None, 'content_filter_results': {}}, id='run--99e6ce5c-ff69-41a5-b637-f3554cf4d3fd-0', tool_calls=[{'name': 'azmcp-subscription-list', 'args': {}, 'id': 'call_WFPeqpMPC9vpgrnav7Psj63R', 'type': 'tool_call'}], usage_metadata={'input_tokens': 22938, 'output_tokens': 15, 'total_tokens': 22953, 'input_token_details': {'audio': 0, 'cache_read': 22784}, 'output_token_details': {'audio': 0, 'reasoning': 0}})]`

**Output:**
`[ToolMessage(content="Error: ToolException('The tool azmcp-subscription-list was not found')\n Please fix your mistakes.", name='azmcp-subscription-list', id='1dea905a-7573-4121-9bb1-466334b1f75f', tool_call_id='call_WFPeqpMPC9vpgrnav7Psj63R', status='error')]`

Note: I logged into my console using Azure CLI.


> [@iamchandanys](https://github.com/iamchandanys) Can you try and ask the agent to perform the same task a second time and see if that works? I suspect the problem lies when performing the very first tool call.

@vcolin7 Thanks. I gave it a try, but it didn’t work.
A recent change in the server causes its internal tools list to not be populated until the first `tools/list` call. I would expect `tools = await client.get_tools()` to make that mcp `tools/list` call.   We were seeing a similar error in our integration tests after the change and had to add an initial ListToolsAsync() call in our test initialization to warm up the tool list.
Hi @hallipr, I tried the solution below, but it didn't work. Still the same error,

```
# Explicitly warm up the MCP tool list
tools = await client.get_tools()  # First call to ensure tool list is populated
# Optionally, call again to ensure it's ready (redundant but safe)
tools = await client.get_tools()
```

This is what I get from `tools = await client.get_tools()`

`tools: [StructuredTool(name='microsoft_docs_search', description="Search official Microsoft/Azure documentation to find the most relevant and trustworthy content for a user's query. This tool returns up to 10 high-quality content chunks (each max 500 tokens), extracted from Microsoft Learn and other official sources. Each result includes the article title, URL, and a self-contained content excerpt optimized for fast retrieval and reasoning. Always use this tool to quickly ground your answers in accurate, first-party Microsoft/Azure knowledge.", args_schema={'title': 'microsoft_docs_search', 'description': "Search official Microsoft/Azure documentation to find the most relevant and trustworthy content for a user's query. This tool returns up to 10 high-quality content chunks (each max 500 tokens), extracted from Microsoft Learn and other official sources. Each result includes the article title, URL, and a self-contained content excerpt optimized for fast retrieval and reasoning. Always use this tool to quickly ground your answers in accurate, first-party Microsoft/Azure knowledge.", 'type': 'object', 'properties': {'question': {'description': 'a question or topic about Microsoft/Azure products, services, platforms, developer tools, frameworks, or APIs', 'type': 'string'}}, 'required': ['question']}, response_format='content_and_artifact', coroutine=<function convert_mcp_tool_to_langchain_tool.<locals>.call_tool at 0x000001CC19B1AF20>), StructuredTool(name='azmcp-appconfig-account-list', description='List all App Configuration stores in a subscription. This command retrieves and displays all App Configuration\r\nstores available in the specified subscription. Results include store names returned as a JSON array.', args_schema={'type': 'object', 'properties': {'tenant': {'type': 'string', 'description': 'The Microsoft Entra ID tenant ID or name. This can be either the GUID identifier or the display name of your Entra ID tenant.'}, 'auth-method': {'type': 'integer', 'description': "Authentication method to use. Options: 'credential' (Azure CLI/managed identity), 'key' (access key), or 'connectionString'."}, 'retry-delay': {'type': 'number', 'description': 'Initial delay in seconds between retry attempts. For exponential backoff, this value is used as the base.'}, 'retry-max-delay': {'type': 'number', 'description': 'Maximum delay in seconds between retries, regardless of the retry strategy.'}, 'retry-max-retries': {'type': 'integer', 'description': 'Maximum number of retry attempts for failed operations before giving up.'}, 'retry-mode': {'type': 'integer', 'description': "Retry strategy to use. 'fixed' uses consistent delays, 'exponential' increases delay between attempts."}, 'retry-network-timeout': {'type': 'number', 'description': 'Network operation timeout in seconds. Operations taking longer than this will be cancelled.'}, 'subscription': {'type': 'string', 'description': 'The Azure subscription ID or name. This can be either the GUID identifier or the display name of the Azure subscription to use.'}}, 'required': ['subscription']}, metadata={'title': 'List App Configuration Stores', 'readOnlyHint': True, 'destructiveHint': False, 'idempotentHint': False, 'openWorldHint': True}, response_format='content_and_artifact', coroutine=<function convert_mcp_tool_to_langchain_tool.<locals>.call_tool at 0x000001CC19F2F100>), StructuredTool(name='azmcp-appconfig-kv-delete', description='Delete a key-value pair from an App Configuration store. This command removes the specified key-value pair from the store.\r\nIf a label is specified, only the labeled version is deleted. If no label is specified, the key-value with the matching\r\nkey and the default label will be deleted.', args_schema={'type': 'object', 'properties': {'tenant': {'type': 'string', 'description': 'The Microsoft Entra ID tenant ID or name. This can be either the GUID identifier or the display name of your Entra ID tenant.'}, 'auth-method': {'type': 'integer', 'description': "Authentication method to use. Options: 'credential' (Azure CLI/managed identity), 'key' (access key), or 'connectionString'."}, 'retry-delay': {'type': 'number', 'description': 'Initial delay in seconds between retry attempts. For exponential backoff, this value is used as the base.'}, 'retry-max-delay': {'type': 'number', 'description': 'Maximum delay in seconds between retries, regardless of the retry strategy.'}, 'retry-max-retries': {'type': 'integer', 'description': 'Maximum number of retry attempts for failed operations before giving up.'}, 'retry-mode': {'type': 'integer', 'description': "Retry strategy to use. 'fixed' uses consistent delays, 'exponential' increases delay between attempts."}, 'retry-network-timeout': {'type': 'number', 'description': 'Network operation timeout in seconds. Operations taking longer than this will be cancelled.'}, 'subscription': {'type': 'string', 'description': 'The Azure subscription ID or name. This can be either the GUID identifier or the display name of the Azure subscription to use.'}, 'account-name': {'type': 'string', 'description': 'The name of the App Configuration store (e.g., my-appconfig).'}, 'key': {'type': 'string', 'description': 'The name of the key to access within the App Configuration store.'}, 'label': {'type': 'string', 'description': 'The label to apply to the configuration key. Labels are used to group and organize settings.'}}, 'required': ['subscription', 'account-name', 'key']}, metadata={'title': 'Delete App Configuration Key-Value Setting', 'readOnlyHint': False, 'destructiveHint': True, 'idempotentHint': False, 'openWorldHint': True}, response_format='content_and_artifact', coroutine=<function convert_mcp_tool_to_langchain_tool.<locals>.call_tool at 0x000001CC19FB2DE0>), StructuredTool(name='azmcp-appconfig-kv-list', description='List all key-values in an App Configuration store. This command retrieves and displays all key-value pairs\r\nfrom the specified store. Each key-value includes its key, value, label, content type, ETag, last modified\r\ntime, and lock status.', args_schema={'type': 'object', 'properties': {'tenant': {'type': 'string', 'description': 'The Microsoft Entra ID tenant ID or name. This can be either the GUID identifier or the display name of your Entra ID tenant.'}, 'auth-method': {'type': 'integer', 'description': "Authentication method to use. Options: 'credential' (Azure CLI/managed identity), 'key' (access key), or 'connectionString'."}, 'retry-delay': {'type': 'number', 'description': 'Initial delay in seconds between retry attempts. For exponential backoff, this value is used as the base.'}, 'retry-max-delay': {'type': 'number', 'description': 'Maximum delay in seconds between retries, regardless of the retry strategy.'}, 'retry-max-retries': {'type': 'integer', 'description': 'Maximum number of retry attempts for failed operations before giving up.'}, 'retry-mode': {'type': 'integer', 'description': "Retry strategy to use. 'fixed' uses consistent delays, 'exponential' increases delay between attempts."}, 'retry-network-timeout': {'type': 'number', 'description': 'Network operation timeout in seconds. Operations taking longer than this will be cancelled.'}, 'subscription': {'type': 'string', 'description': 'The Azure subscription ID or name. This can be either the GUID identifier or the display name of the Azure subscription to use.'}, 'account-name': {'type': 'string', 'description': 'The name of the App Configuration store (e.g., my-appconfig).'}, 'key': {'type': 'string', 'description': 'Specifies the key filter, if any, to be used when retrieving key-values. The filter can be an exact match, for example a filter of "foo" would get all key-values with a key of "foo", or the filter can include a \'*\' character at the end of the string for wildcard searches (e.g., \'App*\'). If omitted all keys will be retrieved.'}, 'label': {'type': 'string', 'description': 'Specifies the label filter, if any, to be used when retrieving key-values. The filter can be an exact match, for example a filter of "foo" would get all key-values with a label of "foo", or the filter can include a \'*\' character at the end of the string for wildcard searches (e.g., \'Prod*\'). This filter is case-sensitive. If omitted, all labels will be retrieved.'}}, 'required': ['subscription', 'account-name']}, metadata={'title': 'List App Configuration Key-Value Settings', 'readOnlyHint': True, 'destructiveHint': False, 'idempotentHint': False, 'openWorldHint': True}, response_format='content_and_artifact', coroutine=<function convert_mcp_tool_to_langchain_tool.<locals>.call_tool at 0x000001CC19FB2CA0>), StructuredTool(name='azmcp-appconfig-kv-lock', description='Lock a key-value in an App Configuration store. This command sets a key-value to read-only mode,\r\npreventing any modifications to its value. You must specify an account name and key. Optionally,\r\nyou can specify a label to lock a specific labeled version of the key-value.', args_schema={'type': 'object', 'properties': {'tenant': {'type': 'string', 'description': 'The Microsoft Entra ID tenant ID or name. This can be either the GUID identifier or the display name of your Entra ID tenant.'}, 'auth-method': {'type': 'integer', 'description': "Authentication method to use. Options: 'credential' (Azure CLI/managed identity), 'key' (access key), or 'connectionString'."}, 'retry-delay': {'type': 'number', 'description': 'Initial delay in seconds between retry attempts. For exponential backoff, this value is used as the base.'}, 'retry-max-delay': {'type': 'number', 'description': 'Maximum delay in seconds between retries, regardless of the retry strategy.'}, 'retry-max-retries': {'type': 'integer', 'description': 'Maximum number of retry attempts for failed operations before giving up.'}, 'retry-mode': {'type': 'integer', 'description': "Retry strategy to use. 'fixed' uses consistent delays`

Any suggestions would be beneficial.
Hi @hallipr, @joshfree, @vcolin7 

I've tried everything I could, but I'm still getting the same error.

```
try:
            print("Fetching tools...")
            await client.get_tools()
            tools = await client.get_tools()
        except Exception as e:
            print(f"Error fetching tools: {e}")
            # on first-call failure, do the list + retry
            await client.get_tools()
            tools = await client.get_tools()
```

Any help would be greatly appreciated.
We typically release updates every Tuesday and Thursday. So it will be a few days, at the earliest until the dev who introduced this regression can address it. 

If it's blocking, I would downgrade to the previous release, which will just be ~48 hours older than the current. 
@joshfree Thank you. It was working fine in version 0.2.2, which was released around 22 days ago. The issue started after that. So any chance it's going to be fixed in the next version or anytime soon?
This GitHub issue will be resolved when a fix is checked in. I suspect you can upgrade to 0.3.0 and don't have to run on 0.2.2.

There's no fix yet, as this was the first report of a problem. 
Thank you, @joshfree. However, as mentioned earlier, none of the versions beyond 0.2.2 are working. Upgrading to 0.3.0 does not resolve the issue.

The tool works correctly in environments like the VS Code agent window, but fails when run directly in the console. It consistently throws the same errors, despite the fact that I have already authenticated successfully in the console using Azure CLI commands.

**I can provide the simple Python code if you’d like to cross‑check the issue.**
@wbreza can you please investigate now that you're back today?
Hello @wbreza & @joshfree, in case you need it, here’s the code I’m using: https://github.com/iamchandanys/azure-mcp-with-langgraph/blob/main/src/clients/client.py. Thanks
I suspect the issue here is that, the [client.get_tools()](https://github.com/iamchandanys/azure-mcp-with-langgraph/blob/eef3f9447856903c289743aee5652998cb16e85d/src/clients/client.py#L39) and the [agent.ainvoke()](https://github.com/iamchandanys/azure-mcp-with-langgraph/blob/eef3f9447856903c289743aee5652998cb16e85d/src/clients/client.py#L73) are internally using two different MCP sessions, for example, the `get_tools` creates a session, obtain the tools and may disposes the session.  Each new MCP session creates a new Azure MCP server process, when it ends, it also terminates the server process. In that case the `ainvoke()` likely using a [new session](https://github.com/langchain-ai/langchain-mcp-adapters/blob/54c0a76f2def13f4672605b367dc56ab07ba6b5a/langchain_mcp_adapters/tools.py#L122), it results in a fresh Azure MCP server process without access to any tool context cached by the previously disposed MCP server.

@iamchandanys, would you be able to verify the hypothesis regarding the azmcp process? You may use the function below to print the list of azmcp processes before and after the get_tools call (wait for 2-3 sec), then compare the results. If there is no difference, it indicates that get_tools disposed of the MCP server after retrieving the tools.

```py

import psutil

def get_azmcp_processes():
    processes = []
    for proc in psutil.process_iter(['pid', 'name', 'cmdline']):
        try:
            if proc.info['cmdline']:
                cmdline = ' '.join(proc.info['cmdline'])
                if 'node' in proc.info['name'].lower() and 'azmcp' in cmdline:
                    processes.append(proc.info['pid'])
        except (psutil.NoSuchProcess, psutil.AccessDenied):
            pass
    return set(processes)

```

@wbreza If the hypothesis is correct, it's unclear how common this behavior is among mcp clients, and impact on dynamic loading.


Edit: @iamchandanys On my dev machine, I got `unhandled errors in a TaskGroup` right after trying to get tools; not sure if this is osx specific or just masked or works in your setup. That might explain why I'm seeing sessions close. Just thought I'd let you know.
PR: https://github.com/Azure/azure-mcp/pull/604
Hi @anuchandy, I tried your suggestion, and here’s what I got:

```
azmcp processes before get_tools: set()
azmcp processes after get_tools: set()
```
It's returning a null set before and after. I’ve committed the same code [here](https://github.com/iamchandanys/azure-mcp-with-langgraph/blob/main/src/clients/client.py). Could you please check if I’m missing something?

Also, about your other question — **_on my dev machine, I’m seeing unhandled errors in a TaskGroup right after calling get_tools_**. I noticed version 0.4.0 was released yesterday, and it seems to be causing a **new error** at getTools. I’ve given logs in my next comment. So I had to downgrade to 0.3.2 to test your code.
 I noticed **version 0.4.0** was released yesterday, and it seems to be causing a new error at **getTools**. Here are the logs that I got. FYI - @anuchandy @jongio @wbreza 

+ Exception Group Traceback (most recent call last):
  |   File "C:\Users\Chandan Y S\Desktop\My Repos\azure-mcp-with-langgraph\app-console.py", line 83, in <module>
  |     asyncio.run(chatbot.chat())
  |   File "C:\anaconda\Lib\asyncio\runners.py", line 194, in run
  |     return runner.run(main)
  |            ^^^^^^^^^^^^^^^^
  |   File "C:\anaconda\Lib\asyncio\runners.py", line 118, in run
  |     return self._loop.run_until_complete(task)
  |            ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  |   File "C:\anaconda\Lib\asyncio\base_events.py", line 687, in run_until_complete
  |     return future.result()
  |            ^^^^^^^^^^^^^^^
  |   File "C:\Users\Chandan Y S\Desktop\My Repos\azure-mcp-with-langgraph\app-console.py", line 59, in chat
  |     result = await self.client.main(
  |              ^^^^^^^^^^^^^^^^^^^^^^^
  |   File "C:\Users\Chandan Y S\Desktop\My Repos\azure-mcp-with-langgraph\src\clients\client.py", line 58, in main
  |     tools = await client.get_tools()
  |             ^^^^^^^^^^^^^^^^^^^^^^^^
  |   File "C:\anaconda\Lib\site-packages\langchain_mcp_adapters\client.py", line 137, in get_tools
  |     tools_list = await asyncio.gather(*load_mcp_tool_tasks)
  |                  ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  |   File "C:\anaconda\Lib\site-packages\langchain_mcp_adapters\tools.py", line 133, in load_mcp_tools
  |     async with create_session(connection) as tool_session:
  |                ^^^^^^^^^^^^^^^^^^^^^^^^^^
  |   File "C:\anaconda\Lib\contextlib.py", line 231, in __aexit__
  |     await self.gen.athrow(value)
  |   File "C:\anaconda\Lib\site-packages\langchain_mcp_adapters\sessions.py", line 310, in create_session
  |     async with _create_stdio_session(
  |                ^^^^^^^^^^^^^^^^^^^^^^
  |   File "C:\anaconda\Lib\contextlib.py", line 231, in __aexit__
  |     await self.gen.athrow(value)
  |   File "C:\anaconda\Lib\site-packages\langchain_mcp_adapters\sessions.py", line 164, in _create_stdio_session
  |     async with stdio_client(server_params) as (read, write):
  |                ^^^^^^^^^^^^^^^^^^^^^^^^^^^
  |   File "C:\anaconda\Lib\contextlib.py", line 231, in __aexit__
  |     await self.gen.athrow(value)
  |   File "C:\anaconda\Lib\site-packages\mcp\client\stdio\__init__.py", line 173, in stdio_client
  |     anyio.create_task_group() as tg,
  |     ^^^^^^^^^^^^^^^^^^^^^^^^^
  |   File "C:\anaconda\Lib\site-packages\anyio\_backends\_asyncio.py", line 772, in __aexit__
  |     raise BaseExceptionGroup(
  | ExceptionGroup: unhandled errors in a TaskGroup (1 sub-exception)
  +-+---------------- 1 ----------------
    | Exception Group Traceback (most recent call last):
    |   File "C:\anaconda\Lib\site-packages\mcp\client\stdio\__init__.py", line 179, in stdio_client
    |     yield read_stream, write_stream
    |   File "C:\anaconda\Lib\site-packages\langchain_mcp_adapters\sessions.py", line 165, in _create_stdio_session
    |     async with ClientSession(read, write, **(session_kwargs or {})) as session:
    |                ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
    |   File "C:\anaconda\Lib\site-packages\mcp\shared\session.py", line 218, in __aexit__
    |     return await self._task_group.__aexit__(exc_type, exc_val, exc_tb)
    |            ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
    |   File "C:\anaconda\Lib\site-packages\anyio\_backends\_asyncio.py", line 772, in __aexit__
    |     raise BaseExceptionGroup(
    | ExceptionGroup: unhandled errors in a TaskGroup (1 sub-exception)
    +-+---------------- 1 ----------------
      | Traceback (most recent call last):
      |   File "C:\anaconda\Lib\site-packages\langchain_mcp_adapters\sessions.py", line 166, in _create_stdio_session
      |     yield session
      |   File "C:\anaconda\Lib\site-packages\langchain_mcp_adapters\sessions.py", line 321, in create_session
      |     yield session
      |   File "C:\anaconda\Lib\site-packages\langchain_mcp_adapters\tools.py", line 134, in load_mcp_tools
      |     await tool_session.initialize()
      |   File "C:\anaconda\Lib\site-packages\mcp\client\session.py", line 151, in initialize
      |     result = await self.send_request(
      |              ^^^^^^^^^^^^^^^^^^^^^^^^
      |   File "C:\anaconda\Lib\site-packages\mcp\shared\session.py", line 286, in send_request
      |     raise McpError(response_or_error.error)
      | mcp.shared.exceptions.McpError: Connection closed
      +------------------------------------
this is not fixed completely, reopening.
@iamchandanys, we have a [fix](https://github.com/Azure/azure-mcp/pull/644) for the `Taskgroup` error that I encountered in my dev box. To me this was reproducible even with 0.2.2.
Hi @anuchandy, Thanks for looking into and fixing the issue. I really appreciate it. Could you please let me know when the new NPM version will be released? If I'm not mistaken, it's supposed to be today (Thursday), right?

_To me this was reproducible even with 0.2.2_ - It was working fine when I first started the project, but then it suddenly stopped. I’m not exactly sure which version it was working on. I just guessed based on the day I began the project.
Hello @iamchandanys, [0.4.1](https://www.npmjs.com/package/@azure/mcp?activeTab=readme) is released with this change. 
Hi @anuchandy, I’m getting the following error now. It might not be related to this ticket, but any help would be greatly appreciated.

I’ve double-checked everything. I’m logged into the correct Azure tenant, and `az account show` displays the right details. Still, I’m not sure why this error keeps coming up.

`{'tools': {'messages': [ToolMessage(content='Error: ToolException(\'{"status":401,"message":"The \\\\u0027EvolvedSecurityTokenService\\\\u0027 access token is from the wrong issuer \\\\u0027https://sts.windows.net/f8cdef31-a31e-4b4a-93e4-5f571e91255a/\\\\u0027. It must match the tenant \\\\u0027https://sts.windows.net/6a5706f6-94a9-4f39-b715-20dafbbb3b76/\\\\u0027 associated with this subscription. Please use the authority (URL) \\\\u0027https://login.windows.net/6a5706f6-94a9-4f39-b715-20dafbbb3b76\\\\u0027 to get the token. Note, if the subscription is transferred to another tenant there is no impact to the services, but information about new tenant could take time to propagate (up to an hour). If you just transferred your subscription and see this error message, please try back later.\\\\r\\\\nStatus: 401 (Unauthorized)\\\\r\\\\nErrorCode: InvalidAuthenticationTokenTenant\\\\r\\\\n\\\\r\\\\nContent:\\\\r\\\\n{\\\\u0022error\\\\u0022:{\\\\u0022code\\\\u0022:\\\\u0022InvalidAuthenticationTokenTenant\\\\u0022,\\\\u0022message\\\\u0022:\\\\u0022The \\\\u0027EvolvedSecurityTokenService\\\\u0027 access token is from the wrong issuer \\\\u0027https://sts.windows.net/f8cdef31-a31e-4b4a-93e4-5f571e91255a/\\\\u0027. It must match the tenant \\\\u0027https://sts.windows.net/6a5706f6-94a9-4f39-b715-20dafbbb3b76/\\\\u0027 associated with this subscription. Please use the authority (URL) \\\\u0027https://login.windows.net/6a5706f6-94a9-4f39-b715-20dafbbb3b76\\\\u0027 to get the token. Note, if the subscription is transferred to another tenant there is no impact to the services, but information about new tenant could take time to propagate (up to an hour). If you just transferred your subscription and see this error message, please try back later.\\\\u0022}}\\\\r\\\\n\\\\r\\\\nHeaders:\\\\r\\\\nCache-Control: no-cache\\\\r\\\\nPragma: no-cache\\\\r\\\\nWWW-Authenticate: Bearer authorization_uri=\\\\u0022https://login.windows.net/6a5706f6-94a9-4f39-b715-20dafbbb3b76\\\\u0022, error=\\\\u0022invalid_token\\\\u0022, error_description=\\\\u0022The primary access token is from the wrong issuer. It must match the tenant associated with this subscription. Please use correct authority to get the token.\\\\u0022\\\\r\\\\nx-ms-failure-cause: REDACTED\\\\r\\\\nx-ms-request-id: a8002504-9881-444c-8d1f-2f24c3113e09\\\\r\\\\nx-ms-correlation-request-id: REDACTED\\\\r\\\\nx-ms-routing-request-id: REDACTED\\\\r\\\\nStrict-Transport-Security: REDACTED\\\\r\\\\nX-Content-Type-Options: REDACTED\\\\r\\\\nX-Cache: REDACTED\\\\r\\\\nX-MSEdge-Ref: REDACTED\\\\r\\\\nDate: Sat, 19 Jul 2025 04:45:23 GMT\\\\r\\\\nContent-Length: 707\\\\r\\\\nContent-Type: application/json; charset=utf-8\\\\r\\\\nExpires: -1\\\\r\\\\n. To mitigate this issue, please refer to the troubleshooting guidelines here at https://aka.ms/azmcp/troubleshooting.","results":{"message":"The \\\\u0027EvolvedSecurityTokenService\\\\u0027 access token is from the wrong issuer \\\\u0027https://sts.windows.net/f8cdef31-a31e-4b4a-93e4-5f571e91255a/\\\\u0027. It must match the tenant \\\\u0027https://sts.windows.net/6a5706f6-94a9-4f39-b715-20dafbbb3b76/\\\\u0027 associated with this subscription. Please use the authority (URL) \\\\u0027https://login.windows.net/6a5706f6-94a9-4f39-b715-20dafbbb3b76\\\\u0027 to get the token. Note, if the subscription is transferred to another tenant there is no impact to the services, but information about new tenant could take time to propagate (up to an hour). If you just transferred your subscription and see this error message, please try back later.\\\\r\\\\nStatus: 401 (Unauthorized)\\\\r\\\\nErrorCode: InvalidAuthenticationTokenTenant\\\\r\\\\n\\\\r\\\\nContent:\\\\r\\\\n{\\\\u0022error\\\\u0022:{\\\\u0022code\\\\u0022:\\\\u0022InvalidAuthenticationTokenTenant\\\\u0022,\\\\u0022message\\\\u0022:\\\\u0022The \\\\u0027EvolvedSecurityTokenService\\\\u0027 access token is from the wrong issuer \\\\u0027https://sts.windows.net/f8cdef31-a31e-4b4a-93e4-5f571e91255a/\\\\u0027. It must match the tenant \\\\u0027https://sts.windows.net/6a5706f6-94a9-4f39-b715-20dafbbb3b76/\\\\u0027 associated with this subscription. Please use the authority (URL) \\\\u0027https://login.windows.net/6a5706f6-94a9-4f39-b715-20dafbbb3b76\\\\u0027 to get the token. Note, if the subscription is transferred to another tenant there is no impact to the services, but information about new tenant could take time to propagate (up to an hour). If you just transferred your subscription and see this error message, please try back later.\\\\u0022}}\\\\r\\\\n\\\\r\\\\nHeaders:\\\\r\\\\nCache-Control: no-cache\\\\r\\\\nPragma: no-cache\\\\r\\\\nWWW-Authenticate: Bearer authorization_uri=\\\\u0022https://login.windows.net/6a5706f6-94a9-4f39-b715-20dafbbb3b76\\\\u0022, error=\\\\u0022invalid_token\\\\u0022, error_description=\\\\u0022The primary access token is from the wrong issuer. It must match the tenant associated with this subscription. Please use correct authority to get the token.\\\\u0022\\\\r\\\\nx-ms-failure-cause: REDACTED\\\\r\\\\nx-ms-request-id: a8002504-9881-444c-8d1f-2f24c3113e09\\\\r\\\\nx-ms-correlation-request-id: REDACTED\\\\r\\\\nx-ms-routing-request-id: REDACTED\\\\r\\\\nStrict-Transport-Security: REDACTED\\\\r\\\\nX-Content-Type-Options: REDACTED\\\\r\\\\nX-Cache: REDACTED\\\\r\\\\nX-MSEdge-Ref: REDACTED\\\\r\\\\nDate: Sat, 19 Jul 2025 04:45:23 GMT\\\\r\\\\nContent-Length: 707\\\\r\\\\nContent-Type: application/json; charset=utf-8\\\\r\\\\nExpires: -1\\\\r\\\\n","stackTrace":"   at Azure.ResourceManager.Resources.SubscriptionsRestOperations.GetAsync(String subscriptionId, CancellationToken cancellationToken)\\\\r\\\\n   at Azure.ResourceManager.Resources.SubscriptionResource.GetAsync(CancellationToken cancellationToken)\\\\r\\\\n   at AzureMcp.Services.Azure.Subscription.SubscriptionService.GetSubscription(String subscription, String tenant, RetryPolicyOptions retryPolicy) in D:\\\\\\\\a\\\\\\\\_work\\\\\\\\1\\\\\\\\s\\\\\\\\src\\\\\\\\Services\\\\\\\\Azure\\\\\\\\Subscription\\\\\\\\SubscriptionService.cs:line 64\\\\r\\\\n   at AzureMcp.Areas.Cosmos.Services.CosmosService.GetCosmosAccountAsync(String subscriptionId, String accountName, String tenant, RetryPolicyOptions retryPolicy) in D:\\\\\\\\a\\\\\\\\_work\\\\\\\\1\\\\\\\\s\\\\\\\\src\\\\\\\\Areas\\\\\\\\Cosmos\\\\\\\\Services\\\\\\\\CosmosService.cs:line 34\\\\r\\\\n   at AzureMcp.Areas.Cosmos.Services.CosmosService.CreateCosmosClientWithAuth(String accountName, String subscriptionId, AuthMethod authMethod, String tenant, RetryPolicyOptions retryPolicy) in D:\\\\\\\\a\\\\\\\\_work\\\\\\\\1\\\\\\\\s\\\\\\\\src\\\\\\\\Areas\\\\\\\\Cosmos\\\\\\\\Services\\\\\\\\CosmosService.cs:line 67\\\\r\\\\n   at AzureMcp.Areas.Cosmos.Services.CosmosService.GetCosmosClientAsync(String accountName, String subscriptionId, AuthMethod authMethod, String tenant, RetryPolicyOptions retryPolicy) in D:\\\\\\\\a\\\\\\\\_work\\\\\\\\1\\\\\\\\s\\\\\\\\src\\\\\\\\Areas\\\\\\\\Cosmos\\\\\\\\Services\\\\\\\\CosmosService.cs:line 139\\\\r\\\\n   at AzureMcp.Areas.Cosmos.Services.CosmosService.QueryItems(String accountName, String databaseName, String containerName, String query, String subscriptionId, AuthMethod authMethod, String tenant, RetryPolicyOptions retryPolicy) in D:\\\\\\\\a\\\\\\\\_work\\\\\\\\1\\\\\\\\s\\\\\\\\src\\\\\\\\Areas\\\\\\\\Cosmos\\\\\\\\Services\\\\\\\\CosmosService.cs:line 249\\\\r\\\\n   at AzureMcp.Areas.Cosmos.Commands.ItemQueryCommand.ExecuteAsync(CommandContext context, ParseResult parseResult) in D:\\\\\\\\a\\\\\\\\_work\\\\\\\\1\\\\\\\\s\\\\\\\\src\\\\\\\\Areas\\\\\\\\Cosmos\\\\\\\\Commands\\\\\\\\ItemQueryCommand.cs:line 61","type":"RequestFailedException"},"duration":0}\')\n Please fix your mistakes.', name='azmcp_cosmos_database_container_item_query', id='d7cb1fae-5eed-41af-918f-6672244d5a1b', tool_call_id='call_ZZ4mAsiDrvRMwN8Fsb1OrNil', status='error')]}}`





Patch:
diff --git a/eng/pipelines/templates/steps/analyze-aot-compact.yml b/eng/pipelines/templates/steps/analyze-aot-compact.yml
index 4cf1148a0..191b43f61 100644
--- a/eng/pipelines/templates/steps/analyze-aot-compact.yml
+++ b/eng/pipelines/templates/steps/analyze-aot-compact.yml
@@ -38,3 +38,4 @@ steps:
       ArtifactPath: ${{ parameters.sourceDirectory }}/.work/aotCompactReport
       ArtifactName: AOTCompatibilityReport
       SbomEnabled: ${{ ne(variables['Build.Reason'], 'PullRequest') }}
+      CustomCondition: eq(variables['AOTReportExists'], 'true')
diff --git a/src/Areas/Server/Commands/ToolLoading/CompositeToolLoader.cs b/src/Areas/Server/Commands/ToolLoading/CompositeToolLoader.cs
index 2ba4ec94c..a39506246 100644
--- a/src/Areas/Server/Commands/ToolLoading/CompositeToolLoader.cs
+++ b/src/Areas/Server/Commands/ToolLoading/CompositeToolLoader.cs
@@ -1,6 +1,7 @@
 // Copyright (c) Microsoft Corporation.
 // Licensed under the MIT License.
 
+using System.Threading;
 using Microsoft.Extensions.Logging;
 using ModelContextProtocol.Protocol;
 
@@ -12,11 +13,12 @@ namespace AzureMcp.Areas.Server.Commands.ToolLoading;
 /// </summary>
 /// <param name="toolLoaders">The collection of tool loaders to combine.</param>
 /// <param name="logger">Logger for tool loading operations.</param>
-public sealed class CompositeToolLoader(IEnumerable<IToolLoader> toolLoaders, ILogger<CompositeToolLoader> logger) : IToolLoader
+public sealed class CompositeToolLoader(IEnumerable<IToolLoader> toolLoaders, ILogger<CompositeToolLoader> logger) : IToolLoader, IDisposable
 {
     private readonly ILogger<CompositeToolLoader> _logger = logger ?? throw new ArgumentNullException(nameof(logger));
     private readonly IEnumerable<IToolLoader> _toolLoaders = InitializeToolLoaders(toolLoaders);
     private readonly Dictionary<string, IToolLoader> _toolLoaderMap = new();
+    private readonly SemaphoreSlim _initializationSemaphore = new(1, 1);
 
     /// <summary>
     /// Initializes the list of tool loaders, validating that at least one is provided.
@@ -89,6 +91,9 @@ public async ValueTask<CallToolResult> CallToolHandler(RequestContext<CallToolRe
             };
         }
 
+        // Ensure tool loader map is populated before attempting tool lookup
+        await EnsureToolLoaderMapInitializedAsync(request, cancellationToken);
+
         if (!_toolLoaderMap.TryGetValue(request.Params.Name, out var toolCaller))
         {
             var content = new TextContentBlock
@@ -107,4 +112,46 @@ public async ValueTask<CallToolResult> CallToolHandler(RequestContext<CallToolRe
 
         return await toolCaller.CallToolHandler(request, cancellationToken);
     }
+
+    /// <summary>
+    /// Ensures that the tool loader map is initialized by populating it if it's empty.
+    /// This provides lazy initialization to handle cases where tool calls occur before ListToolsHandler is called.
+    /// Thread-safe initialization using a semaphore to prevent race conditions.
+    /// </summary>
+    /// <param name="request">The request context containing metadata and parameters.</param>
+    /// <param name="cancellationToken">A token to monitor for cancellation requests.</param>
+    private async ValueTask EnsureToolLoaderMapInitializedAsync(RequestContext<CallToolRequestParams> request, CancellationToken cancellationToken)
+    {
+        if (_toolLoaderMap.Count == 0)
+        {
+            await _initializationSemaphore.WaitAsync(cancellationToken);
+            try
+            {
+                // Double-check pattern: verify the map is still empty after acquiring the lock
+                if (_toolLoaderMap.Count == 0)
+                {
+                    // Create a dummy request for listing tools to populate the tool loader map
+                    var listToolsRequest = new RequestContext<ListToolsRequestParams>(request.Server)
+                    {
+                        Params = new ListToolsRequestParams()
+                    };
+
+                    // Populate the tool loader map by calling ListToolsHandler internally
+                    await ListToolsHandler(listToolsRequest, cancellationToken);
+                }
+            }
+            finally
+            {
+                _initializationSemaphore.Release();
+            }
+        }
+    }
+
+    /// <summary>
+    /// Disposes the semaphore used for thread-safe initialization.
+    /// </summary>
+    public void Dispose()
+    {
+        _initializationSemaphore?.Dispose();
+    }
 }


Test patch:
diff --git a/tests/Areas/Server/UnitTests/Commands/ToolLoading/CompositeToolLoaderTests.cs b/tests/Areas/Server/UnitTests/Commands/ToolLoading/CompositeToolLoaderTests.cs
index c74c6ae54..ff47214ff 100644
--- a/tests/Areas/Server/UnitTests/Commands/ToolLoading/CompositeToolLoaderTests.cs
+++ b/tests/Areas/Server/UnitTests/Commands/ToolLoading/CompositeToolLoaderTests.cs
@@ -322,4 +322,133 @@ public async Task ListToolsHandler_WithSingleEmptyToolLoader_ReturnsEmptyResult(
         Assert.NotNull(result.Tools);
         Assert.Empty(result.Tools);
     }
+
+    [Fact]
+    public async Task CallToolHandler_WithoutListingToolsFirst_LazilyInitializesAndCallsTool()
+    {
+        var serviceProvider = CreateServiceProvider();
+        var logger = serviceProvider.GetRequiredService<ILogger<CompositeToolLoader>>();
+
+        // Setup a loader that has the tool we want to call
+        var mockLoader = Substitute.For<IToolLoader>();
+        var testTool = CreateTestTool("test-tool");
+        mockLoader.ListToolsHandler(Arg.Any<RequestContext<ListToolsRequestParams>>(), Arg.Any<CancellationToken>())
+            .Returns(new ListToolsResult { Tools = new List<Tool> { testTool } });
+
+        var expectedResult = new CallToolResult
+        {
+            Content = new List<ContentBlock> { new TextContentBlock { Text = "Tool executed successfully" } },
+            IsError = false
+        };
+        mockLoader.CallToolHandler(Arg.Any<RequestContext<CallToolRequestParams>>(), Arg.Any<CancellationToken>())
+            .Returns(expectedResult);
+
+        var toolLoaders = new List<IToolLoader> { mockLoader };
+        var toolLoader = new CompositeToolLoader(toolLoaders, logger);
+
+        // Call tool directly WITHOUT first calling ListToolsHandler
+        var callRequest = CreateCallToolRequest("test-tool");
+        var result = await toolLoader.CallToolHandler(callRequest, CancellationToken.None);
+
+        // Verify the tool was found and executed successfully
+        Assert.NotNull(result);
+        Assert.False(result.IsError);
+        Assert.NotNull(result.Content);
+        Assert.Single(result.Content);
+        var textContent = Assert.IsType<TextContentBlock>(result.Content[0]);
+        Assert.Equal("Tool executed successfully", textContent.Text);
+
+        // Verify that ListToolsHandler was called internally to populate the map
+        await mockLoader.Received(1).ListToolsHandler(Arg.Any<RequestContext<ListToolsRequestParams>>(), Arg.Any<CancellationToken>());
+
+        // Verify that CallToolHandler was called on the loader
+        await mockLoader.Received(1).CallToolHandler(callRequest, Arg.Any<CancellationToken>());
+    }
+
+    [Fact]
+    public async Task CallToolHandler_WithoutListingToolsFirst_ReturnsErrorForUnknownTool()
+    {
+        var serviceProvider = CreateServiceProvider();
+        var logger = serviceProvider.GetRequiredService<ILogger<CompositeToolLoader>>();
+
+        // Setup a loader that has a different tool
+        var mockLoader = Substitute.For<IToolLoader>();
+        var testTool = CreateTestTool("different-tool");
+        mockLoader.ListToolsHandler(Arg.Any<RequestContext<ListToolsRequestParams>>(), Arg.Any<CancellationToken>())
+            .Returns(new ListToolsResult { Tools = new List<Tool> { testTool } });
+
+        var toolLoaders = new List<IToolLoader> { mockLoader };
+        var toolLoader = new CompositeToolLoader(toolLoaders, logger);
+
+        // Call tool directly WITHOUT first calling ListToolsHandler
+        var callRequest = CreateCallToolRequest("unknown-tool");
+        var result = await toolLoader.CallToolHandler(callRequest, CancellationToken.None);
+
+        // Verify the tool was not found
+        Assert.NotNull(result);
+        Assert.True(result.IsError);
+        Assert.NotNull(result.Content);
+        Assert.Single(result.Content);
+        var textContent = Assert.IsType<TextContentBlock>(result.Content[0]);
+        Assert.Equal("The tool unknown-tool was not found", textContent.Text);
+
+        // Verify that ListToolsHandler was called internally to populate the map
+        await mockLoader.Received(1).ListToolsHandler(Arg.Any<RequestContext<ListToolsRequestParams>>(), Arg.Any<CancellationToken>());
+
+        // Verify that CallToolHandler was NOT called since the tool was not found
+        await mockLoader.DidNotReceive().CallToolHandler(Arg.Any<RequestContext<CallToolRequestParams>>(), Arg.Any<CancellationToken>());
+    }
+
+    [Fact]
+    public async Task CallToolHandler_ConcurrentCallsWithoutListingFirst_InitializesOnlyOnce()
+    {
+        var serviceProvider = CreateServiceProvider();
+        var logger = serviceProvider.GetRequiredService<ILogger<CompositeToolLoader>>();
+
+        // Setup a loader that has the tool we want to call
+        var mockLoader = Substitute.For<IToolLoader>();
+        var testTool = CreateTestTool("test-tool");
+        mockLoader.ListToolsHandler(Arg.Any<RequestContext<ListToolsRequestParams>>(), Arg.Any<CancellationToken>())
+            .Returns(new ListToolsResult { Tools = new List<Tool> { testTool } });
+
+        var expectedResult = new CallToolResult
+        {
+            Content = new List<ContentBlock> { new TextContentBlock { Text = "Tool executed successfully" } },
+            IsError = false
+        };
+        mockLoader.CallToolHandler(Arg.Any<RequestContext<CallToolRequestParams>>(), Arg.Any<CancellationToken>())
+            .Returns(expectedResult);
+
+        var toolLoaders = new List<IToolLoader> { mockLoader };
+        var toolLoader = new CompositeToolLoader(toolLoaders, logger);
+
+        // Make multiple concurrent calls to the same tool
+        var tasks = new List<Task<CallToolResult>>();
+        const int concurrentCalls = 5;
+
+        for (int i = 0; i < concurrentCalls; i++)
+        {
+            var callRequest = CreateCallToolRequest("test-tool");
+            tasks.Add(toolLoader.CallToolHandler(callRequest, CancellationToken.None).AsTask());
+        }
+
+        var results = await Task.WhenAll(tasks);
+
+        // Verify all calls succeeded
+        foreach (var result in results)
+        {
+            Assert.NotNull(result);
+            Assert.False(result.IsError);
+            Assert.NotNull(result.Content);
+            Assert.Single(result.Content);
+            var textContent = Assert.IsType<TextContentBlock>(result.Content[0]);
+            Assert.Equal("Tool executed successfully", textContent.Text);
+        }
+
+        // Verify that ListToolsHandler was called exactly once (not once per concurrent call)
+        await mockLoader.Received(1).ListToolsHandler(Arg.Any<RequestContext<ListToolsRequestParams>>(), Arg.Any<CancellationToken>());
+
+        // Verify that CallToolHandler was called for each concurrent request
+        await mockLoader.Received(concurrentCalls).CallToolHandler(Arg.Any<RequestContext<CallToolRequestParams>>(), Arg.Any<CancellationToken>());
+    }
 }