MCP

Use MCP to connect agents to external tools and data.

The Model Context Protocol (MCP) is an open standard for connecting agents to external tools, resources, and prompts. Better Agent includes an MCP client that speaks HTTP and SSE transports.

Two ways to use it:

  1. Convert remote MCP tools into Better Agent server tools and add them to an agent
  2. Use the raw client for direct access to tools, resources, and prompts

Add MCP Tools to an Agent

Use createMCPClient() to connect, listTools() to discover tools, and convertMCPTools() to turn them into Better Agent server tools.

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

const docsAgent = defineAgent({
  name: "docs",
  model: openai.model("gpt-4o"),
  tools: async () => {
    const client = await createMCPClient({
      transport: { type: "http", url: "https://mcp.context7.com/mcp" },
    });

    const listed = await client.listTools();

    return convertMCPTools(client, listed.tools, { prefix: "docs" });
  },
});

convertMCPTools takes the MCP client, the tool list from listTools(), and an optional prefix. Each MCP tool becomes a server tool that proxies execution back to the MCP server.

Use prefix to namespace tool names and avoid collisions when combining tools from multiple sources.

Reuse a Client with lazyTools

The example above creates a new client on every run. Use lazyTools() to cache a client across runs and control its lifecycle manually.

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

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

  const listed = await client.listTools();

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

Then pass it directly to an agent.

server.ts
const docsAgent = defineAgent({
  name: "docs",
  model: openai.model("gpt-4o"),
  tools: mcpTools,
});

lazyTools() caches the first successful load, shares concurrent resolves, and retries after failures. Call await mcpTools.dispose() when your app shuts down.

Per-run cleanup does not dispose the cached client. You must call dispose() yourself when the app exits.

Transport Configuration

The client supports two transport types: http and sse.

HTTP

transport: {
  type: "http",
  url: "https://mcp.example.com/mcp",
  headers: { "X-API-Key": "sk-abc123" },
  redirect: "follow",
  sessionId: "session-xyz",
}

SSE

transport: {
  type: "sse",
  url: "https://mcp.example.com/sse",
  headers: { "X-API-Key": "sk-abc123" },
  redirect: "follow",
}

Both transports accept the same base fields:

FieldTypeDescription
urlstringMCP server URL. Required.
headersRecord<string, string>Additional HTTP headers sent with every request.
redirect"follow" | "error"How to handle HTTP redirects.
sessionIdstringSession id for resumable connections (HTTP only).
advancedobjectReconnect options for the inbound SSE channel.

Reconnect Options

The inbound SSE channel reconnects automatically on disconnect. Configure the backoff behavior with advanced.

transport: {
  type: "http",
  url: "https://mcp.example.com/mcp",
  advanced: {
    reconnectInitialDelayMs: 1000,
    reconnectMaxDelayMs: 30_000,
    reconnectBackoffFactor: 1.5,
    reconnectMaxRetries: 2,
  },
}

Custom Transport

Pass any object that implements the MCPTransport interface (start, send, close) directly as the transport field.

const client = await createMCPClient({
  transport: myCustomTransport,
});

Client Configuration

const client = await createMCPClient({
  transport: { type: "http", url: "https://mcp.example.com/mcp" },
  name: "my-app",
  version: "1.0.0",
  capabilities: {
    roots: { listChanged: true },
  },
  onUncaughtError: (error) => {
    console.error("MCP error:", error.message);
  },
});
FieldTypeDescription
transportMCPTransportConfig | MCPTransportTransport config or custom transport instance. Required.
namestringClient name sent during the MCP handshake. Defaults to "better-agent-mcp-client".
versionstringClient version sent during the handshake. Defaults to "1.0.0".
capabilitiesClientCapabilitiesClient capabilities to advertise to the server.
onUncaughtError(error: MCPClientError) => voidCallback for errors that occur outside a request, such as SSE channel failures.

During initialization, the client performs the MCP handshake automatically. It sends initialize with the latest supported protocol version, validates the server's version, and sends notifications/initialized.

Raw Client API

Use the client directly when you need access to resources, prompts, or fine-grained tool control.

Tools

const client = await createMCPClient({
  transport: { type: "http", url: "https://mcp.example.com/mcp" },
});

const { tools } = await client.listTools();
const result = await client.callTool({
  name: "search",
  arguments: { query: "agent frameworks" },
});

Resources

const { resources } = await client.listResources();
const doc = await client.readResource({ uri: resources[0].uri });
const { resourceTemplates } = await client.listResourceTemplates();

Prompts

const { prompts } = await client.listPrompts();
const result = await client.getPrompt({
  name: "summarize",
  arguments: { topic: "climate change" },
});

Request Options

Every client method accepts RequestOptions for cancellation and timeouts.

const controller = new AbortController();

const { tools } = await client.listTools({
  options: {
    signal: controller.signal,
    timeout: 5000,
  },
});
FieldTypeDescription
signalAbortSignalAbort signal for cancellation.
timeoutnumberTimeout in milliseconds for a single request.

Close

Always close the client when you're done.

await client.close();

Multiple MCP Servers

Combine tools from multiple servers with lazyTools() so the clients are reused across runs and cleaned up explicitly.

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

export const multiMCPTools = lazyTools(async () => {
  const docsClient = await createMCPClient({
    transport: { type: "http", url: "https://docs.example.com/mcp" },
  });
  const analyticsClient = await createMCPClient({
    transport: { type: "http", url: "https://analytics.example.com/mcp" },
  });

  const docsListed = await docsClient.listTools();
  const analyticsListed = await analyticsClient.listTools();

  return {
    tools: [
      ...convertMCPTools(docsClient, docsListed.tools, { prefix: "docs" }),
      ...convertMCPTools(analyticsClient, analyticsListed.tools, { prefix: "analytics" }),
    ],
    dispose: async () => {
      await Promise.all([docsClient.close(), analyticsClient.close()]);
    },
  };
});
server.ts
const multiAgent = defineAgent({
  name: "multi",
  model: openai.model("gpt-4o"),
  tools: multiMCPTools,
});

Error Handling

MCP operations throw MCPClientError on failure.

import { MCPClientError } from "@better-agent/core/mcp";

try {
  const client = await createMCPClient({
    transport: { type: "http", url: "https://mcp.example.com/mcp" },
  });

  const { tools } = await client.listTools();
} catch (error) {
  if (error instanceof MCPClientError) {
    console.error("MCP failed:", error.message);
  }
}

Common failure modes:

  • Server unavailable: transport cannot connect or server returns an error status
  • Capability not supported: calling listTools() when the server doesn't advertise tools capability
  • Protocol version mismatch: server uses an unsupported MCP protocol version
  • Timeout: request exceeds the configured timeout
  • Connection closed: server or network dropped the connection mid-request