-
Notifications
You must be signed in to change notification settings - Fork 2.8k
Description
Checked other resources
- I added a very descriptive title to this issue.
- I searched the LangChain.js documentation with the integrated search.
- I used the GitHub search to find a similar question and didn't find it.
- I am sure that this is a bug in LangChain.js rather than my code.
- The bug is not resolved by updating to the latest stable version of LangChain (or the specific integration package).
Example Code
import { StateGraph, Annotation } from "@langchain/langgraph";
import { ToolNode } from "@langchain/langgraph/prebuilt";
import { MultiServerMCPClient } from "@langchain/mcp-adapters";
// 1) Load your MCP tools…
const client = new MultiServerMCPClient({ /*…*/ });
const tools = await client.getTools();
const toolNode = new ToolNode(tools);
// 2) Build a simple graph that routes every AI message to the tools:
const StateAnn = Annotation.Root({ messages: Annotation() });
async function echoModel(state) {
return { messages: [state.messages.at(-1)!] };
}
function route(state) { return "tools"; }
const graph = new StateGraph(StateAnn)
.addNode("agent", echoModel)
.addNode("tools", toolNode)
.addEdge("__start__", "agent")
.addEdge("agent", "tools")
.addEdge("tools", "agent")
.addConditionalEdges("agent", route)
.compile();
// 3) Bind a timeout+signal on the top‐level runnable…
const runner = graph.withConfig({
timeout: 10_000, // 10 seconds
signal: new AbortController().signal,
});
// 4) Stream… and watch your debug logs inside `ToolNode.run`:
// you’ll see “config.timeout === undefined”
const stream = await runner.stream(
{ messages: [{ role: "user", content: "…" }] },
{ recursionLimit: 5, streamMode: "messages" }
);
Error Message and Stack Trace (if applicable)
No response
Description
LangChain’s MCP adapters docs claim you can set per‐tool timeouts via the standard RunnableConfig interface:
// either…
const slowToolWithTimeout = slowTool.withConfig({ timeout: 300_000 });
await slowToolWithTimeout.invoke({ /*…*/ });
// or…
await slowTool.invoke({ /*…*/ }, { timeout: 300_000 });
When you do
const toolWithTimeout = tool.withConfig({ timeout: 5000 });
you actually get back a RunnableBinding, not a DynamicStructuredTool. A RunnableBinding does not expose the .invoke(args, config) signature that ToolNode expects, so even trying to wrap the tool in a withConfig before passing it to ToolNode breaks invocation entirely.
In addition, when you embed tools with custom timeout in a graph via ToolNode, any { timeout, signal } in the config is silently dropped. Inside ToolNode.run, the config argument passed down to tool.invoke(...) always has timeout === undefined, so the MCP client falls back to its default 1 minute default
System Info
@langchain/core: ^0.3.57
@langchain/mcp-adapters: v0.5.1
@langchain/langgraph: v0.2.74
@modelcontextprotocol/sdk: ^1.11.2
Node.js: 20.18.0