cd839c7f0c
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)
79 lines
2.4 KiB
TypeScript
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));
|
|
}
|