mirror of
https://github.com/rowboatlabs/rowboat.git
synced 2026-06-18 20:15:20 +02:00
server for rowboatx
This commit is contained in:
parent
ae877e70ae
commit
9ad6331fbc
38 changed files with 2223 additions and 1088 deletions
|
|
@ -1,14 +1,13 @@
|
|||
import { z, ZodType } from "zod";
|
||||
import * as fs from "fs/promises";
|
||||
import * as path from "path";
|
||||
import { WorkDir as BASE_DIR } from "../config/config.js";
|
||||
import { WorkDir as BASE_DIR } from "../../config/config.js";
|
||||
import { executeCommand } from "./command-executor.js";
|
||||
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
|
||||
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
|
||||
import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js";
|
||||
import { Client } from "@modelcontextprotocol/sdk/client";
|
||||
import { resolveSkill, availableSkills } from "../assistant/skills/index.js";
|
||||
import { McpServerDefinition, McpServerConfig } from "../entities/mcp.js";
|
||||
import { executeTool, listServers, listTools } from "../../mcp/mcp.js";
|
||||
import container from "../../di/container.js";
|
||||
import { IMcpConfigRepo } from "../..//mcp/repo.js";
|
||||
import { McpServerDefinition } from "../../mcp/mcp.js";
|
||||
|
||||
const BuiltinToolsSchema = z.record(z.string(), z.object({
|
||||
description: z.string(),
|
||||
|
|
@ -310,109 +309,33 @@ export const BuiltinTools: z.infer<typeof BuiltinToolsSchema> = {
|
|||
description: 'Add or update an MCP server in the configuration with validation. This ensures the server definition is valid before saving.',
|
||||
inputSchema: z.object({
|
||||
serverName: z.string().describe('Name/alias for the MCP server'),
|
||||
serverType: z.enum(['stdio', 'http']).describe('Type of MCP server: "stdio" for command-based or "http" for HTTP/SSE-based'),
|
||||
command: z.string().optional().describe('Command to execute (required for stdio type, e.g., "npx", "python", "node")'),
|
||||
args: z.array(z.string()).optional().describe('Command arguments (optional, for stdio type)'),
|
||||
env: z.record(z.string(), z.string()).optional().describe('Environment variables (optional, for stdio type)'),
|
||||
url: z.string().optional().describe('HTTP/SSE endpoint URL (required for http type)'),
|
||||
headers: z.record(z.string(), z.string()).optional().describe('HTTP headers (optional, for http type)'),
|
||||
config: McpServerDefinition,
|
||||
}),
|
||||
execute: async ({ serverName, serverType, command, args, env, url, headers }: {
|
||||
execute: async ({ serverName, config }: {
|
||||
serverName: string;
|
||||
serverType: 'stdio' | 'http';
|
||||
command?: string;
|
||||
args?: string[];
|
||||
env?: Record<string, string>;
|
||||
url?: string;
|
||||
headers?: Record<string, string>;
|
||||
config: z.infer<typeof McpServerDefinition>;
|
||||
}) => {
|
||||
try {
|
||||
// Build server definition based on type
|
||||
let serverDef: any;
|
||||
if (serverType === 'stdio') {
|
||||
if (!command) {
|
||||
return {
|
||||
success: false,
|
||||
message: 'For stdio type servers, "command" is required. Example: "npx" or "python"',
|
||||
validationErrors: ['Missing required field: command'],
|
||||
};
|
||||
}
|
||||
serverDef = {
|
||||
type: 'stdio',
|
||||
command,
|
||||
...(args ? { args } : {}),
|
||||
...(env ? { env } : {}),
|
||||
};
|
||||
} else if (serverType === 'http') {
|
||||
if (!url) {
|
||||
return {
|
||||
success: false,
|
||||
message: 'For http type servers, "url" is required. Example: "http://localhost:3000/sse"',
|
||||
validationErrors: ['Missing required field: url'],
|
||||
};
|
||||
}
|
||||
serverDef = {
|
||||
type: 'http',
|
||||
url,
|
||||
...(headers ? { headers } : {}),
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
success: false,
|
||||
message: `Invalid serverType: ${serverType}. Must be "stdio" or "http"`,
|
||||
validationErrors: [`Invalid serverType: ${serverType}`],
|
||||
};
|
||||
}
|
||||
|
||||
// Validate against Zod schema
|
||||
const validationResult = McpServerDefinition.safeParse(serverDef);
|
||||
const validationResult = McpServerDefinition.safeParse(config);
|
||||
if (!validationResult.success) {
|
||||
return {
|
||||
success: false,
|
||||
message: 'Server definition failed validation. Check the errors below.',
|
||||
validationErrors: validationResult.error.issues.map((e: any) => `${e.path.join('.')}: ${e.message}`),
|
||||
providedDefinition: serverDef,
|
||||
providedDefinition: config,
|
||||
};
|
||||
}
|
||||
|
||||
// Read existing config
|
||||
const configPath = path.join(BASE_DIR, 'config', 'mcp.json');
|
||||
let currentConfig: z.infer<typeof McpServerConfig> = { mcpServers: {} };
|
||||
try {
|
||||
const content = await fs.readFile(configPath, 'utf-8');
|
||||
currentConfig = McpServerConfig.parse(JSON.parse(content));
|
||||
} catch (error: any) {
|
||||
if (error?.code !== 'ENOENT') {
|
||||
return {
|
||||
success: false,
|
||||
message: `Failed to read existing MCP config: ${error.message}`,
|
||||
};
|
||||
}
|
||||
// File doesn't exist, use empty config
|
||||
}
|
||||
|
||||
// Check if server already exists
|
||||
const isUpdate = !!currentConfig.mcpServers[serverName];
|
||||
|
||||
// Add/update server
|
||||
currentConfig.mcpServers[serverName] = validationResult.data;
|
||||
|
||||
// Write back to file
|
||||
await fs.mkdir(path.dirname(configPath), { recursive: true });
|
||||
await fs.writeFile(configPath, JSON.stringify(currentConfig, null, 2), 'utf-8');
|
||||
|
||||
const repo = container.resolve<IMcpConfigRepo>('mcpConfigRepo');
|
||||
await repo.upsert(serverName, config);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: `MCP server '${serverName}' ${isUpdate ? 'updated' : 'added'} successfully`,
|
||||
serverName,
|
||||
serverType,
|
||||
isUpdate,
|
||||
configuration: validationResult.data,
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
message: `Failed to add MCP server: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
||||
error: `Failed to update MCP server: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
||||
};
|
||||
}
|
||||
},
|
||||
|
|
@ -421,47 +344,17 @@ export const BuiltinTools: z.infer<typeof BuiltinToolsSchema> = {
|
|||
listMcpServers: {
|
||||
description: 'List all available MCP servers from the configuration',
|
||||
inputSchema: z.object({}),
|
||||
execute: async (): Promise<{ success: boolean, servers: any[], count: number, message: string }> => {
|
||||
execute: async () => {
|
||||
try {
|
||||
const configPath = path.join(BASE_DIR, 'config', 'mcp.json');
|
||||
|
||||
// Check if config exists
|
||||
try {
|
||||
await fs.access(configPath);
|
||||
} catch {
|
||||
return {
|
||||
success: true,
|
||||
servers: [],
|
||||
count: 0,
|
||||
message: 'No MCP servers configured yet',
|
||||
};
|
||||
}
|
||||
|
||||
const content = await fs.readFile(configPath, 'utf-8');
|
||||
const config = JSON.parse(content);
|
||||
|
||||
const servers = Object.keys(config.mcpServers || {}).map(name => {
|
||||
const server = config.mcpServers[name];
|
||||
return {
|
||||
name,
|
||||
type: 'command' in server ? 'stdio' : 'http',
|
||||
command: server.command,
|
||||
url: server.url,
|
||||
};
|
||||
});
|
||||
const result = await listServers();
|
||||
|
||||
return {
|
||||
success: true,
|
||||
servers,
|
||||
count: servers.length,
|
||||
message: `Found ${servers.length} MCP server(s)`,
|
||||
result,
|
||||
count: Object.keys(result.mcpServers).length,
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
servers: [],
|
||||
count: 0,
|
||||
message: `Failed to list MCP servers: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
||||
error: `Failed to list MCP servers: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
||||
};
|
||||
}
|
||||
},
|
||||
|
|
@ -471,69 +364,19 @@ export const BuiltinTools: z.infer<typeof BuiltinToolsSchema> = {
|
|||
description: 'List all available tools from a specific MCP server',
|
||||
inputSchema: z.object({
|
||||
serverName: z.string().describe('Name of the MCP server to query'),
|
||||
cursor: z.string().optional(),
|
||||
}),
|
||||
execute: async ({ serverName }: { serverName: string }) => {
|
||||
execute: async ({ serverName, cursor }: { serverName: string, cursor?: string }) => {
|
||||
try {
|
||||
const configPath = path.join(BASE_DIR, 'config', 'mcp.json');
|
||||
const content = await fs.readFile(configPath, 'utf-8');
|
||||
const config = JSON.parse(content);
|
||||
|
||||
const mcpConfig = config.mcpServers[serverName];
|
||||
if (!mcpConfig) {
|
||||
return {
|
||||
success: false,
|
||||
message: `MCP server '${serverName}' not found in configuration`,
|
||||
};
|
||||
}
|
||||
|
||||
// Create transport based on config type
|
||||
let transport;
|
||||
if ('command' in mcpConfig) {
|
||||
transport = new StdioClientTransport({
|
||||
command: mcpConfig.command,
|
||||
args: mcpConfig.args || [],
|
||||
env: mcpConfig.env || {},
|
||||
});
|
||||
} else {
|
||||
try {
|
||||
transport = new StreamableHTTPClientTransport(new URL(mcpConfig.url));
|
||||
} catch {
|
||||
transport = new SSEClientTransport(new URL(mcpConfig.url));
|
||||
}
|
||||
}
|
||||
|
||||
// Create and connect client
|
||||
const client = new Client({
|
||||
name: 'rowboat-copilot',
|
||||
version: '1.0.0',
|
||||
});
|
||||
|
||||
await client.connect(transport);
|
||||
|
||||
// List available tools
|
||||
const toolsList = await client.listTools();
|
||||
|
||||
// Close connection
|
||||
client.close();
|
||||
transport.close();
|
||||
|
||||
const tools = toolsList.tools.map((t: any) => ({
|
||||
name: t.name,
|
||||
description: t.description || 'No description',
|
||||
inputSchema: t.inputSchema,
|
||||
}));
|
||||
|
||||
const result = await listTools(serverName, cursor);
|
||||
return {
|
||||
success: true,
|
||||
serverName,
|
||||
tools,
|
||||
count: tools.length,
|
||||
message: `Found ${tools.length} tool(s) in MCP server '${serverName}'`,
|
||||
result,
|
||||
count: result.tools.length,
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
message: `Failed to list MCP tools: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
||||
error: `Failed to list MCP tools: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
||||
};
|
||||
}
|
||||
},
|
||||
|
|
@ -547,108 +390,19 @@ export const BuiltinTools: z.infer<typeof BuiltinToolsSchema> = {
|
|||
arguments: z.record(z.string(), z.any()).optional().describe('Arguments to pass to the tool (as key-value pairs matching the tool\'s input schema). MUST include all required parameters from the tool\'s inputSchema.'),
|
||||
}),
|
||||
execute: async ({ serverName, toolName, arguments: args = {} }: { serverName: string, toolName: string, arguments?: Record<string, any> }) => {
|
||||
let transport: any;
|
||||
let client: any;
|
||||
|
||||
try {
|
||||
const configPath = path.join(BASE_DIR, 'config', 'mcp.json');
|
||||
const content = await fs.readFile(configPath, 'utf-8');
|
||||
const config = JSON.parse(content);
|
||||
|
||||
const mcpConfig = config.mcpServers[serverName];
|
||||
if (!mcpConfig) {
|
||||
return {
|
||||
success: false,
|
||||
message: `MCP server '${serverName}' not found in configuration. Use listMcpServers to see available servers.`,
|
||||
};
|
||||
}
|
||||
|
||||
// Create transport based on config type
|
||||
if ('command' in mcpConfig) {
|
||||
transport = new StdioClientTransport({
|
||||
command: mcpConfig.command,
|
||||
args: mcpConfig.args || [],
|
||||
env: mcpConfig.env || {},
|
||||
});
|
||||
} else {
|
||||
try {
|
||||
transport = new StreamableHTTPClientTransport(new URL(mcpConfig.url));
|
||||
} catch {
|
||||
transport = new SSEClientTransport(new URL(mcpConfig.url));
|
||||
}
|
||||
}
|
||||
|
||||
// Create and connect client
|
||||
client = new Client({
|
||||
name: 'rowboat-copilot',
|
||||
version: '1.0.0',
|
||||
});
|
||||
|
||||
await client.connect(transport);
|
||||
|
||||
// Get tool list to validate the tool exists and check schema
|
||||
const toolsList = await client.listTools();
|
||||
const toolDef = toolsList.tools.find((t: any) => t.name === toolName);
|
||||
|
||||
if (!toolDef) {
|
||||
await client.close();
|
||||
transport.close();
|
||||
return {
|
||||
success: false,
|
||||
message: `Tool '${toolName}' not found in server '${serverName}'. Use listMcpTools to see available tools.`,
|
||||
availableTools: toolsList.tools.map((t: any) => t.name),
|
||||
};
|
||||
}
|
||||
|
||||
// Validate required parameters
|
||||
const inputSchema = toolDef.inputSchema;
|
||||
if (inputSchema && inputSchema.required && Array.isArray(inputSchema.required)) {
|
||||
const missingParams = inputSchema.required.filter((param: string) => !(param in args));
|
||||
if (missingParams.length > 0) {
|
||||
await client.close();
|
||||
transport.close();
|
||||
return {
|
||||
success: false,
|
||||
message: `Missing required parameters: ${missingParams.join(', ')}`,
|
||||
requiredParameters: inputSchema.required,
|
||||
providedArguments: Object.keys(args),
|
||||
toolSchema: inputSchema,
|
||||
hint: `Use listMcpTools to see the full schema for '${toolName}' and ensure all required parameters are included in the arguments field.`,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Call the tool
|
||||
const result = await client.callTool({
|
||||
name: toolName,
|
||||
arguments: args,
|
||||
});
|
||||
|
||||
// Close connection
|
||||
await client.close();
|
||||
transport.close();
|
||||
|
||||
const result = await executeTool(serverName, toolName, args);
|
||||
return {
|
||||
success: true,
|
||||
serverName,
|
||||
toolName,
|
||||
result: result.content,
|
||||
result,
|
||||
message: `Successfully executed tool '${toolName}' from server '${serverName}'`,
|
||||
};
|
||||
} catch (error) {
|
||||
// Ensure cleanup
|
||||
try {
|
||||
if (client) await client.close();
|
||||
if (transport) transport.close();
|
||||
} catch (cleanupError) {
|
||||
// Ignore cleanup errors
|
||||
}
|
||||
|
||||
return {
|
||||
success: false,
|
||||
message: `Failed to execute MCP tool: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
||||
serverName,
|
||||
toolName,
|
||||
error: `Failed to execute MCP tool: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
||||
hint: 'Use listMcpTools to verify the tool exists and check its schema. Ensure all required parameters are provided in the arguments field.',
|
||||
};
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue