Skip to main content
Rowboat supports the Model Context Protocol (MCP), enabling connections to external tools, APIs, and data sources.

Overview

MCP allows you to:
  • Connect to external APIs and services
  • Execute tools via MCP servers
  • Extend Rowboat’s capabilities with custom integrations
  • Use both local (stdio) and remote (HTTP/SSE) MCP servers
MCP is an open standard for connecting AI assistants to external data sources and tools.

Configuration

Config File Location

const configPath = path.join(WorkDir, 'config', 'mcp.json');
Location: ~/.rowboat/config/mcp.json

Config Format

{
  "mcpServers": {
    "exa": {
      "type": "http",
      "url": "https://mcp.exa.ai/mcp"
    },
    "filesystem": {
      "type": "stdio",
      "command": "npx",
      "args": ["-y", "@modelcontextprotocol/server-filesystem", "/path/to/allowed/files"],
      "env": {
        "NODE_ENV": "production"
      }
    }
  }
}

Server Types

HTTP/SSE Servers

For remote MCP servers over HTTP or Server-Sent Events:
if (config.type === "http") {
  try {
    transport = new StreamableHTTPClientTransport(new URL(config.url));
  } catch {
    // Fallback to SSE if HTTP fails
    transport = new SSEClientTransport(new URL(config.url));
  }
}
{
  "exa": {
    "type": "http",
    "url": "https://mcp.exa.ai/mcp"
  }
}

Stdio Servers

For local MCP servers that run as child processes:
if (config.type === "stdio") {
  transport = new StdioClientTransport({
    command: config.command,
    args: config.args,
    env: config.env,
  });
}

Example: Local Filesystem Server

{
  "filesystem": {
    "type": "stdio",
    "command": "npx",
    "args": [
      "-y",
      "@modelcontextprotocol/server-filesystem",
      "/Users/username/Documents"
    ]
  }
}
Stdio servers spawn a new process. Make sure the command is installed and accessible in your PATH.

Client Management

Connection States

type ConnectionState = "connected" | "disconnected" | "error";

interface McpState {
  state: ConnectionState;
  client: Client | null;
  error: string | null;
}

Lazy Connection

Clients are created on first use:
async function getClient(serverName: string): Promise<Client> {
  if (clients[serverName] && clients[serverName].state === "connected") {
    return clients[serverName].client!; // Reuse existing
  }
  
  const config = mcpServers[serverName];
  if (!config) {
    throw new Error(`MCP server ${serverName} not found`);
  }
  
  // Create transport
  const transport = createTransport(config);
  
  // Create client
  const client = new Client({
    name: 'rowboatx',
    version: '1.0.0',
  });
  
  await client.connect(transport);
  
  clients[serverName] = {
    state: "connected",
    client,
    error: null,
  };
  
  return client;
}

Using MCP Tools

List Available Tools

export async function listTools(
  serverName: string,
  cursor?: string
): Promise<{ tools: Tool[]; nextCursor?: string }> {
  const client = await getClient(serverName);
  const { tools, nextCursor } = await client.listTools({ cursor });
  return { tools, nextCursor };
}

Execute a Tool

export async function executeTool(
  serverName: string,
  toolName: string,
  input: Record<string, unknown>
): Promise<unknown> {
  const client = await getClient(serverName);
  const result = await client.callTool({
    name: toolName,
    arguments: input,
  });
  return result;
}
const result = await executeTool(
  'exa',
  'exa_search',
  {
    query: 'latest AI news',
    numResults: 10,
  }
);

List MCP Servers

export async function listServers(): Promise<{
  mcpServers: Record<string, {
    config: McpServerDefinition;
    state: ConnectionState;
    error: string | null;
  }>;
}> {
  const { mcpServers } = await repo.getConfig();
  const result = { mcpServers: {} };
  
  for (const [serverName, config] of Object.entries(mcpServers)) {
    const state = clients[serverName];
    result.mcpServers[serverName] = {
      config,
      state: state ? state.state : "disconnected",
      error: state ? state.error : null,
    };
  }
  
  return result;
}

