Building AI Agents: From Streaming Chatbots to LangGraph with MCP
A hands-on Next.js project that walks you through building AI agents step by step β from a simple streaming chatbot all the way to a LangGraph-powered agent with database tools served over MCP.
Complete Tutorial Code
Follow along with the complete source code for this AI agent tutorial. Includes five progressive tabs from a basic streaming chatbot to a LangGraph-powered agent with MCP tools.
View on GitHubTable of Contents
Introduction
Building AI agents has never been more accessible, yet the landscape of tools and frameworks can be overwhelming. This tutorial takes a progressive approach β each tab in the UI corresponds to a more advanced API route, letting you see exactly how the complexity grows and what capabilities each layer adds.
You'll start with a simple streaming chatbot backed by SQLite memory, then add inline tool-calling, move those tools to an MCP server, and finally explore LangChain and LangGraph as a powerful alternative to the Vercel AI SDK.
The Database
Before diving into agents, it helps to understand the data they work with.
File: lib/db.ts
The app uses a single in-memory SQLite database (via better-sqlite3) that is created once when the Node.js process starts. It contains three business tables and three internal tables used by the agents.
Business Tables
| Table | Columns |
|---|---|
inventory | id, product_name, category, unit_price, stock_quantity, supplier, created_at |
customers | id, first_name, last_name, email, city, joined_date |
sales | id, inventory_id, customer_id, quantity_sold, sale_price, sale_date |
Internal Tables
| Table | Purpose |
|---|---|
chat_sessions | Tracks active chat sessions by UUID |
chat_messages | Stores the full conversation history per session |
tool_calls | Logs every SQL tool call an agent makes |
The View Database tab (app/api/database/route.ts) simply reads all three business tables and returns them as JSON so you can see the live state of the database.
π€ Tab 1 β Basic AI Agent
API Route: app/api/chat/route.ts
Key library: Vercel AI SDK (ai, @ai-sdk/openai)
This is the simplest possible AI agent: a streaming chatbot with persistent memory.
User message β load history from SQLite β streamText(GPT-4o-mini) β stream response β save to SQLiteHow it works
- 1. The client sends
{ prompt, sessionId }toPOST /api/chat. - 2.
initChatSession()(lib/chat-session.ts) ensures the session exists in the DB, saves the user message, and returns the full conversation history as an array of{ role, content }messages. - 3.
streamText()from the AI SDK calls GPT-4o-mini with the history as context, producing a streaming response. - 4.
onFinishsaves the assistant's reply back to the DB viasaveAssistantMessage(). - 5. The response is returned as a UI message stream (
result.toUIMessageStreamResponse()).
Key concept: conversation memory
Memory is implemented manually β every message is written to chat_messages and the entire history is re-sent to the model on each turn. This is the simplest form of stateful chat.
// lib/chat-session.ts
export function initChatSession(db, sessionId, prompt) {
// 1. Create session if new
// 2. Save user message
// 3. Return full history for context window
}π οΈ Tab 2 β AI Agent with Tools
API Route: app/api/chat-with-tools/route.ts
Key library: Vercel AI SDK β tool(), streamText() with stopWhen
This agent can call functions (tools) to query or modify the database. This is the foundation of agentic behavior: the model decides when and how to use tools.
User message β LLM decides to call a tool β tool executes SQL β result fed back to LLM β final answerHow it works
Three tools are registered β one per database table:
tools: {
inventory: tool({
description: TOOL_DESCRIPTIONS.inventory,
inputSchema: sqlInputSchema, // { sql: string, params?: [] }
execute: makeSqlExecute("inventory", sessionId),
}),
customers: tool({ ... }),
sales: tool({ ... }),
}The model receives a system prompt describing the database schema and can call any tool by generating a structured JSON payload matching sqlInputSchema:
// lib/sql-tools.ts
export const sqlInputSchema = z.object({
sql: z.string(), // e.g. "SELECT * FROM inventory WHERE category = ?"
params: z.array(...), // e.g. ["Electronics"]
});makeSqlExecute() validates the SQL (only SELECT/INSERT/UPDATE allowed), runs it against the SQLite DB, logs the call to the tool_calls table, and returns the result.
stopWhen: stepCountIs(10) prevents infinite tool-call loops by capping the agent at 10 reasoning steps.
Key concept: tool-calling loop
The AI SDK handles the agentic loop automatically:
- 1. LLM generates a tool call
- 2. SDK executes the tool
- 3. Result is appended to the message history
- 4. LLM is called again with the updated context
- 5. Repeat until the LLM produces a plain text response (no tool calls)
β‘ Tab 3 β AI Agent with MCP
API Route: app/api/chat-with-mcp/route.ts
MCP Server: app/api/mcp/[transport]/route.ts
Key library: @ai-sdk/mcp, mcp-handler, @modelcontextprotocol/sdk
This agent uses the same tools as Tab 2, but they are now served over the Model Context Protocol (MCP) β a standard protocol for exposing tools to AI models over HTTP.
Architecture
Client β POST /api/chat-with-mcp
β
createMCPClient connects to /api/mcp/mcp (HTTP transport)
β
tools = await mcpClient.tools() β discovers tools dynamically
β
streamText(GPT-4o-mini, { tools })
β
Tool calls are routed back through the MCP client to /api/mcp/mcpThe MCP Server (app/api/mcp/[transport]/route.ts)
createMcpHandler((server) => {
server.registerTool("inventory", { description, inputSchema }, makeMcpSqlExecute("inventory", sessionId));
server.registerTool("customers", { ... }, makeMcpSqlExecute("customers", sessionId));
server.registerTool("sales", { ... }, makeMcpSqlExecute("sales", sessionId));
});The [transport] dynamic segment means the same handler supports both GET /api/mcp/mcp (SSE) and POST /api/mcp/mcp (HTTP streaming), as required by the MCP spec. The sessionId is passed via the x-session-id request header so tool calls can be attributed to the correct session.
Key concept: MCP vs inline tools
Inline Tools (Tab 2)
- β’ Defined in the same file as the route
- β’ Tightly coupled to the agent
- β’ No network overhead
- β’ Simpler to set up
MCP Tools (Tab 3)
- β’ Defined in a separate server
- β’ Discoverable by any MCP-compatible client
- β’ Communicates over HTTP
- β’ Reusable across multiple agents/apps
MCP is useful when you want to share tools across multiple agents, or when tools are maintained by a different team or service.
π¦ Tab 4 β Basic LangChain Agent
API Route: app/api/chat-with-langchain/route.ts
Key libraries: @langchain/openai, @langchain/langgraph, @langchain/langgraph-checkpoint-sqlite
This tab introduces LangChain and LangGraph as an alternative to the Vercel AI SDK. It's a simple chatbot (no tools) that demonstrates LangGraph's built-in checkpointing for conversation memory.
How it works
// Reuse the existing SQLite connection for checkpointing
const checkpointer = new SqliteSaver(getDb());
const model = new ChatOpenAI({ model: "gpt-4o-mini", streaming: true });
const agent = createAgent({
model,
tools: [], // no tools β pure chat
systemPrompt: "...",
checkpointer, // LangGraph persists state automatically
});Each request streams events from the agent:
const eventStream = await agent.streamEvents(
{ messages: [new HumanMessage(prompt)] },
{ configurable: { thread_id: sessionId }, version: "v2" }
);
for await (const event of eventStream) {
if (event.event === "on_chat_model_stream") {
// stream token to client
}
}Key concept: LangGraph checkpointing
Instead of manually saving messages to the DB (as in Tab 1), LangGraph's SqliteSaver checkpointer automatically persists the full graph state (including message history) keyed by thread_id. On the next request with the same thread_id, LangGraph restores the state and continues the conversation.
πΈοΈ Tab 5 β LangGraph with Tools
API Route: app/api/chat-with-langgraph/route.ts
Key libraries: @langchain/langgraph, @langchain/core/tools, @langchain/langgraph-checkpoint-sqlite
This is the most advanced tab. It combines LangGraph's StateGraph with database tools, giving you full control over the agent's reasoning loop as an explicit graph.
The Graph
START β llmCall β (conditional) β toolNode β llmCall β ... β ENDThe graph is built manually:
new StateGraph(MessagesState)
.addNode("llmCall", llmCall) // calls the LLM
.addNode("toolNode", toolNode) // executes tool calls
.addEdge(START, "llmCall")
.addConditionalEdges("llmCall", shouldContinue, {
toolNode: "toolNode", // if LLM made tool calls β run tools
[END]: END, // if LLM gave a final answer β stop
})
.addEdge("toolNode", "llmCall") // after tools run β back to LLM
.compile({ checkpointer });The Nodes
llmCall node β invokes the model with the current message history:
const llmCall = async (state) => {
const response = await modelWithTools.invoke([
new SystemMessage("..."),
...state.messages,
]);
return { messages: [response] };
};toolNode node β executes any tool calls the LLM requested:
const toolNode = async (state) => {
const lastMessage = state.messages.at(-1); // AIMessage with tool_calls
const results = [];
for (const toolCall of lastMessage.tool_calls) {
const tool = toolsByName[toolCall.name];
results.push(await tool.invoke(toolCall));
}
return { messages: results }; // ToolMessages appended to state
};shouldContinue function β the conditional edge logic:
const shouldContinue = (state) => {
const last = state.messages.at(-1);
if (last.tool_calls?.length) return "toolNode"; // keep going
return END; // done
};Key concept: explicit graph vs. automatic loop
Vercel AI SDK (Tab 2)
- β’ Agentic loop is handled by the SDK
- β’ Less code, less control
- β’ Hard to add custom logic between steps
- β’ Good for standard tool-calling patterns
LangGraph StateGraph (Tab 5)
- β’ You define the loop as a graph
- β’ More code, full control
- β’ Easy to add nodes (e.g., validation, logging)
- β’ Good for complex multi-step workflows
LangGraph shines when you need branching logic, parallel tool execution, human-in-the-loop steps, or other non-linear agent behaviors.
Shared Utilities
lib/sql-tools.ts
Shared across Tabs 2, 3, and 5. Provides:
- β’
sqlInputSchemaβ Zod schema for tool inputs ({ sql, params }) - β’
makeSqlExecute(tableName, sessionId)β Returns an async function that validates and runs SQL, then logs the call totool_calls - β’
makeMcpSqlExecute(tableName, sessionId)β Same as above but wraps the result in MCP's{ content: [{ type: "text", text }] }format - β’
TOOL_DESCRIPTIONSβ Shared natural-language descriptions of each table's tool, used in all three agent variants
lib/chat-session.ts
Used by Tabs 1 and 2 (Vercel AI SDK routes). Provides:
- β’
initChatSession(db, sessionId, prompt)β Creates the session if new, saves the user message, returns full history - β’
saveAssistantMessage(db, sessionId, text)β Saves the assistant reply and updatesupdated_at
// lib/sql-tools.ts β shared across Tabs 2, 3, 5
export const sqlInputSchema = z.object({
sql: z.string(),
params: z.array(z.union([z.string(), z.number(), z.null()])).optional(),
});
export function makeSqlExecute(tableName: string, sessionId: string) {
return async ({ sql, params }: z.infer<typeof sqlInputSchema>) => {
// validate, run, log, return
};
}
// lib/chat-session.ts β used by Tabs 1 and 2
export function initChatSession(db, sessionId, prompt) { ... }
export function saveAssistantMessage(db, sessionId, text) { ... }Architecture Summary
Each tab in the UI corresponds to a progressively more advanced API route. This guide focuses on the API routes and the concepts behind them.
Tab 1: Basic AI Agent
POST /api/chat
βββ streamText (AI SDK) + manual SQLite memory
Tab 2: AI Agent with Tools
POST /api/chat-with-tools
βββ streamText (AI SDK) + inline tool() definitions + SQLite memory
Tab 3: AI Agent with MCP
POST /api/chat-with-mcp
βββ streamText (AI SDK) + MCP client β GET/POST /api/mcp/mcp
βββ createMcpHandler (mcp-handler)
Tab 4: Basic LangChain Agent
POST /api/chat-with-langchain
βββ LangChain createAgent + SqliteSaver checkpointer (no tools)
Tab 5: LangGraph with Tools
POST /api/chat-with-langgraph
βββ LangGraph StateGraph (llmCall β toolNode) + SqliteSaver checkpointerKey Dependencies
The tutorial uses a modern TypeScript / Next.js stack with carefully selected dependencies:
| Package | Purpose |
|---|---|
ai | Vercel AI SDK core β streamText, tool |
@ai-sdk/openai | OpenAI provider for the AI SDK |
@ai-sdk/react | React hooks (useCompletion, useChat) |
@ai-sdk/mcp | MCP client for the AI SDK |
mcp-handler | MCP server handler for Next.js API routes |
@modelcontextprotocol/sdk | Official MCP TypeScript SDK |
@langchain/openai | LangChain OpenAI integration |
@langchain/langgraph | LangGraph StateGraph and agent primitives |
@langchain/langgraph-checkpoint-sqlite | SQLite checkpointer for LangGraph |
better-sqlite3 | Synchronous SQLite driver for Node.js |
zod | Schema validation for tool inputs |
Conclusion
This tutorial demonstrates that there is no single "right" way to build AI agents. The Vercel AI SDK offers a streamlined, low-boilerplate path for standard tool-calling patterns, while LangGraph gives you explicit control over the agent's reasoning loop β ideal for complex, branching workflows.
MCP sits orthogonally to both: it's a protocol for making tools reusable and discoverable, regardless of which agent framework you choose. Together, these five tabs give you a complete picture of the modern AI agent stack.
About the Author
Wayne Cheng is the founder and AI app developer at Audoir, LLC. Prior to founding Audoir, he worked as a hardware design engineer for Silicon Valley startups and an audio engineer for creative organizations. He holds an MSEE from UC Davis and a Music Technology degree from Foothill College.
Further Exploration
Explore the complete tutorial repository and experiment with extending the examples. Consider adding new tools, implementing human-in-the-loop steps, or connecting to external APIs to deepen your understanding of AI agent architectures.
For more AI-powered development tools and tutorials, visit Audoir .