LangChain MCPAdapter: gate paid MCP tools

LangChain's MCPAdapter example gates tools on destructive_hint, but Sume's paid tools report false. Gate on read_only_hint instead, per the snapshot.

5 min readSume
All posts

With LangChain's MCPAdapter, you can pause paid tools by reading each tool's read_only_hint from its metadata and passing a when predicate to HumanInTheLoopMiddleware. Do not copy the docs' destructive_hint predicate as it is: in Sume's current MCP code, paid tools such as generate_video report destructiveHint: false, so that predicate would let them run without a pause.

The LangChain facts come from its MCP and MCP tools pages, read 2026-09-29; the langchain.mcp namespace is marked beta there. The Sume facts come from MCP tools and gates and the MCP server code. Sume has no LangChain connector: hosted MCP is a plain MCP endpoint, and a LangChain agent reaches it through the adapter.

What does the LangChain docs example gate on?

The docs say MCPAdapter surfaces annotations under metadata["mcp"]["tool"]["annotations"], using snake_case keys such as destructive_hint and read_only_hint. Their human-in-the-loop example reads destructive_hint once at load time, builds a set of tool names, and returns whether the pending tool call is in it from an InterruptOnConfig when callable. The reasoning they give is to gate on what the server declares, not on hardcoded tool names.

That is a good structure, but the choice of hint matters, because every hint is optional and server-defined.

Which hints do Sume's tools send?

In the current MCP server code, read tools (tools_list, jobs_status, jobs_wait) are readOnlyHint: true. Write and paid tools use a shared write helper that sets readOnlyHint: false and destructiveHint: false, and only jobs_cancel sets destructiveHint: true. This is code behavior, not a documented contract, so check tools_list in your session.

From the Sume MCP server code (current main) and the LangChain MCP tools page, read 2026-09-29.
Toolread_only_hintdestructive_hintPaused by a destructive_hint gate?
tools_list, jobs_status, jobs_waittruefalseNo
generate_video (paid)falsefalseNo
jobs_cancelfalsetrueYes

How do I gate on read_only_hint?

Flip the predicate: pause every tool that is not explicitly read-only. This mirrors the conservative policy the Vercel AI SDK docs give for MCP annotations. Connect the adapter to Sume's hosted MCP endpoint with a Bearer key as LangChain's Authentication page describes; the sketch keeps that target as a parameter.

from langchain.agents import create_agent
from langchain.agents.middleware import HumanInTheLoopMiddleware
from langchain.agents.middleware.human_in_the_loop import InterruptOnConfig
from langchain.mcp import MCPAdapter
from langchain.tools import BaseTool
from langgraph.checkpoint.memory import InMemorySaver

def not_read_only(tool: BaseTool) -> bool:
    ann = (tool.metadata or {}).get("mcp", {}).get("tool", {}).get("annotations", {})
    return ann.get("read_only_hint") is not True

async def build_agent(server):
    async with MCPAdapter(server) as adapter:
        tools = await adapter.list_tools()
        gated = {t.name for t in tools if not_read_only(t)}
        gate = InterruptOnConfig(
            allowed_decisions=["approve", "reject"],
            when=lambda request: request.tool_call["name"] in gated,
        )
        return create_agent(
            "claude-sonnet-5", tools,
            middleware=[HumanInTheLoopMiddleware(
                interrupt_on={t.name: gate for t in tools})],
            checkpointer=InMemorySaver(),
        )

How do I approve or reject a paused call?

When the predicate returns true, the run pauses. The docs resume it with Command(resume={"decisions": [{"type": "approve"}]}) through agent.ainvoke, and a rejection skips the tool and tells the model. The docs' example builds the agent with a checkpointer (InMemorySaver) so the paused run has somewhere to wait.

Sume's own idempotency_key on paid MCP tools stays required either way; Sume's docs describe it as a key for transport and dedup, not human approval, so the pause above is what supplies the human decision.

What are the limits of this approach?

Annotations are hints. Pair them with an allowlist of tool names and a scoped key.

  • The langchain.mcp namespace is in beta and the LangChain docs say the API may change.
  • The adapter's annotation keys and Sume's hint values are read at load time. If a server sends no annotations, metadata may lack them, which is why the predicate treats a missing hint as "not read-only".
  • The predicate also sees request.tool_call["args"], so you can pause only above a spend you choose, and Sume's optional max_spend_usd on hosted MCP tools is enforced only when you send it.

Sources

Related posts

More in Integrations

All Integrations posts

Written by Sume