LangChain.js
|
This content was generated with the assistance of AI and should be verified against the official documentation of each tool and framework before being relied on in production. This section’s bibliography lists the reference material consulted while preparing these pages. |
This page was written against langchain 1.5.14, @langchain/core 1.2.13, @langchain/langgraph 1.4.18, @langchain/ollama 1.3.0,
@langchain/mcp-adapters 1.1.4, @langchain/react 1.2.0, @langchain/langgraph-checkpoint-postgres 1.0.5,
deepagents 1.14.1 and @langchain/langgraph-cli 1.5.1 on Node.js 24.20 with TypeScript 7.0.2, Zod 4.6.5 and React 19,
as verified on 2026-09-29. The Python pages of this sub-section were written against langchain 1.4.3 and langgraph
1.2.12; the JavaScript packages are versioned independently, so the numbers do not line up. The running example is
DocsAssistant, which answers questions over this site’s documentation with Ollama’s llama3.1:8b chat model and
nomic-embed-text embedding model over the pgvector store built in
Integrating with LangChain. Ollama was not available while the
snippets were checked, so every agent run below uses fakeModel, a scripted chat model that ships with langchain, and
a stub docs_search tool instead of the pgvector store; the tools, agents, middleware, graphs, checkpointers, the MCP
client and the Agent Server are the real packages, and no output comes from a live model. Every snippet was
type-checked with tsc --noEmit in strict mode against the packages above, and every snippet that has printed output
was executed with them.
LangChain.js is the same framework as the Python one, written in TypeScript: createAgent builds a LangGraph graph, tools
are described by a schema, middleware hooks into the agent loop, and a checkpointer plus thread_id gives an agent
memory. The concepts and the DocsAssistant scenario carry over unchanged, so this page does not re-teach them; it shows
the TypeScript spelling, then gathers everything that is genuinely different in
Differences from Python. It also covers the one thing that only exists on the JavaScript
side: the frontend SDKs, which connect a React, Vue, Svelte or Angular UI directly to an agent.
The package map
The JavaScript ecosystem is split into scoped npm packages, one per job. The diagram below shows how the packages relate: a solid arrow means "lists as a dependency", a dashed arrow means "lists as a peer dependency", which the application must install itself.
batteries-included harness"] LC["langchain
createAgent, middleware, initChatModel, fakeModel"] LG["@langchain/langgraph
StateGraph, checkpointers, interrupt"] CO["@langchain/core
messages, tools, base model classes"] P["partner packages
@langchain/ollama, @langchain/anthropic, @langchain/openai"] M["@langchain/mcp-adapters
MultiServerMCPClient"] PG["@langchain/langgraph-checkpoint-postgres
PostgresSaver"] R["@langchain/react, vue, svelte, angular
useStream, injectStream"] SDK["@langchain/langgraph-sdk
Agent Server client"] LS["langsmith
tracing client"] LC --> LG LC --> LS LG --> SDK R --> SDK LC -.-> CO LG -.-> CO P -.-> CO M -.-> CO M -.-> LG PG -.-> CO DA -.-> LC DA -.-> LG DA -.-> CO R -.-> CO
The edges come from each package’s own package.json. This script prints them for the packages installed in a project,
together with the engines field, so you can compare it with any version you have:
import { createRequire } from "node:module";
const require = createRequire(import.meta.url);
for (const name of ["langchain", "@langchain/core", "@langchain/langgraph", "@langchain/ollama", "@langchain/mcp-adapters", "@langchain/react", "@langchain/langgraph-checkpoint-postgres", "deepagents"]) {
const { version, engines } = require(`${name}/package.json`);
console.log(name.padEnd(42), version.padEnd(8), engines?.node ?? "");
}
const lc = await import("langchain");
const lg = await import("@langchain/langgraph");
console.log("langchain exports:", ["createAgent", "createMiddleware", "tool", "initChatModel", "fakeModel"].map((n) => `${n}=${typeof lc[n]}`).join(" "));
console.log("langgraph exports:", ["StateGraph", "StateSchema", "MessagesValue", "MemorySaver", "InMemoryStore", "Command", "interrupt"].map((n) => `${n}=${typeof lg[n]}`).join(" "));
Executed output:
langchain 1.5.14 >=20
@langchain/core 1.2.13 >=20
@langchain/langgraph 1.4.18 >=18
@langchain/ollama 1.3.0 >=20
@langchain/mcp-adapters 1.1.4 >=20.10.0
@langchain/react 1.2.0
@langchain/langgraph-checkpoint-postgres 1.0.5 >=18
deepagents 1.14.1
langchain exports: createAgent=function createMiddleware=function tool=function initChatModel=function fakeModel=function
langgraph exports: StateGraph=function StateSchema=function MessagesValue=object MemorySaver=function InMemoryStore=function Command=function interrupt=function
And the dependency edges, from deps.mjs (the same idea, restricted to the LangChain family):
import { createRequire } from "node:module";
const require = createRequire(import.meta.url);
const family = /^(langchain|deepagents|langsmith|@langchain\/)/;
for (const name of ["langchain", "@langchain/langgraph", "@langchain/ollama", "@langchain/mcp-adapters", "@langchain/react", "@langchain/langgraph-checkpoint-postgres", "deepagents"]) {
const pkg = require(`${name}/package.json`);
const show = (o = {}) => Object.keys(o).filter((d) => family.test(d)).sort();
console.log(name.padEnd(42), "requires", show(pkg.dependencies).join(" ") || "-", "| peer:", show(pkg.peerDependencies).join(" ") || "-");
}
Executed output:
langchain requires @langchain/langgraph @langchain/langgraph-checkpoint langsmith | peer: @langchain/core
@langchain/langgraph requires @langchain/langgraph-checkpoint @langchain/langgraph-sdk @langchain/protocol | peer: @langchain/core
@langchain/ollama requires - | peer: @langchain/core
@langchain/mcp-adapters requires - | peer: @langchain/core @langchain/langgraph
@langchain/react requires @langchain/langgraph-sdk | peer: @langchain/core
@langchain/langgraph-checkpoint-postgres requires - | peer: @langchain/core @langchain/langgraph-checkpoint
deepagents requires - | peer: @langchain/core @langchain/langgraph @langchain/langgraph-checkpoint @langchain/langgraph-sdk langchain langsmith
| Package | What it is | Install it when |
|---|---|---|
|
The agent framework: |
You are building an agent or an LLM application. This is the default entry point. |
|
The base abstractions: messages, tools, prompt templates, Runnables and the base chat-model class. It is a peer
dependency of |
Always, alongside |
|
The orchestration runtime: |
You build graphs yourself or import |
Partner packages ( |
One package per provider, each with a peer dependency on |
You talk to that provider: |
|
|
Conversations must survive a restart or be shared between processes. |
|
|
Your agent uses MCP servers. |
|
The frontend SDKs: |
A browser UI talks to an agent served by an Agent Server. |
|
The typed client for the Agent Server’s REST and streaming API, and the layer under the frontend SDKs. |
A Node.js or browser program calls a deployed agent without a UI framework. |
|
|
The task is long-running and multi-step; see Multi-Agent & Deep Agents. |
|
The |
You serve agents to the frontend SDKs or deploy them. |
Installing what DocsAssistant needs looks like this; the LangChain.js
installation page says Node.js 22 or later is required, while
the packages themselves declare engines of 20 or later (see the output above), so treat 22 as the supported floor:
npm install langchain @langchain/core @langchain/langgraph @langchain/ollama zod
npm install --save-dev typescript @types/node
The examples use ES modules and this tsconfig.json; strict matters, because most of the type inference this page
shows (tool arguments, structured responses, useStream state) disappears without it:
{"compilerOptions":{"target":"ES2022","module":"ESNext","moduleResolution":"Bundler","jsx":"react-jsx","strict":true,"noEmit":true,"skipLibCheck":true,"lib":["ES2022","DOM","DOM.Iterable"],"types":["node"]},"include":["*.ts","*.tsx"]}
The package split is described in the LangChain.js overview; the Python equivalent is on Ecosystem & Packages.
Models and tools
DocsAssistant’s models are two constructor calls. ChatOllama and OllamaEmbeddings read OLLAMA_BASE_URL from the
environment when no baseUrl is passed, and default to http://127.0.0.1:11434:
import { ChatOllama, OllamaEmbeddings } from "@langchain/ollama";
// Reads OLLAMA_BASE_URL from the environment when set; defaults to http://127.0.0.1:11434.
export const chatModel = new ChatOllama({ model: "llama3.1:8b", temperature: 0 });
export const embeddings = new OllamaEmbeddings({ model: "nomic-embed-text" });
See LangChain.js — Models for the provider packages and
the standard call options. Where a provider-prefixed string is more convenient, for example to switch model from
configuration, initChatModel resolves it to the provider’s class. It returns a configurable wrapper rather than the
class itself, and it needs the matching partner package installed:
import { initChatModel } from "langchain";
const m = await initChatModel("ollama:llama3.1:8b", { temperature: 0 });
console.log(m.constructor.name);
Executed output:
ConfigurableModel
Tools are created with the tool function from langchain, which takes the implementation and an options object whose
schema is a Zod object. The schema plays the part of the Python type hints and docstring: its field descriptions are
what the model reads. Here is DocsAssistant’s search tool over a small in-memory corpus standing in for the pgvector
store:
import { tool } from "langchain";
import * as z from "zod";
// A stub standing in for the pgvector-backed search built in "Integrating with LangChain".
const CORPUS = [
{ page: "modules/ROOT/pages/database/vector-rag/hnsw.adoc", section: "ef_search", content: "ef_search sets the size of the candidate list HNSW keeps while searching; raise it for recall, lower it for speed." },
{ page: "modules/ROOT/pages/database/vector-rag/hnsw.adoc", section: "m", content: "m is the number of bidirectional links per node in the HNSW graph." },
];
export const docsSearch = tool(
async ({ query, limit }) => {
const words = query.toLowerCase().split(/\s+/);
const hits = CORPUS.filter((d) => words.some((w) => d.content.toLowerCase().includes(w))).slice(0, limit);
return JSON.stringify({ results: hits.map((h) => ({ page: h.page, section: h.section, snippet: h.content.slice(0, 160) })) });
},
{
name: "docs_search",
description: "Search this site's documentation and return matching sections (page path, section, snippet).",
schema: z.object({
query: z.string().describe("A key phrase from the question, e.g. 'HNSW ef_search'"),
limit: z.number().int().min(1).max(10).default(3),
}),
},
);
The .default(3) on limit makes the argument optional for the model, and .min(1).max(10) becomes part of the JSON
Schema the model sees. Tool design advice (narrow tools, concise responses, actionable errors) is the same as in
Tool Design; tool itself is covered in the official
LangChain.js — Tools page, and the Python decorator on
Tools.
createAgent
createAgent takes a model (an instance or a "provider:model" string), tools, a systemPrompt, an optional
checkpointer, middleware, a responseFormat and a name. The helper below builds DocsAssistant from any chat model,
so the same code runs against Ollama in production and a scripted model in a test:
import { createAgent } from "langchain";
import type { BaseChatModel } from "@langchain/core/language_models/chat_models";
import type { BaseCheckpointSaver } from "@langchain/langgraph";
import { docsSearch } from "./tools";
export function buildDocsAssistant(model: BaseChatModel | string, checkpointer?: BaseCheckpointSaver) {
return createAgent({
model,
tools: [docsSearch],
systemPrompt:
"You are DocsAssistant. Answer only from documentation returned by docs_search, and cite the page path.",
checkpointer,
name: "docs_assistant",
});
}
The run below uses fakeModel, the chat model LangChain ships for tests: respond queues the next reply, and a reply
that carries tool_calls makes the agent call the tool. The first scripted turn asks for docs_search, the second
answers from its result. thread_id, passed under configurable, is what ties repeated calls to one conversation:
import { AIMessage, HumanMessage, fakeModel } from "langchain";
import { MemorySaver } from "@langchain/langgraph";
import { buildDocsAssistant } from "./agent";
// Two scripted turns: first a tool call, then the final answer. No network, no Ollama.
const model = fakeModel()
.respond(
new AIMessage({
content: "",
tool_calls: [{ name: "docs_search", args: { query: "HNSW ef_search" }, id: "call_1", type: "tool_call" }],
}),
)
.respond(new AIMessage("ef_search sets how many candidates HNSW keeps while searching (hnsw.adoc)."));
const agent = buildDocsAssistant(model, new MemorySaver());
const config = { configurable: { thread_id: "t-1" } }; // same thread_id = same conversation
const result = await agent.invoke({ messages: [new HumanMessage("What does ef_search do?")] }, config);
for (const m of result.messages) {
const calls = AIMessage.isInstance(m) ? m.tool_calls?.map((c) => `${c.name}(${JSON.stringify(c.args)})`) : [];
console.log(m.type.padEnd(5), calls?.length ? calls.join(", ") : String(m.content).slice(0, 72));
}
Executed output:
human What does ef_search do?
ai docs_search({"query":"HNSW ef_search"})
tool {"results":[{"page":"modules/ROOT/pages/database/vector-rag/hnsw.adoc","
ai ef_search sets how many candidates HNSW keeps while searching (hnsw.adoc
fakeModel also records what the model received (model.calls, model.callCount), so a test can assert on the
prompts it was sent; the Python testing approach is on Testing. The agent loop, its options and the
Python spelling are on Agents with create_agent; the official page is
LangChain.js — Agents, and the testing helpers are on
LangChain.js — Unit testing.
Structured output
responseFormat accepts a Zod schema, and toolStrategy or providerStrategy choose how the model is forced into it:
providerStrategy uses the provider’s native structured output, toolStrategy adds a hidden tool whose arguments are the
answer. The result carries a structuredResponse whose type is inferred from the schema. With toolStrategy the hidden
tool is named after the schema’s title, which is why the schema below sets one with .meta; the scripted model "calls" that
tool just as a real one would:
import { createAgent, fakeModel, toolStrategy, HumanMessage } from "langchain";
import * as z from "zod";
const DocAnswer = z
.object({
answer: z.string(),
pages: z.array(z.string()).describe("Repository paths of the pages the answer is based on"),
confidence: z.number().min(0).max(1),
})
.meta({ title: "DocAnswer" }); // becomes the name of the structured-output tool
// A fake model that "calls" the DocAnswer tool, as a real model would with toolStrategy.
const model = fakeModel().respondWithTools([
{
name: "DocAnswer",
args: {
answer: "ef_search sets the HNSW candidate list size.",
pages: ["modules/ROOT/pages/database/vector-rag/hnsw.adoc"],
confidence: 0.9,
},
},
]);
const agent = createAgent({ model, tools: [], responseFormat: toolStrategy(DocAnswer) });
const result = await agent.invoke({ messages: [new HumanMessage("What does ef_search do?")] });
console.log(result.structuredResponse);
console.log(result.structuredResponse.pages.length); // typed: structuredResponse is inferred from the schema
Executed output:
{
answer: 'ef_search sets the HNSW candidate list size.',
pages: [ 'modules/ROOT/pages/database/vector-rag/hnsw.adoc' ],
confidence: 0.9
}
1
Because structuredResponse is inferred, result.structuredResponse.pages.length type-checks and a misspelt field would
not. Strategy selection, error handling for invalid output and the Python ToolStrategy are on
Structured Output; the JavaScript reference is
LangChain.js — Structured output.
Middleware and approval gates
createMiddleware builds a middleware from hooks: node-style beforeModel and afterModel, and wrap-style
wrapModelCall and wrapToolCall that receive the request and a handler. The built-in humanInTheLoopMiddleware
pauses the run when the model asks for a tool listed in interruptOn, which needs a checkpointer. In the example DocsAssistant
may search freely, but opening a GitHub issue needs a human decision (in real code the tool would call the GitHub API with a
narrowly scoped token read from an environment variable, never one written into the source):
import { AIMessage, HumanMessage, createAgent, createMiddleware, fakeModel, humanInTheLoopMiddleware, tool } from "langchain";
import { Command, MemorySaver } from "@langchain/langgraph";
import * as z from "zod";
import { docsSearch } from "./tools";
// The destructive tool. A real one would call the GitHub API with a fine-grained token read from the environment.
const createGithubIssue = tool(
async ({ title, body }) => `Created issue #101: ${title} (${body.length} characters)`,
{
name: "create_github_issue",
description: "Open a GitHub issue on the documentation repository. Requires human approval.",
schema: z.object({ title: z.string().max(120), body: z.string() }),
},
);
// A custom middleware: log every model call and its number of messages.
const logModelCalls = createMiddleware({
name: "LogModelCalls",
wrapModelCall: (request, handler) => {
console.log(`model call with ${request.messages.length} messages`);
return handler(request);
},
});
const model = fakeModel()
.respond(new AIMessage({ content: "", tool_calls: [{ name: "create_github_issue", args: { title: "ef_search is undocumented", body: "Please document ef_search." }, id: "call_9", type: "tool_call" }] }))
.respond(new AIMessage("I opened the issue."));
const agent = createAgent({
model,
tools: [docsSearch, createGithubIssue],
middleware: [
logModelCalls,
humanInTheLoopMiddleware({
interruptOn: { create_github_issue: { allowedDecisions: ["approve", "reject"] }, docs_search: false },
}),
],
checkpointer: new MemorySaver(),
});
const config = { configurable: { thread_id: "t-hitl" } };
const paused = await agent.invoke({ messages: [new HumanMessage("File an issue: ef_search is undocumented")] }, config);
const request = paused.__interrupt__?.[0]?.value as { actionRequests: { name: string; args: unknown }[] };
console.log("paused for approval:", JSON.stringify(request.actionRequests[0]));
const done = await agent.invoke(new Command({ resume: { decisions: [{ type: "approve" }] } }), config);
console.log(done.messages.at(-2)?.content, "|", done.messages.at(-1)?.content);
Executed output:
model call with 1 messages
paused for approval: {"name":"create_github_issue","args":{"title":"ef_search is undocumented","body":"Please document ef_search."},"description":"Tool execution requires approval\n\nTool: create_github_issue\nArgs: {\n \"title\": \"ef_search is undocumented\",\n \"body\": \"Please document ef_search.\"\n}"}
model call with 3 messages
Created issue #101: ef_search is undocumented (26 characters) | I opened the issue.
The first invoke returns with interrupt set, carrying an actionRequests list; the second, given
new Command({ resume: { decisions: [{ type: "approve" }] } }) and the same thread_id, executes the tool. reject
skips the tool and reports the message to the model, and edit runs it with changed arguments. The model was called
once before the pause and once after, with the tool result in its context. The hooks are covered on
Middleware and the approval pattern on
Human-in-the-Loop and
Human-in-the-Loop & Time Travel. The official pages are
LangChain.js — Custom middleware and
LangChain.js — Human-in-the-loop.
LangGraph.js
createAgent returns a compiled LangGraph graph, so everything in LangGraph applies to it. Building a graph directly
is worth it when the control flow is yours rather than the model’s. In LangGraph.js, state is declared with
StateSchema: each key is a Zod schema or a special value type, and MessagesValue is the message list with the
appending reducer built in. Nodes are plain functions typed as GraphNode<typeof State> that return partial state
updates. The graph below retrieves documents, routes on whether it found any, answers when it did, and otherwise pauses with
interrupt to ask for approval before reporting a documentation gap:
import { AIMessage, HumanMessage, fakeModel } from "langchain";
import {
Command, END, GraphNode, InMemoryStore, MemorySaver, MessagesValue, START, StateGraph, StateSchema, interrupt,
} from "@langchain/langgraph";
import * as z from "zod";
import { docsSearch } from "./tools";
const State = new StateSchema({
messages: MessagesValue, // built-in reducer: appends messages
hits: z.array(z.string()).default(() => []),
issueUrl: z.string().optional(),
});
const model = fakeModel().respond(new AIMessage("ef_search controls how many candidates HNSW keeps while searching."));
const retrieve: GraphNode<typeof State> = async (state) => {
const question = String(state.messages.at(-1)?.content ?? "");
const raw = JSON.parse(await docsSearch.invoke({ query: question, limit: 3 })) as { results: { page: string }[] };
return { hits: raw.results.map((r) => r.page) };
};
const answer: GraphNode<typeof State> = async (state) => {
const reply = await model.invoke([...state.messages, new HumanMessage(`Sources: ${state.hits.join(", ")}`)]);
return { messages: [reply] };
};
const escalate: GraphNode<typeof State> = (state) => {
// Pauses the run; the value is surfaced to the caller under __interrupt__.
const approved = interrupt({ action: "create_github_issue", title: `Missing docs: ${state.messages.at(-1)?.content}` });
return approved ? { issueUrl: "https://github.com/example/docs/issues/101" } : {};
};
const graph = new StateGraph(State)
.addNode("retrieve", retrieve)
.addNode("answer", answer)
.addNode("escalate", escalate)
.addEdge(START, "retrieve")
.addConditionalEdges("retrieve", (state) => (state.hits.length > 0 ? "answer" : "escalate"), ["answer", "escalate"])
.addEdge("answer", END)
.addEdge("escalate", END)
.compile({ checkpointer: new MemorySaver(), store: new InMemoryStore() });
const found = await graph.invoke({ messages: [new HumanMessage("HNSW ef_search")] }, { configurable: { thread_id: "g1" } });
console.log(found.hits.length, "hit(s);", String(found.messages.at(-1)?.content).slice(0, 40));
const config = { configurable: { thread_id: "g2" } };
const paused = await graph.invoke({ messages: [new HumanMessage("kubernetes operators")] }, config);
if ("__interrupt__" in paused) {
console.log((paused.__interrupt__ as { value: unknown }[])[0].value);
}
const resumed = await graph.invoke(new Command({ resume: true }), config);
console.log(resumed.issueUrl);
Executed output:
2 hit(s); ef_search controls how many candidates H
{
action: 'create_github_issue',
title: 'Missing docs: kubernetes operators'
}
https://github.com/example/docs/issues/101
compile receives the checkpointer and a store, so the graph can be paused and resumed and can share long-term data across
threads. Resuming is graph.invoke(new Command({ resume: true }), config) with the same thread_id; the value given to
resume becomes the return value of interrupt. The "interrupt" in paused check is needed because the state type
of a StateSchema graph does not declare that key, so TypeScript would otherwise reject paused.interrupt. The graph
concepts are on LangGraph Fundamentals and the workflow patterns on
LangGraph Workflows; the official pages are
LangGraph.js — Overview,
LangGraph.js — Use the graph API and
LangGraph.js — Interrupts.
Persistence
MemorySaver (the JavaScript name for the in-memory checkpointer) loses everything on restart. For durable
conversations, PostgresSaver from @langchain/langgraph-checkpoint-postgres is a drop-in replacement; the connection
string comes from the environment, and setup() creates its tables on first use:
import { PostgresSaver } from "@langchain/langgraph-checkpoint-postgres";
import { HumanMessage } from "langchain";
import { buildDocsAssistant } from "./agent";
import { chatModel } from "./model";
const checkpointer = PostgresSaver.fromConnString(process.env.DATABASE_URL!);
await checkpointer.setup(); // creates the checkpoint tables on first use
const agent = buildDocsAssistant(chatModel, checkpointer);
await agent.invoke(
{ messages: [new HumanMessage("What does ef_search do?")] },
{ configurable: { thread_id: "user-42-session-7" } },
);
This snippet was type-checked but not executed, because no PostgreSQL server was available. Keep thread_id values short
(a user and session identifier, or a UUID); the persistence documentation warns that very long thread ids overflow the
column. Checkpointers, stores and time travel are covered in Python on
Short-Term Memory & Checkpointers; the JavaScript pages are
LangChain.js — Short-term memory and
LangGraph.js — Persistence.
Deep Agents and MCP tools
Deep Agents are on npm as deepagents. createDeepAgent takes the same model, tools and systemPrompt, and returns a
graph with planning, a virtual filesystem and subagent tools already wired in:
import { createDeepAgent } from "deepagents";
import { docsSearch } from "./tools";
import { chatModel } from "./model";
const agent = createDeepAgent({
model: chatModel,
tools: [docsSearch],
systemPrompt: "You are DocsAssistant's research mode. Plan, search, then write a short cited summary.",
});
console.log(typeof agent.invoke);
Executed output:
function
The snippet builds the agent (the constructor does not call the model) but does not run it. Model-driven behaviour is covered on Multi-Agent & Deep Agents; the official page is Deep Agents — Overview.
MCP servers plug in through @langchain/mcp-adapters. MultiServerMCPClient takes a named map of servers, each with a
transport ("stdio" for a local subprocess, "http" with a url for a remote server), and getTools() returns ordinary
LangChain tools. The client is stateless by default, and close() releases it. The server here is a real stdio MCP
server built with the official TypeScript SDK, so the tool call in the run goes over the protocol:
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import * as z from "zod";
const server = new McpServer({ name: "docs-server", version: "1.0.0" });
server.registerTool(
"docs_lookup",
{ description: "Look up a documentation page by topic.", inputSchema: { topic: z.string() } },
async ({ topic }) => ({ content: [{ type: "text", text: `modules/ROOT/pages/database/vector-rag/${topic}.adoc` }] }),
);
await server.connect(new StdioServerTransport());
import { MultiServerMCPClient } from "@langchain/mcp-adapters";
import { AIMessage, HumanMessage, createAgent, fakeModel } from "langchain";
import { MemorySaver } from "@langchain/langgraph";
const client = new MultiServerMCPClient({
docs: {
transport: "stdio", // a local subprocess; use "http" with a `url` for a remote server
command: "node",
args: ["./mcp-server.mjs"],
},
});
try {
const tools = await client.getTools(); // LangChain tools backed by the MCP server
console.log(tools.map((t) => t.name));
const model = fakeModel()
.respond(new AIMessage({ content: "", tool_calls: [{ name: "docs_lookup", args: { topic: "hnsw" }, id: "call_1", type: "tool_call" }] }))
.respond(new AIMessage("The HNSW page is modules/ROOT/pages/database/vector-rag/hnsw.adoc."));
const agent = createAgent({ model, tools, checkpointer: new MemorySaver() });
const result = await agent.invoke({ messages: [new HumanMessage("Where is the HNSW page?")] }, { configurable: { thread_id: "t-mcp" } });
console.log(result.messages.map((m) => m.type + ": " + JSON.stringify(m.content).slice(0, 70)));
} finally {
await client.close();
}
Executed output:
[ 'docs_lookup' ]
[
'human: "Where is the HNSW page?"',
'ai: ""',
'tool: "modules/ROOT/pages/database/vector-rag/hnsw.adoc"',
'ai: "The HNSW page is modules/ROOT/pages/database/vector-rag/hnsw.adoc."'
]
The FastMCP-based MCPAdapter described for Python on MCP Adapters was not found in
the JavaScript packages installed for this page, so the JavaScript client to use is MultiServerMCPClient. Writing MCP
servers and clients in TypeScript without LangChain is covered on
Building MCP Clients. The official page is
LangChain.js — Model Context Protocol (MCP).
Streaming
agent.streamEvents(input, { version: "v3" }) returns a run object with typed projections that are consumed
independently: messages yields one entry per model call whose text is an async iterable of deltas, toolCalls yields
the lifecycle of each tool execution, values yields state snapshots and output resolves to the final state. Because the
projections are independent, the loop over them runs concurrently:
import { AIMessage, HumanMessage, fakeModel } from "langchain";
import { buildDocsAssistant } from "./agent";
import { MemorySaver } from "@langchain/langgraph";
const model = fakeModel()
.respond(
new AIMessage({
content: "",
tool_calls: [{ name: "docs_search", args: { query: "HNSW ef_search" }, id: "call_1", type: "tool_call" }],
}),
)
.respond(new AIMessage("ef_search controls how many candidates HNSW keeps while searching."));
const agent = buildDocsAssistant(model, new MemorySaver());
const stream = await agent.streamEvents(
{ messages: [new HumanMessage("What does ef_search do?")] },
{ version: "v3", configurable: { thread_id: "t-stream" } },
);
await Promise.all([
(async () => {
for await (const message of stream.messages) {
let text = "";
for await (const delta of message.text) text += delta;
console.log(`[${message.node}] ${text}`);
}
})(),
(async () => {
for await (const call of stream.toolCalls) {
console.log(`tool ${call.name}(${JSON.stringify(call.input)})`);
console.log(" ->", String(await call.output).slice(0, 80));
}
})(),
]);
const finalState = await stream.output;
console.log(finalState.messages.length, "messages in the final state");
Executed output:
[model_request]
tool docs_search({"query":"HNSW ef_search"})
-> {"results":[{"page":"modules/ROOT/pages/database/vector-rag/hnsw.adoc","section"
[model_request] ef_search controls how many candidates HNSW keeps while searching.
4 messages in the final state
[model_request] is the graph node that made the model call, so the first entry is the tool-call turn (it has no text)
and the last is the answer. The scripted model returns each message in a single delta; a live model would emit many
short ones, and the same loop would print them as they arrive.
The lower-level stream method takes a streamMode. "updates" emits one entry after every node, keyed by node name, and
"messages" and "custom" are the other two modes for agents:
import { AIMessage, HumanMessage, fakeModel } from "langchain";
import { buildDocsAssistant } from "./agent";
import { MemorySaver } from "@langchain/langgraph";
const model = fakeModel()
.respond(new AIMessage({ content: "", tool_calls: [{ name: "docs_search", args: { query: "HNSW ef_search" }, id: "call_1", type: "tool_call" }] }))
.respond(new AIMessage("ef_search controls how many candidates HNSW keeps while searching."));
const agent = buildDocsAssistant(model, new MemorySaver());
const updates = await agent.stream(
{ messages: [new HumanMessage("What does ef_search do?")] },
{ streamMode: "updates", configurable: { thread_id: "t-modes" } },
);
for await (const chunk of updates) {
for (const [node, update] of Object.entries(chunk)) {
const last = (update as { messages: { type: string; content: unknown }[] }).messages.at(-1);
console.log(node, "->", last?.type, JSON.stringify(last?.content).slice(0, 60));
}
}
Executed output:
model_request -> ai ""
tools -> tool "{\"results\":[{\"page\":\"modules/ROOT/pages/database/vecto
model_request -> ai "ef_search controls how many candidates HNSW keeps while sea
The official pages are LangChain.js — Event streaming and LangChain.js — Streaming; the Python side, with a FastAPI Server-Sent Events endpoint, is on Streaming. What to show a user while this happens is on Agent UX.
Streaming to React with useStream
The frontend SDKs are the part of LangChain.js with no Python counterpart. On the server, createAgent produces a graph that an
Agent Server exposes over HTTP; in the browser, useStream from @langchain/react connects to it and gives the component
reactive messages, tool calls, interrupts, a loading flag and thread state. Vue and Svelte export the same name from
@langchain/vue and @langchain/svelte, and Angular has injectStream in @langchain/angular.
The server side
The graph is exported from a module, and langgraph.json maps a graph name to that export. The file below builds
DocsAssistant on the Ollama model without a checkpointer, because the Agent Server provides persistence:
import { chatModel } from "./model";
import { buildDocsAssistant } from "./agent";
// Exported so that langgraph.json can point at it; the Agent Server supplies persistence.
export const docsAssistant = buildDocsAssistant(chatModel);
{
"node_version": "24",
"graphs": { "docs_assistant": "./server.ts:docsAssistant" },
"env": ".env"
}
npx @langchain/langgraph-cli dev --no-browser starts the in-memory development server on port 2024; on first run it adds
.langgraph_api to .gitignore. To check the server side without a browser, the program below uses the SDK client. It was
run against a real langgraph dev process serving a copy of server.ts whose model is the scripted one (Ollama being
unavailable), and the output is what the server streamed back:
import { Client } from "@langchain/langgraph-sdk";
const client = new Client({ apiUrl: "http://localhost:2024" });
const thread = await client.threads.create();
const events = client.runs.stream(thread.thread_id, "docs_assistant", {
input: { messages: [{ role: "user", content: "What does ef_search do?" }] },
streamMode: "updates",
});
for await (const event of events) {
if (event.event === "updates") {
for (const [node, update] of Object.entries(event.data as Record<string, { messages: { type: string; content: string }[] }>)) {
const last = update.messages.at(-1);
console.log(node, "->", last?.type, JSON.stringify(last?.content).slice(0, 60));
}
} else console.log(event.event);
}
Executed output:
metadata
model_request -> ai ""
tools -> tool "{\"results\":[{\"page\":\"modules/ROOT/pages/database/vecto
model_request -> ai "ef_search controls how many candidates HNSW keeps while sea
The metadata line is the run’s start event. Then come the three node updates of one turn: the model asks for a tool, the
tool node returns its result, the model answers. This is exactly the stream a useStream hook consumes. Deploying the same
graph, and the production alternatives to the development server, are on
Deployment. The official pages are
LangGraph.js — Application structure and
LangGraph.js — Run a local server.
The React component
useStream takes the server URL (apiUrl) and the graph name (assistantId). Passing typeof docsAssistant as the type
parameter lets TypeScript infer the state schema from the compiled graph, so stream.messages and any custom state keys are
typed without a hand-written interface. The import is type-only, so none of the server code reaches the browser bundle:
import { useState } from "react";
import { useStream } from "@langchain/react";
import { AIMessage, HumanMessage } from "langchain";
import type { docsAssistant } from "./server"; // type-only import: no server code reaches the browser bundle
export function Chat() {
const [draft, setDraft] = useState("");
const stream = useStream<typeof docsAssistant>({
apiUrl: "http://localhost:2024", // `langgraph dev` locally; your Agent Server URL in production
assistantId: "docs_assistant", // a graph name from langgraph.json
});
return (
<form
onSubmit={(e) => {
e.preventDefault();
void stream.submit({ messages: [new HumanMessage(draft)] });
setDraft("");
}}
>
{stream.messages.map((msg) => (
<p key={msg.id}>
{HumanMessage.isInstance(msg) ? "You: " : AIMessage.isInstance(msg) ? "DocsAssistant: " : "tool: "}
{msg.text}
</p>
))}
{stream.error ? <p role="alert">Something went wrong. Try again.</p> : null}
<input value={draft} onChange={(e) => setDraft(e.target.value)} disabled={stream.isLoading} />
{stream.isLoading ? (
<button type="button" onClick={() => void stream.stop()}>Stop</button>
) : (
<button type="submit">Send</button>
)}
</form>
);
}
The component was type-checked with tsc --noEmit against @langchain/react 1.2.0; it was not rendered in a browser
here. stream.submit starts a run and streams into stream.messages, stream.isLoading disables the input while the run is
in flight, and stream.stop aborts it. The hook exposes more than this page uses: stream.toolCalls, stream.interrupt for
the approval flow shown above, and stream.values. The patterns are catalogued on the
LangChain.js — Frontend overview, including
approval flows, durable stream rejoin and time travel. The comparison with hand-written fetch and Server-Sent Events is on
Streaming, and the UX rules for what such a component should show are on
Agent UX.
Differences from Python
The mental model is identical; the differences are in packaging, naming, schemas and a few API shapes. Everything in this table was observed in the packages and pages described at the top of the page.
| Topic | Python | JavaScript / TypeScript |
|---|---|---|
Installation |
|
|
Versions |
|
|
Naming |
|
|
Tools |
|
|
State schemas |
|
|
Structured output |
Pydantic model or dataclass in |
Zod schema in |
In-memory checkpointer |
|
|
Postgres checkpointer |
|
|
Sync and async |
Both: |
Everything returns a promise or an async iterable; use |
Event streaming |
|
|
Interrupts |
|
|
Testing |
|
|
MCP |
|
|
Frontend |
Not applicable; the browser talks to your own HTTP endpoint. |
|
Runtime |
Python 3.10 or later (3.12 here). |
Node.js 22 or later per the docs (packages declare 20); |
When a Python page of this sub-section shows an API you cannot find in TypeScript, check the reference documentation before assuming it is missing: the two libraries are released separately, and a feature often lands in one first. For Java the equivalent library is a separate project with its own design; see LangChain4j.