langchain - 💡(How to fix) Fix StructuredTool._run() raises NotImplementedError when only coroutine is available

Official PRs (…)
ON THIS PAGE

Recommended Tools

×6

Utilities matched from this issue’s tags and category — try them while you read without losing context.

GitHub issue graph ai analysis

Paste a GitHub issue URL. We fetch that issue, discover linked issues from bodies/comments/timeline, collect linked pull requests, and produce a structured English report.

The report is written in English Markdown for sharing and archival.

Helpful · Quick feedback

Loading…

Error Message

from langchain_mcp_adapters.tools import load_mcp_tools from langchain_mcp_adapters.sessions import StdioConnection

Load an MCP tool (which has coroutine but no func)

conn = StdioConnection( transport='stdio', command='npx', args=['@modelcontextprotocol/server-filesystem', '/tmp'], ) tools = await load_mcp_tools(session=None, connection=conn) t = tools[0] # e.g., 'read_file'

print(f"t.func = {t.func}") # None print(f"t.coroutine = {t.coroutine}") # Not None

This works (async)

result = await t.ainvoke({'path': '/tmp/test.txt'})

This fails (sync)

result = t.invoke({'path': '/tmp/test.txt'})

Raises: NotImplementedError: StructuredTool does not support sync invocation.

Root Cause

The issue is in langchain-core's StructuredTool._run() method (file: langchain_core/tools/structured.py:93):

def _run(
    self,
    *args: Any,
    config: RunnableConfig,
    run_manager: CallbackManagerForToolRun | None = None,
    **kwargs: Any,
) -> Any:
    if self.func:
        if run_manager and signature(self.func).parameters.get("callbacks"):
            kwargs["callbacks"] = run_manager.get_child()
        if config_param := _get_runnable_config_param(self.func):
            kwargs[config_param] = config
        return self.func(*args, **kwargs)
    msg = "StructuredTool does not support sync invocation."
    raise NotImplementedError(msg)

Problem: When self.func is None (which is the case for MCP tools that only have coroutine), the method immediately raises NotImplementedError instead of checking if self.coroutine is available and falling back to async execution.

Code Example

StructuredTool does not support sync invocation.

---

def _run(
    self,
    *args: Any,
    config: RunnableConfig,
    run_manager: CallbackManagerForToolRun | None = None,
    **kwargs: Any,
) -> Any:
    if self.func:
        if run_manager and signature(self.func).parameters.get("callbacks"):
            kwargs["callbacks"] = run_manager.get_child()
        if config_param := _get_runnable_config_param(self.func):
            kwargs[config_param] = config
        return self.func(*args, **kwargs)
    msg = "StructuredTool does not support sync invocation."
    raise NotImplementedError(msg)

---

async def ainvoke(
    self,
    input: str | dict | ToolCall,
    config: RunnableConfig | None = None,
    **kwargs: Any,
) -> Any:
    if not self.coroutine:
        # Fallback to thread pool for sync tools
        return await run_in_executor(config, self.invoke, input, config, **kwargs)
    return await super().ainvoke(input, config, **kwargs)

---

def _run(self, *args, **kwargs) -> Any:
    if self.func:
        return self.func(*args, **kwargs)
    if self.coroutine:
        # Fallback: Execute async coroutine in event loop
        loop = asyncio.get_event_loop()
        return loop.run_until_complete(self.coroutine(*args, **kwargs))
    raise NotImplementedError(msg)

---

from langchain_mcp_adapters.tools import load_mcp_tools
from langchain_mcp_adapters.sessions import StdioConnection

# Load an MCP tool (which has coroutine but no func)
conn = StdioConnection(
    transport='stdio',
    command='npx',
    args=['@modelcontextprotocol/server-filesystem', '/tmp'],
)
tools = await load_mcp_tools(session=None, connection=conn)
t = tools[0]  # e.g., 'read_file'

print(f"t.func = {t.func}")       # None
print(f"t.coroutine = {t.coroutine}")  # Not None

