Skip to content

fix: skip tools with langgraph-injected args in bind_tools MCP bridge - #6

Open
jasoncarreira wants to merge 1 commit into
thehumanworks:mainfrom
jasoncarreira:fix/skip-injected-tool-args-in-mcp-bridge
Open

fix: skip tools with langgraph-injected args in bind_tools MCP bridge#6
jasoncarreira wants to merge 1 commit into
thehumanworks:mainfrom
jasoncarreira:fix/skip-injected-tool-args-in-mcp-bridge

Conversation

@jasoncarreira

Copy link
Copy Markdown

Summary

`ClaudeCodeChatModel.bind_tools` wraps each LangChain tool into an SDK MCP server tool by invoking `tool._arun(**user_args)` directly inside `_wrap_langchain_tool`. This bypasses LangChain's normal tool-invocation pipeline — including the langgraph injection step that populates `ToolRuntime` and other `InjectedToolArg`-marked parameters.

The result: any tool whose underlying function takes a runtime-injected arg raises on every invocation through the bridge:

```
TypeError: missing 1 required positional argument: 'runtime'
```

Where this bites

Applications built on `deepagents` (and other langgraph-based frameworks) routinely declare middleware-injected tools — `read_file`, `write_file`, `edit_file`, `ls`, `glob`, `grep` from `deepagents.middleware.filesystem.FilesystemMiddleware` all take a `runtime: ToolRuntime[...]` parameter. When such an app is bound through `ClaudeCodeChatModel`, every one of those tools fails on every invocation. The agent typically retries a couple of times then falls back to a same-named framework-native path that works — at the cost of wasted turns and confusing traces in logs.

Real-world failure observed in a deepagents 0.6-based agent calling `mcp__langchain-tools__read_file` three times before falling back to the framework's native `Read` tool.

Fix

Detect tools with runtime-injected args at bind time and skip them. The new `_has_runtime_injected_args` helper:

  • Walks the tool's underlying callable (`coroutine` / `func` for StructuredTool, `_arun` / `_run` for BaseTool subclasses).
  • Checks each parameter's annotation against:
    • `_DirectlyInjectedToolArg` base class from `langgraph.prebuilt.tool_node` — covers `ToolRuntime[X, Y]` bare-type annotations.
    • `InjectedToolArg` from `langchain_core.tools.base` — covers `Annotated[T, InjectedToolArg(...)]` form.
  • Both imports are guarded by try/except so the helper degrades to a no-op when langgraph isn't installed.

Skipped tools log an INFO line but the bind is otherwise silent — the framework's native tool path still serves them when the model needs them.

Why skip rather than fix the bridge

The MCP transport doesn't carry langgraph state (no access to the graph runtime, channels, store, etc.). Even if we constructed a synthetic `ToolRuntime` and passed it in, the tool body would then call `_get_backend(runtime)` and fail because the synthetic runtime has no backend wired up. The right answer is: don't bridge tools that need state they can't get over MCP. Frameworks like deepagents already expose these tools natively to the model through their own middleware, so the bridge wrapping was redundant in the first place.

Tests

New `test_bind_tools_skips_runtime_injected_args`:

  • Asserts the helper returns True for a tool whose `_run` takes `ToolRuntime[Any, dict]`.
  • Asserts the helper returns False for a plain tool with no injection.
  • Asserts `bind_tools` filters the injected tool out of `allowed_tools` while keeping the plain tool.

`langgraph>=0.2` added to dev dependencies (only used in the new test; production runtime stays optional via import guards).

All 12 existing tests pass.

🤖 Generated with Claude Code

``ClaudeCodeChatModel.bind_tools`` wraps each LangChain tool into an
SDK MCP server tool by invoking ``tool._arun(**user_args)`` directly
inside ``_wrap_langchain_tool``. This bypasses LangChain's normal
tool-invocation pipeline — including the langgraph injection step
that populates ``ToolRuntime`` and other ``InjectedToolArg``-marked
parameters.

The result: any tool whose underlying function takes a runtime-
injected arg (notably ``ToolRuntime`` from
``langgraph.prebuilt.tool_node``) raises on every invocation through
the bridge:

    TypeError: missing 1 required positional argument: 'runtime'

In the wild this surfaces when an application built on
``deepagents`` is bound through ``ClaudeCodeChatModel``: the
framework's middleware-injected filesystem tools (``read_file``,
``write_file``, ``edit_file``, ``ls``, ``glob``, ``grep``) all carry
the ``ToolRuntime`` injection, so all of them fail when routed
through MCP. The agent typically retries a couple of times then
falls back to a same-named framework-native path that works — at
the cost of wasted turns and confusing log traces.

Fix: detect these tools at bind time via a new
``_has_runtime_injected_args`` helper and skip them. Detection walks
the tool's coroutine / func / class-method signatures and checks
each parameter's annotation against the ``_DirectlyInjectedToolArg``
base class (covers ``ToolRuntime[X, Y]`` bare-type annotations) and
the ``InjectedToolArg`` marker (covers ``Annotated[T,
InjectedToolArg(...)]`` form). Both imports are guarded by try/except
so the helper degrades to a no-op when langgraph isn't installed.

