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
{
"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));
}
}
Example: Exa Search
{
"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;
}
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 };
}
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;
}
Example: Execute Exa Search
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