# This works (async)
result = await t.ainvoke({'path': '/tmp/test.txt'})

# This fails (sync)
result = t.invoke({'path': '/tmp/test.txt'})
# Raises: NotImplementedError: StructuredTool does not support sync invocation.
RAW_BUFFERClick to expand / collapse

Problem Description

When using MCP tools (via langchain-mcp-adapters) with frameworks that only support synchronous tool invocation (like DeepAgents/LangGraph's ToolNode), the tools fail with:

StructuredTool does not support sync invocation.

Root Cause Analysis

The issue is in langchain-core's StructuredTool._run() method (file: langchain_core/tools/structured.py:93):

def _run(
    self,
    *args: Any,
    config: RunnableConfig,
    run_manager: CallbackManagerForToolRun | None = None,
    **kwargs: Any,
) -> Any:
    if self.func:
        if run_manager and signature(self.func).parameters.get("callbacks"):
            kwargs["callbacks"] = run_manager.get_child()
        if config_param := _get_runnable_config_param(self.func):
            kwargs[config_param] = config
        return self.func(*args, **kwargs)
    msg = "StructuredTool does not support sync invocation."
    raise NotImplementedError(msg)

Problem: When self.func is None (which is the case for MCP tools that only have coroutine), the method immediately raises NotImplementedError instead of checking if self.coroutine is available and falling back to async execution.

Contrast with ainvoke()

The ainvoke() method correctly handles this case (file: langchain_core/tools/structured.py:66):

async def ainvoke(
    self,
    input: str | dict | ToolCall,
    config: RunnableConfig | None = None,
    **kwargs: Any,
) -> Any:
    if not self.coroutine:
        # Fallback to thread pool for sync tools
        return await run_in_executor(config, self.invoke, input, config, **kwargs)
    return await super().ainvoke(input, config, **kwargs)

Expected Behavior

When func is None but coroutine is available, _run() should fall back to async execution (similar to how ainvoke() falls back to sync execution):

def _run(self, *args, **kwargs) -> Any:
    if self.func:
        return self.func(*args, **kwargs)
    if self.coroutine:
        # Fallback: Execute async coroutine in event loop
        loop = asyncio.get_event_loop()
        return loop.run_until_complete(self.coroutine(*args, **kwargs))
    raise NotImplementedError(msg)

Reproduction

from langchain_mcp_adapters.tools import load_mcp_tools
from langchain_mcp_adapters.sessions import StdioConnection

# Load an MCP tool (which has coroutine but no func)
conn = StdioConnection(
    transport='stdio',
    command='npx',
    args=['@modelcontextprotocol/server-filesystem', '/tmp'],
)
tools = await load_mcp_tools(session=None, connection=conn)
t = tools[0]  # e.g., 'read_file'

print(f"t.func = {t.func}")       # None
print(f"t.coroutine = {t.coroutine}")  # Not None

# This works (async)
result = await t.ainvoke({'path': '/tmp/test.txt'})

# This fails (sync)
result = t.invoke({'path': '/tmp/test.txt'})
# Raises: NotImplementedError: StructuredTool does not support sync invocation.

Impact

This prevents MCP tools from being used with:

  • DeepAgents (uses LangGraph ToolNode which calls invoke() synchronously)
  • Any framework that doesn't use ainvoke() for tool execution

Environment

  • langchain-core: 0.3.x (tested)
  • langchain-mcp-adapters: 0.2.x
  • Python: 3.11+

Vote matrix · Quick signals

Works
Did the solution work? Tap to confirm.
Easy Fix
Was it a quick fix?
Time Saver
Did it save you time?
Blocking
Was it severely blocking?
Common Issue
Are others likely hitting this too?
Flaky / Intermittent
Is it intermittent?
Verified / Reproducible
Can you reproduce it reliably?
Loading…

Still need to ship something?

×6

Another batch ranked right after the header list — different links, same matching logic.

Back to top recommendations

TRENDING