Skipped tools are logged but otherwise silent — the framework's
native tool path still serves them when the model needs them
(deepagents auto-injects filesystem tools into every model request
via SubAgent middleware, regardless of what's in the MCP allowlist).

Adds ``langgraph>=0.2`` to dev dependencies for the new test —
``langgraph`` stays optional at runtime via the import guards.
jasoncarreira added a commit to jasoncarreira/mimir that referenced this pull request May 22, 2026
Pins the dep to the ``mimir-bundled-fixes`` integration branch on
jasoncarreira/langchain-claude-code, which combines three fixes
currently open as PRs upstream:

* thehumanworks/langchain-claude-code#2 — preserve full
  ``ResultMessage`` field set on ``response_metadata`` in both
  streaming and non-streaming paths
* thehumanworks/langchain-claude-code#4 — capture all tool
  calls/results via SDK hooks instead of inferring from the
  message stream (closes upstream issue #3)
* thehumanworks/langchain-claude-code#6 — skip tools with
  langgraph-injected args in the bind_tools MCP bridge, fixing
  ``TypeError: missing 1 required positional argument: 'runtime'``
  on every ``mcp__langchain-tools__read_file`` / ``write_file`` /
  ``edit_file`` invocation when running on deepagents 0.6+

The third one is the load-bearing fix for the production-observed
duplicate-tools failure on mimirbot (turn 078eb0f98a1b on
2026-05-22): the framework's middleware-injected filesystem tools
were also being bridged through MCP, and every bridged invocation
failed with the missing-runtime error. The agent retried a few
times before falling back to the native framework tool (``Read``),
wasting prompt tokens and turn cycles in the process.

Adds ``[tool.hatch.metadata] allow-direct-references = true`` so
hatchling permits the ``pkg @ git+https://...`` form in
``optional-dependencies``. Reverts to the registry version (and
drops the flag) once upstream PRs land + a new PyPI release ships.

Three places in pyproject.toml carry the pin (claude-code extra,
dev extra, deepagents extra) — all updated to the same SHA.

Co-authored-by: Jason Carreira <jason@visotrust.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
jasoncarreira added a commit to jasoncarreira/mimir that referenced this pull request May 24, 2026
Pins the dep to the ``mimir-bundled-fixes`` integration branch on
jasoncarreira/langchain-claude-code, which combines three fixes
currently open as PRs upstream:

* thehumanworks/langchain-claude-code#2 — preserve full
  ``ResultMessage`` field set on ``response_metadata`` in both
  streaming and non-streaming paths
* thehumanworks/langchain-claude-code#4 — capture all tool
  calls/results via SDK hooks instead of inferring from the
  message stream (closes upstream issue #3)
* thehumanworks/langchain-claude-code#6 — skip tools with
  langgraph-injected args in the bind_tools MCP bridge, fixing
  ``TypeError: missing 1 required positional argument: 'runtime'``
  on every ``mcp__langchain-tools__read_file`` / ``write_file`` /
  ``edit_file`` invocation when running on deepagents 0.6+

The third one is the load-bearing fix for the production-observed
duplicate-tools failure on mimirbot (turn 078eb0f98a1b on
2026-05-22): the framework's middleware-injected filesystem tools
were also being bridged through MCP, and every bridged invocation
failed with the missing-runtime error. The agent retried a few
times before falling back to the native framework tool (``Read``),
wasting prompt tokens and turn cycles in the process.

Adds ``[tool.hatch.metadata] allow-direct-references = true`` so
hatchling permits the ``pkg @ git+https://...`` form in
``optional-dependencies``. Reverts to the registry version (and
drops the flag) once upstream PRs land + a new PyPI release ships.

Three places in pyproject.toml carry the pin (claude-code extra,
dev extra, deepagents extra) — all updated to the same SHA.

Co-authored-by: Jason Carreira <jason@visotrust.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
jasoncarreira added a commit to jasoncarreira/mimir that referenced this pull request May 25, 2026
Pins the dep to the ``mimir-bundled-fixes`` integration branch on
jasoncarreira/langchain-claude-code, which combines three fixes
currently open as PRs upstream:

* thehumanworks/langchain-claude-code#2 — preserve full
  ``ResultMessage`` field set on ``response_metadata`` in both
  streaming and non-streaming paths
* thehumanworks/langchain-claude-code#4 — capture all tool
  calls/results via SDK hooks instead of inferring from the
  message stream (closes upstream issue #3)
* thehumanworks/langchain-claude-code#6 — skip tools with
  langgraph-injected args in the bind_tools MCP bridge, fixing
  ``TypeError: missing 1 required positional argument: 'runtime'``
  on every ``mcp__langchain-tools__read_file`` / ``write_file`` /
  ``edit_file`` invocation when running on deepagents 0.6+

The third one is the load-bearing fix for the production-observed
duplicate-tools failure on mimirbot (turn 078eb0f98a1b on
2026-05-22): the framework's middleware-injected filesystem tools
were also being bridged through MCP, and every bridged invocation
failed with the missing-runtime error. The agent retried a few
times before falling back to the native framework tool (``Read``),
wasting prompt tokens and turn cycles in the process.

Adds ``[tool.hatch.metadata] allow-direct-references = true`` so
hatchling permits the ``pkg @ git+https://...`` form in
``optional-dependencies``. Reverts to the registry version (and
drops the flag) once upstream PRs land + a new PyPI release ships.

Three places in pyproject.toml carry the pin (claude-code extra,
dev extra, deepagents extra) — all updated to the same SHA.

Co-authored-by: Jason Carreira <jason@visotrust.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants