Back to Cookbook

Build an MCP Chat

Build a chat app that uses tools from an MCP server.

Build a chat app that can call tools exposed by an MCP server.

Connect to the MCP server

Use lazyTools() so the MCP client is reused and cleaned up correctly.

mcp.ts
import { convertMCPTools, createMCPClient } from "@better-agent/core/mcp";
import { lazyTools } from "@better-agent/core";

export const mcpTools = lazyTools(async () => {
  const client = await createMCPClient({
    transport: {
      type: "http",
      url: "https://mcp.context7.com/mcp",
    },
  });

  const listed = await client.listTools();

  return {
    tools: convertMCPTools(client, listed.tools, { prefix: "mcp" }),
    dispose: async () => await client.close?.(),
  };
});

Define the agent

MCP tools plug into the agent like any other tool source.

server.ts
import { betterAgent, defineAgent } from "@better-agent/core";
import { createOpenAI } from "@better-agent/providers/openai";
import { mcpTools } from "./mcp";

const openai = createOpenAI({
  apiKey: process.env.OPENAI_API_KEY,
});

const assistant = defineAgent({
  name: "assistant",
  model: openai.text("gpt-5-mini"),
  instruction: `
    Use MCP tools when they help answer the user's question.
    Prefer tool results over guessing.
  `,
  tools: mcpTools,
});

const app = betterAgent({
  agents: [assistant],
  baseURL: "/api",
  secret: "dev-secret",
});

export default app;

Create the client

client.ts
import { createClient } from "@better-agent/client";
import type app from "./server";

export const client = createClient<typeof app>({
  baseURL: "/api",
  secret: "dev-secret",
});

Build the chat

Chat.tsx
import { useState } from "react";
import { useAgent } from "@better-agent/client/react";
import { client } from "./client";

export function Chat() {
  const [input, setInput] = useState("");
  const { messages, status, sendMessage } = useAgent(client, {
    agent: "assistant",
  });

  const getText = (message: (typeof messages)[number]) =>
    message.parts.map((part) => (part.type === "text" ? part.text : "")).join("");

  return (
    <div>
      <form
        onSubmit={async (event) => {
          event.preventDefault();
          if (!input.trim()) return;
          await sendMessage({ input });
          setInput("");
        }}
      >
        <input value={input} onChange={(event) => setInput(event.target.value)} />
        <button type="submit">Send</button>
      </form>

      <p>Status: {status}</p>

      <ul>
        {messages.map((message) => (
          <li key={message.localId}>
            {message.role}: {getText(message)}
          </li>
        ))}
      </ul>
    </div>
  );
}

Use lazyTools() for MCP so the tool source can reuse the client connection and dispose it cleanly when needed.