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.

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.
| Tool | read_only_hint | destructive_hint | Paused by a destructive_hint gate? |
|---|---|---|---|
tools_list, jobs_status, jobs_wait | true | false | No |
generate_video (paid) | false | false | No |
jobs_cancel | false | true | Yes |
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.mcpnamespace 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,
metadatamay 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 optionalmax_spend_usdon hosted MCP tools is enforced only when you send it.
Sources
Related posts
More in Integrations
- LangGraph interrupt before a paid API call
Put LangGraph's interrupt() in its own node ahead of the node that calls a paid video API, because a resume re-runs the whole node from its top.
- Looping by Zapier: send one API request per item in a list
Looping by Zapier runs every later action once per list value, all in parallel, up to 500 times. Pace and key paid API calls to match.
- Make.com error handling: retry a paid API call without duplicates
Make.com has five error handlers; Retry stores the failed bundle and reruns it. Send a fixed Idempotency-Key so a rerun can't bill twice.
- MCP config API key: use an environment variable, not the file
Cursor, Claude Code, Windsurf and Codex can read an MCP API key from an environment variable. Here is each client's syntax and what an unset variable does.
Written by Sume