Managing Servers

Add/Update Server

export class FSMcpConfigRepo implements IMcpConfigRepo {
  async upsert(
    serverName: string,
    config: McpServerDefinition
  ): Promise<void> {
    const conf = await this.getConfig();
    conf.mcpServers[serverName] = config;
    await fs.writeFile(this.configPath, JSON.stringify(conf, null, 2));
  }
}

Remove Server

async delete(serverName: string): Promise<void> {
  const conf = await this.getConfig();
  delete conf.mcpServers[serverName];
  await fs.writeFile(this.configPath, JSON.stringify(conf, null, 2));
}

Cleanup

Close All Connections

export async function cleanup() {
  for (const [serverName, { client }] of Object.entries(clients)) {
    await client?.transport?.close();
    await client?.close();
    delete clients[serverName];
  }
}

Force Close (During Abort)

export async function forceCloseAllMcpClients(): Promise<void> {
  for (const [serverName, { client }] of Object.entries(clients)) {
    try {
      await client?.close();
    } catch {
      // Ignore errors during force close
    }
    delete clients[serverName];
  }
}
Clients are automatically reconnected on next use after force close.

Default Servers

Rowboat comes with Exa search pre-configured:
const DEFAULT_MCP_SERVERS = {
  exa: {
    type: "http" as const,
    url: "https://mcp.exa.ai/mcp",
  },
};

Configuration Schema

const McpServerDefinition = z.union([
  z.object({
    type: z.literal("http"),
    url: z.string(),
  }),
  z.object({
    type: z.literal("stdio"),
    command: z.string(),
    args: z.array(z.string()).optional(),
    env: z.record(z.string(), z.string()).optional(),
  }),
]);

const McpServerConfig = z.object({
  mcpServers: z.record(z.string(), McpServerDefinition),
});

Example: Custom MCP Server

Here’s how to add a custom MCP server:
{
  "mcpServers": {
    "my-custom-server": {
      "type": "stdio",
      "command": "python",
      "args": ["-m", "my_mcp_server"],
      "env": {
        "API_KEY": "your-api-key"
      }
    }
  }
}
Then use it:
const tools = await listTools('my-custom-server');
const result = await executeTool('my-custom-server', 'my_tool', { param: 'value' });

Troubleshooting

Connection Errors

If a server fails to connect:
try {
  await client.connect(transport);
} catch (error) {
  clients[serverName] = {
    state: "error",
    client: null,
    error: error instanceof Error ? error.message : "Unknown error",
  };
  transport?.close();
  throw error;
}
Check the error in the server list:
const servers = await listServers();
console.log(servers.mcpServers['my-server'].error);

Stdio Server Won’t Start

Make sure the command is executable and in your PATH. Test it manually first:
npx -y @modelcontextprotocol/server-filesystem /path/to/files

Transport Type Mismatch

If you’re not sure whether a server uses HTTP or SSE, try HTTP first:
try {
  transport = new StreamableHTTPClientTransport(new URL(config.url));
} catch {
  // Fallback to SSE
  transport = new SSEClientTransport(new URL(config.url));
}

Use Cases

Web Search (Exa)

const results = await executeTool('exa', 'exa_search', {
  query: 'Model Context Protocol',
  numResults: 5,
});

File System Access

const files = await executeTool('filesystem', 'list_directory', {
  path: '/Users/username/Documents',
});

Database Queries

{
  "postgres": {
    "type": "stdio",
    "command": "mcp-server-postgres",
    "env": {
      "DATABASE_URL": "postgresql://localhost/mydb"
    }
  }
}
  • Fireflies - Uses MCP for API access
  • Slack - Could use MCP for custom tools

Build docs developers (and LLMs) love