Files
flynn/src/mcp/bridge.ts
T
William Valentin cd839c7f0c feat: add MCP integration for external tool servers
Implement Model Context Protocol (MCP) support so Flynn can spawn MCP
server processes, discover their tools, and make them available to the
agent alongside builtin tools.

- McpClient: wraps @modelcontextprotocol/sdk with StdioClientTransport
  for process lifecycle, tool discovery (listTools), and invocation (callTool)
- McpManager: lifecycle management for multiple MCP servers with
  startAll/stopAll/restart, tool bridging into ToolRegistry
- Bridge: converts MCP tools to Flynn Tool interface with mcp:<server>:<tool>
  namespacing to avoid collisions with builtin tools
- Config: add env and cwd fields to mcp server schema
- ToolRegistry: add unregister() method for MCP server cleanup
- Daemon: wire McpManager into startup and shutdown lifecycle
- Tests: 28 new tests (bridge, manager, registry unregister)
2026-02-05 20:10:37 -08:00

79 lines
2.4 KiB
TypeScript

/**
* MCP Bridge — converts MCP tools into Flynn's Tool interface.
*
* Each MCP tool is prefixed with "mcp:<serverName>:" to avoid
* namespace collisions with builtin tools. When the tool is executed,
* the bridge routes the call back to the originating McpClient.
*/
import type { Tool, ToolResult } from '../tools/types.js';
import type { McpClient } from './client.js';
import type { McpToolInfo } from './types.js';
/**
* Create the prefixed tool name used in Flynn's tool registry.
*
* Example: MCP server "filesystem" with tool "read_file" -> "mcp:filesystem:read_file"
*/
export function mcpToolName(serverName: string, toolName: string): string {
return `mcp:${serverName}:${toolName}`;
}
/**
* Parse a prefixed tool name back into server + tool components.
* Returns null if the name doesn't match the mcp: prefix pattern.
*/
export function parseMcpToolName(prefixedName: string): { serverName: string; toolName: string } | null {
const match = prefixedName.match(/^mcp:([^:]+):(.+)$/);
if (!match) return null;
return { serverName: match[1], toolName: match[2] };
}
/**
* Convert a single MCP tool into a Flynn Tool.
*
* The returned Tool's execute() calls back into the McpClient
* to invoke the tool on the remote MCP server.
*/
export function bridgeMcpTool(client: McpClient, toolInfo: McpToolInfo): Tool {
const prefixedName = mcpToolName(client.serverName, toolInfo.name);
return {
name: prefixedName,
description: `[MCP:${client.serverName}] ${toolInfo.description}`,
inputSchema: {
type: 'object',
properties: toolInfo.inputSchema.properties ?? {},
required: toolInfo.inputSchema.required,
},
async execute(args: unknown): Promise<ToolResult> {
try {
const result = await client.callTool(
toolInfo.name,
(args ?? {}) as Record<string, unknown>,
);
return {
success: !result.isError,
output: result.content,
error: result.isError ? result.content : undefined,
};
} catch (error) {
return {
success: false,
output: '',
error: error instanceof Error ? error.message : String(error),
};
}
},
};
}
/**
* Bridge all tools from an MCP client into Flynn Tool objects.
*/
export function bridgeAllTools(client: McpClient): Tool[] {
return client.tools.map((t) => bridgeMcpTool(client, t));
}