Introduction
Threadplane publishes middleware for backend graphs that need browser-executed client tools. The browser declares tools, the model can call those tools, and the backend routes client-tool-only turns back to the browser for execution.
There are two package surfaces:
| Runtime | Package | Entry point |
|---|---|---|
| LangGraph.js | @threadplane/middleware | @threadplane/middleware/langgraph |
| Python LangGraph | threadplane-middleware | threadplane.middleware.langgraph |
The TypeScript package currently publishes one runtime entry point:
There is no root @threadplane/middleware JavaScript entry point. Import from @threadplane/middleware/langgraph.
#What it does
The LangGraph entry points read a client tool catalog from graph state, convert it into OpenAI function-tool objects, bind those tool stubs onto your chat model, and route client-tool calls to END so the browser can execute them.
The catalog is read from state.tools first. If that channel is absent or empty, both packages fall back to state.client_tools.
#Runtime flow
- The browser sends tool specs with the run request.
- Your LangGraph node calls
bindClientTools()orbind_client_tools()inside the run, because the catalog can differ per request. - The model emits a tool call for a browser-declared tool.
- The router routes client-only tool calls to
END. - The browser executes the local tool and resumes the graph with a
ToolMessage.
If a turn mixes server tool calls and client tool calls, server tools win the first route. The server tool node runs first, and the client call can surface on a later turn.
#TypeScript public surface
@threadplane/middleware/langgraph exports:
| API | Purpose |
|---|---|
clientToolsChannel() | Adds the tools and client_tools state channels to a LangGraph annotation. |
bindClientTools() | Binds server tools plus client-declared tool stubs onto a model. |
clientToolsRouter() | Creates a conditional-edge router for server-tool vs client-tool routing. |
clientToolSpecs() | Converts state catalog entries into OpenAI function-tool specs. |
clientToolNames() | Returns the set of client-declared tool names for a run. |
hasClientToolCall() | Checks whether the last message calls a client tool. |
hasServerToolCall() | Checks whether the last message calls a server or unknown tool. |
routeAfterAgent() | Lower-level routing helper used by clientToolsRouter(). |
lastMessage() | Reads the last message from state. |
#Python public surface
threadplane.middleware.langgraph exports:
| API | Purpose |
|---|---|
bind_client_tools() | Binds server tools plus client-declared tool stubs onto a model. |
client_tool_specs() | Converts state catalog entries into OpenAI function-tool specs. |
client_tool_names() | Returns the set of client-declared tool names for a run. |
has_client_tool_call() | Checks whether the last message calls a client tool. |
has_server_tool_call() | Checks whether the last message calls a server or unknown tool. |
route_after_agent() | Routing helper for conditional edges. |
last_message() | Reads the last message from state. |
#When to use it
Use middleware when you own a LangGraph backend and want browser-declared tools from @threadplane/chat to participate in model tool calling without executing browser-only code on the server.
If your backend already speaks AG-UI, use @threadplane/ag-ui instead. If your frontend talks directly to LangGraph and does not need browser-executed tools, @threadplane/langgraph can run without this middleware.
#Next steps
- Quick Start - install and wire the LangGraph helper.
- LangGraph Client Tools - routing details and server-tool behavior.
- Python LangGraph Middleware - the Python package and snake_case helpers.
- Client Tool Helpers - generated API reference for the TypeScript helpers.