Technology

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.

20 min read
Published

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 GitHub

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

TableColumns
inventoryid, product_name, category, unit_price, stock_quantity, supplier, created_at
customersid, first_name, last_name, email, city, joined_date
salesid, inventory_id, customer_id, quantity_sold, sale_price, sale_date

Internal Tables

TablePurpose
chat_sessionsTracks active chat sessions by UUID
chat_messagesStores the full conversation history per session
tool_callsLogs 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 SQLite

How it works

  1. 1. The client sends { prompt, sessionId } to POST /api/chat.
  2. 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. 3. streamText() from the AI SDK calls GPT-4o-mini with the history as context, producing a streaming response.
  4. 4. onFinish saves the assistant's reply back to the DB via saveAssistantMessage().
  5. 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 answer

How 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. 1. LLM generates a tool call
  2. 2. SDK executes the tool
  3. 3. Result is appended to the message history
  4. 4. LLM is called again with the updated context
  5. 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/mcp

The 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 β†’ ... β†’ END

The 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 to tool_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 updates updated_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 checkpointer

Key Dependencies

The tutorial uses a modern TypeScript / Next.js stack with carefully selected dependencies:

PackagePurpose
aiVercel AI SDK core β€” streamText, tool
@ai-sdk/openaiOpenAI provider for the AI SDK
@ai-sdk/reactReact hooks (useCompletion, useChat)
@ai-sdk/mcpMCP client for the AI SDK
mcp-handlerMCP server handler for Next.js API routes
@modelcontextprotocol/sdkOfficial MCP TypeScript SDK
@langchain/openaiLangChain OpenAI integration
@langchain/langgraphLangGraph StateGraph and agent primitives
@langchain/langgraph-checkpoint-sqliteSQLite checkpointer for LangGraph
better-sqlite3Synchronous SQLite driver for Node.js
zodSchema 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 .