The Model Context Protocol gives agents a standardized way to call tools on any server — local or remote. Learn how to connect Vibes agents to MCP servers using MCPStdioClient, MCPHttpClient, MCPToolset, and MCPManager.
When you want your agent to read files, query databases, call external APIs, or use specialized computation, you have two choices: write a tool() for each operation, or connect to an MCP server. MCP — the Model Context Protocol — is an open standard that lets any MCP-compatible server expose tools to any MCP-compatible client. Connect once and your agent gets access to every tool that server offers, without writing a single tool definition.The practical benefit is ecosystem leverage. There are MCP servers for the filesystem, GitHub, Postgres, Puppeteer, Slack, and hundreds more. Instead of writing a GitHub integration from scratch, you connect to a GitHub MCP server and your agent can search repos, read files, and open issues immediately.Vibes implements the MCP client side in four classes: MCPStdioClient, MCPHttpClient, MCPToolset, and MCPManager. Each has a single job and they compose cleanly.
The most common pattern is connecting to a local MCP server that runs as a subprocess. The filesystem MCP server is a good first example: you point it at a directory and your agent can list, read, and write files.
import { Agent, MCPStdioClient, MCPToolset } from "jsr:@vibesjs/sdk";import { anthropic } from "npm:@ai-sdk/anthropic";const model = anthropic("claude-sonnet-4-6");// (1) Describe the process to launch — MCPStdioClient handles spawning itconst client = new MCPStdioClient({ command: "npx", args: ["-y", "@modelcontextprotocol/server-filesystem", "/tmp/my-project"],});await client.connect(); // (2) Spawn the process and complete MCP handshake// (3) MCPToolset fetches the tool list from the server and caches itconst toolset = new MCPToolset(client);const agent = new Agent({ model, systemPrompt: "You are a file management assistant.", toolsets: [toolset], // (4) Pass the toolset, not individual tools});try { const result = await agent.run("List the files in the project and summarize what each one does."); console.log(result.output); // "The project contains 4 files: // - main.ts: Entry point, sets up the HTTP server // - agent.ts: Core agent configuration with search and summarize tools // - config.json: Environment configuration for development and production // - README.md: Setup instructions and API documentation"} finally { await client.disconnect(); // (5) Always disconnect to terminate the subprocess}
(1)MCPStdioConfig takes command, optional args, and optional env. The filesystem server accepts a path as its last argument — tools from that server will be scoped to that directory. (2)connect() spawns the subprocess and completes the MCP initialization handshake. Always await it before doing anything else. (3)MCPToolset discovers available tools lazily on first use and caches them for 60 seconds by default. You never call listTools() manually — the toolset handles that inside the agent’s turn loop. (4) Pass toolsets not tools. The toolset interface allows dynamic per-turn tool discovery with caching, which raw tools arrays don’t support. (5)disconnect() terminates the subprocess. Without this, the process keeps running after your code exits. Always call it in a finally block.
Always call connect() before using a client, and always call disconnect() in a finally block. Forgetting connect() causes all tool calls to throw immediately. Forgetting disconnect() leaks subprocess resources.
MCPHttpClient connects to an MCP server running over HTTP with Server-Sent Events. The API is identical to MCPStdioClient — you swap the config shape and the transport handles the rest.
import { Agent, MCPHttpClient, MCPToolset } from "jsr:@vibesjs/sdk";import { anthropic } from "npm:@ai-sdk/anthropic";const model = anthropic("claude-sonnet-4-6");// (1) Point at the HTTP endpointconst client = new MCPHttpClient({ url: "https://search-mcp.example.com/mcp", headers: { Authorization: `Bearer ${Deno.env.get("MCP_API_KEY")}`, // (2) },});await client.connect();const toolset = new MCPToolset(client);const agent = new Agent({ model, systemPrompt: "You are a research assistant with access to a search API.", toolsets: [toolset],});try { const result = await agent.run("Find recent news about TypeScript 5.5 features."); console.log(result.output); // "TypeScript 5.5 introduced several significant features including // inferred type predicates, which allow TypeScript to automatically // infer the return type of type guard functions..."} finally { await client.disconnect();}
(1)MCPHttpConfig takes a url and optional headers. (2) Authentication headers go here — Authorization, API keys, session tokens. Headers are sent with every request to the server.MCPHttpClient uses the MCP Streamable HTTP transport, which supports both standard HTTP/2 and SSE-based streaming responses.
MCPToolset is the bridge between a raw MCPClient and the Toolset interface. Its two configuration options control performance and agent context:
import { MCPToolset } from "jsr:@vibesjs/sdk";const toolset = new MCPToolset(client, { toolCacheTtlMs: 30_000, // (1) cache tool list for 30 seconds instead of 60 instructions: true, // (2) include the server's instruction text (default: true)});
(1) The server’s tool list doesn’t change often, so the toolset caches it. For servers whose tools change frequently (dynamic tool registration), lower the TTL or call toolset.invalidateCache() to force a re-fetch on the next turn. (2) Some MCP servers provide an instruction text during initialization — guidance the server author wrote for the AI agent consuming it. When instructions: true (the default), this text is available via toolset.getServerInstructions() and can be appended to your system prompt if desired.
// Manually include server instructions in the system promptconst toolset = new MCPToolset(client);await client.connect();const serverInstructions = toolset.getServerInstructions();const agent = new Agent({ model, systemPrompt: [ "You are a file management assistant.", serverInstructions, // includes server's own guidance, if any ].filter(Boolean).join("\n\n"), toolsets: [toolset],});
Most real applications need more than one MCP server. You might want the filesystem server for reading local files and a remote search server for web queries simultaneously. MCPManager handles this: register servers, call connect() once, and the manager connects all of them in parallel and merges their tool lists.
import { Agent, MCPHttpClient, MCPManager, MCPStdioClient } from "jsr:@vibesjs/sdk";import { anthropic } from "npm:@ai-sdk/anthropic";const model = anthropic("claude-sonnet-4-6");// (1) Create an empty manager — no constructor argumentsconst manager = new MCPManager();// (2) Register servers with addServer() — returns this for chainingmanager .addServer( new MCPStdioClient({ command: "npx", args: ["-y", "@modelcontextprotocol/server-filesystem", "/data"] }), { name: "filesystem" }, // (3) optional name for instruction aggregation ) .addServer( new MCPHttpClient({ url: "https://search-mcp.example.com/mcp", headers: { Authorization: `Bearer ${Deno.env.get("SEARCH_KEY")}` }, }), { name: "search" }, );// (4) Connect all servers in parallel with a single callawait manager.connect();// (5) Pass the manager directly as a toolset — it is a Toolsetconst agent = new Agent({ model, systemPrompt: "You have access to a local filesystem and a web search API. " + "Use them together to answer questions with both local context and current information.", toolsets: [manager],});try { const result = await agent.run( "Search for recent Deno documentation updates and then save a summary to /data/deno-notes.md", ); console.log(result.output); // "I found recent updates to the Deno documentation and saved a summary. // The key changes include: new Web API implementations, updated FFI docs, // and the Deno KV documentation was expanded significantly..."} finally { // (6) Disconnect all servers in parallel await manager.disconnect();}
(1)new MCPManager() takes no arguments. (2)addServer() returns this so you can chain calls. The second argument takes any MCPToolsetOptions plus an optional name. (3) Naming a server matters when servers provide instruction text — named servers prefix their instructions with [name] in the aggregated output. (4)manager.connect() calls connect() on every registered client in parallel. One call to connect everything. (5)MCPManager implements the Toolset interface directly. Pass it to toolsets the same way you’d pass a MCPToolset. (6)manager.disconnect() disconnects all servers in parallel. If any fail to disconnect, all failures are collected and thrown as a single AggregateError — so you see all problems, not just the first.
Do NOT use new MCPManager(client) — the constructor takes no arguments. Register clients with addServer() after construction.Do NOT call manager.connectAll() — the method is manager.connect().Do NOT wrap the manager in another MCPToolset — pass manager directly to toolsets. The manager is already a Toolset.
If two servers expose a tool with the same name, the last registered server wins. Control this by ordering addServer() calls from lowest to highest priority, or prefix server names when registering:
const manager = new MCPManager();manager.addServer(primaryClient); // lower prioritymanager.addServer(overrideClient); // higher priority — its tools win on collision
For production deployments, store your MCP server list in a JSON config file and load it at startup. createManagerFromConfig reads the file, creates clients, connects them, and returns a ready MCPManager:
import { Agent, createManagerFromConfig } from "jsr:@vibesjs/sdk";import { anthropic } from "npm:@ai-sdk/anthropic";const model = anthropic("claude-sonnet-4-6");// (1) One function call: load → create clients → connect → return managerconst manager = await createManagerFromConfig("./mcp.config.json");const agent = new Agent({ model, toolsets: [manager] });try { const result = await agent.run("What tools do you have available?"); console.log(result.output);} finally { await manager.disconnect();}
(1)createManagerFromConfig is a convenience wrapper around loadMCPConfig + createClientsFromConfig + MCPManager. The returned manager is already connected.
String values in the config support ${ENV_VAR} interpolation. Variables are read from Deno.env at load time. If a referenced variable is not set, loadMCPConfig throws immediately with a clear error message — your app fails fast at startup rather than silently at tool call time.
If you need more control over the setup process, use loadMCPConfig and createClientsFromConfig individually:
import { createClientsFromConfig, loadMCPConfig, MCPManager, MCPToolset,} from "jsr:@vibesjs/sdk";// Load and parse config fileconst configs = await loadMCPConfig("./mcp.config.json");// Create client instances (not yet connected)const clients = createClientsFromConfig(configs);// Set up manager manually — useful when you need custom MCPToolsetOptions per serverconst manager = new MCPManager();for (let i = 0; i < clients.length; i++) { manager.addServer(clients[i], { name: configs[i].name, toolCacheTtlMs: 5_000, // custom cache TTL for every server });}await manager.connect();