2025-11-15 01:51:22 +05:30
|
|
|
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 { 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";
|
2025-11-16 11:36:50 +05:30
|
|
|
import { resolveSkill, availableSkills } from "../assistant/skills/index.js";
|
2025-11-25 20:00:43 +05:30
|
|
|
import { McpServerDefinition, McpServerConfig } from "../entities/mcp.js";
|
2025-11-15 01:51:22 +05:30
|
|
|
|
|
|
|
|
const BuiltinToolsSchema = z.record(z.string(), z.object({
|
|
|
|
|
description: z.string(),
|
|
|
|
|
inputSchema: z.custom<ZodType>(),
|
|
|
|
|
execute: z.function({
|
|
|
|
|
input: z.any(),
|
|
|
|
|
output: z.promise(z.any()),
|
|
|
|
|
}),
|
|
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
export const BuiltinTools: z.infer<typeof BuiltinToolsSchema> = {
|
2025-11-16 11:36:50 +05:30
|
|
|
loadSkill: {
|
|
|
|
|
description: "Load a Rowboat skill definition into context by fetching its guidance string",
|
|
|
|
|
inputSchema: z.object({
|
|
|
|
|
skillName: z.string().describe("Skill identifier or path (e.g., 'workflow-run-ops' or 'src/application/assistant/skills/workflow-run-ops/skill.ts')"),
|
|
|
|
|
}),
|
|
|
|
|
execute: async ({ skillName }: { skillName: string }) => {
|
|
|
|
|
const resolved = resolveSkill(skillName);
|
|
|
|
|
|
|
|
|
|
if (!resolved) {
|
|
|
|
|
return {
|
|
|
|
|
success: false,
|
|
|
|
|
message: `Skill '${skillName}' not found. Available skills: ${availableSkills.join(", ")}`,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
success: true,
|
|
|
|
|
skillName: resolved.id,
|
|
|
|
|
path: resolved.catalogPath,
|
|
|
|
|
content: resolved.content,
|
|
|
|
|
};
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
|
2025-11-15 01:51:22 +05:30
|
|
|
exploreDirectory: {
|
2025-11-17 23:27:00 +05:30
|
|
|
description: 'Recursively explore directory structure to understand existing agents and file organization',
|
2025-11-15 01:51:22 +05:30
|
|
|
inputSchema: z.object({
|
|
|
|
|
subdirectory: z.string().optional().describe('Subdirectory to explore (optional, defaults to root)'),
|
|
|
|
|
maxDepth: z.number().optional().describe('Maximum depth to traverse (default: 3)'),
|
|
|
|
|
}),
|
|
|
|
|
execute: async ({ subdirectory, maxDepth = 3 }: { subdirectory?: string, maxDepth?: number }) => {
|
|
|
|
|
async function explore(dir: string, depth: number = 0): Promise<any> {
|
|
|
|
|
if (depth > maxDepth) return null;
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
const entries = await fs.readdir(dir, { withFileTypes: true });
|
|
|
|
|
const result: any = { files: [], directories: {} };
|
|
|
|
|
|
|
|
|
|
for (const entry of entries) {
|
|
|
|
|
const fullPath = path.join(dir, entry.name);
|
|
|
|
|
if (entry.isFile()) {
|
|
|
|
|
const ext = path.extname(entry.name);
|
|
|
|
|
const size = (await fs.stat(fullPath)).size;
|
|
|
|
|
result.files.push({
|
|
|
|
|
name: entry.name,
|
|
|
|
|
type: ext || 'no-extension',
|
|
|
|
|
size: size,
|
|
|
|
|
relativePath: path.relative(BASE_DIR, fullPath),
|
|
|
|
|
});
|
|
|
|
|
} else if (entry.isDirectory()) {
|
|
|
|
|
result.directories[entry.name] = await explore(fullPath, depth + 1);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return result;
|
|
|
|
|
} catch (error) {
|
|
|
|
|
return { error: error instanceof Error ? error.message : 'Unknown error' };
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const dirPath = subdirectory ? path.join(BASE_DIR, subdirectory) : BASE_DIR;
|
|
|
|
|
const structure = await explore(dirPath);
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
success: true,
|
|
|
|
|
basePath: path.relative(BASE_DIR, dirPath) || '.',
|
|
|
|
|
structure,
|
|
|
|
|
};
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
readFile: {
|
|
|
|
|
description: 'Read and parse file contents. For JSON files, provides parsed structure.',
|
|
|
|
|
inputSchema: z.object({
|
|
|
|
|
filename: z.string().describe('The name of the file to read (relative to .rowboat directory)'),
|
|
|
|
|
}),
|
|
|
|
|
execute: async ({ filename }: { filename: string }) => {
|
|
|
|
|
try {
|
|
|
|
|
const filePath = path.join(BASE_DIR, filename);
|
|
|
|
|
const content = await fs.readFile(filePath, 'utf-8');
|
|
|
|
|
|
|
|
|
|
let parsed = null;
|
|
|
|
|
let fileType = path.extname(filename);
|
|
|
|
|
|
|
|
|
|
if (fileType === '.json') {
|
|
|
|
|
try {
|
|
|
|
|
parsed = JSON.parse(content);
|
|
|
|
|
} catch {
|
|
|
|
|
parsed = { error: 'Invalid JSON' };
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
success: true,
|
|
|
|
|
filename,
|
|
|
|
|
fileType,
|
|
|
|
|
content,
|
|
|
|
|
parsed,
|
|
|
|
|
path: filePath,
|
|
|
|
|
size: content.length,
|
|
|
|
|
};
|
|
|
|
|
} catch (error) {
|
|
|
|
|
return {
|
|
|
|
|
success: false,
|
|
|
|
|
message: `Failed to read file: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
createFile: {
|
|
|
|
|
description: 'Create a new file with content. Automatically creates parent directories if needed.',
|
|
|
|
|
inputSchema: z.object({
|
|
|
|
|
filename: z.string().describe('The name of the file to create (relative to .rowboat directory)'),
|
|
|
|
|
content: z.string().describe('The content to write to the file'),
|
|
|
|
|
description: z.string().optional().describe('Optional description of why this file is being created'),
|
|
|
|
|
}),
|
|
|
|
|
execute: async ({ filename, content, description }: { filename: string, content: string, description?: string }) => {
|
|
|
|
|
try {
|
|
|
|
|
const filePath = path.join(BASE_DIR, filename);
|
|
|
|
|
const dir = path.dirname(filePath);
|
|
|
|
|
|
|
|
|
|
// Ensure directory exists
|
|
|
|
|
await fs.mkdir(dir, { recursive: true });
|
|
|
|
|
|
|
|
|
|
// Write file
|
|
|
|
|
await fs.writeFile(filePath, content, 'utf-8');
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
success: true,
|
|
|
|
|
message: `File '${filename}' created successfully`,
|
|
|
|
|
description: description || 'No description provided',
|
|
|
|
|
path: filePath,
|
|
|
|
|
size: content.length,
|
|
|
|
|
};
|
|
|
|
|
} catch (error) {
|
|
|
|
|
return {
|
|
|
|
|
success: false,
|
|
|
|
|
message: `Failed to create file: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
updateFile: {
|
|
|
|
|
description: 'Update or overwrite the contents of an existing file',
|
|
|
|
|
inputSchema: z.object({
|
|
|
|
|
filename: z.string().describe('The name of the file to update (relative to .rowboat directory)'),
|
|
|
|
|
content: z.string().describe('The new content to write to the file'),
|
|
|
|
|
reason: z.string().optional().describe('Optional reason for the update'),
|
|
|
|
|
}),
|
|
|
|
|
execute: async ({ filename, content, reason }: { filename: string, content: string, reason?: string }) => {
|
|
|
|
|
try {
|
|
|
|
|
const filePath = path.join(BASE_DIR, filename);
|
|
|
|
|
|
|
|
|
|
// Check if file exists
|
|
|
|
|
await fs.access(filePath);
|
|
|
|
|
|
|
|
|
|
// Update file
|
|
|
|
|
await fs.writeFile(filePath, content, 'utf-8');
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
success: true,
|
|
|
|
|
message: `File '${filename}' updated successfully`,
|
|
|
|
|
reason: reason || 'No reason provided',
|
|
|
|
|
path: filePath,
|
|
|
|
|
size: content.length,
|
|
|
|
|
};
|
|
|
|
|
} catch (error) {
|
|
|
|
|
return {
|
|
|
|
|
success: false,
|
|
|
|
|
message: `Failed to update file: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
deleteFile: {
|
|
|
|
|
description: 'Delete a file from the .rowboat directory',
|
|
|
|
|
inputSchema: z.object({
|
|
|
|
|
filename: z.string().describe('The name of the file to delete (relative to .rowboat directory)'),
|
|
|
|
|
}),
|
|
|
|
|
execute: async ({ filename }: { filename: string }) => {
|
|
|
|
|
try {
|
|
|
|
|
const filePath = path.join(BASE_DIR, filename);
|
|
|
|
|
await fs.unlink(filePath);
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
success: true,
|
|
|
|
|
message: `File '${filename}' deleted successfully`,
|
|
|
|
|
path: filePath,
|
|
|
|
|
};
|
|
|
|
|
} catch (error) {
|
|
|
|
|
return {
|
|
|
|
|
success: false,
|
|
|
|
|
message: `Failed to delete file: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
listFiles: {
|
|
|
|
|
description: 'List all files and directories in the .rowboat directory or subdirectory',
|
|
|
|
|
inputSchema: z.object({
|
|
|
|
|
subdirectory: z.string().optional().describe('Optional subdirectory to list (relative to .rowboat directory)'),
|
|
|
|
|
}),
|
|
|
|
|
execute: async ({ subdirectory }: { subdirectory?: string }) => {
|
|
|
|
|
try {
|
|
|
|
|
const dirPath = subdirectory ? path.join(BASE_DIR, subdirectory) : BASE_DIR;
|
|
|
|
|
const entries = await fs.readdir(dirPath, { withFileTypes: true });
|
|
|
|
|
|
|
|
|
|
const files = entries
|
|
|
|
|
.filter(entry => entry.isFile())
|
|
|
|
|
.map(entry => ({
|
|
|
|
|
name: entry.name,
|
|
|
|
|
type: path.extname(entry.name) || 'no-extension',
|
|
|
|
|
relativePath: path.relative(BASE_DIR, path.join(dirPath, entry.name)),
|
|
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
const directories = entries
|
|
|
|
|
.filter(entry => entry.isDirectory())
|
|
|
|
|
.map(entry => entry.name);
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
success: true,
|
|
|
|
|
path: dirPath,
|
|
|
|
|
relativePath: path.relative(BASE_DIR, dirPath) || '.',
|
|
|
|
|
files,
|
|
|
|
|
directories,
|
|
|
|
|
totalFiles: files.length,
|
|
|
|
|
totalDirectories: directories.length,
|
|
|
|
|
};
|
|
|
|
|
} catch (error) {
|
|
|
|
|
return {
|
|
|
|
|
success: false,
|
|
|
|
|
message: `Failed to list files: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
|
2025-11-17 23:27:00 +05:30
|
|
|
analyzeAgent: {
|
|
|
|
|
description: 'Read and analyze an agent file to understand its structure, tools, and configuration',
|
2025-11-15 01:51:22 +05:30
|
|
|
inputSchema: z.object({
|
2025-11-17 23:27:00 +05:30
|
|
|
agentName: z.string().describe('Name of the agent file to analyze (with or without .json extension)'),
|
2025-11-15 01:51:22 +05:30
|
|
|
}),
|
2025-11-17 23:27:00 +05:30
|
|
|
execute: async ({ agentName }: { agentName: string }) => {
|
2025-11-15 01:51:22 +05:30
|
|
|
try {
|
2025-11-17 23:27:00 +05:30
|
|
|
const filename = agentName.endsWith('.json') ? agentName : `${agentName}.json`;
|
|
|
|
|
const filePath = path.join(BASE_DIR, 'agents', filename);
|
2025-11-15 01:51:22 +05:30
|
|
|
|
|
|
|
|
const content = await fs.readFile(filePath, 'utf-8');
|
2025-11-17 23:27:00 +05:30
|
|
|
const agent = JSON.parse(content);
|
2025-11-15 01:51:22 +05:30
|
|
|
|
|
|
|
|
// Extract key information
|
2025-11-17 23:27:00 +05:30
|
|
|
const toolsList = agent.tools ? Object.keys(agent.tools) : [];
|
|
|
|
|
const agentTools = agent.tools ? Object.entries(agent.tools).map(([key, tool]: [string, any]) => ({
|
|
|
|
|
key,
|
|
|
|
|
type: tool.type,
|
|
|
|
|
name: tool.name || key,
|
|
|
|
|
})) : [];
|
|
|
|
|
|
2025-11-15 01:51:22 +05:30
|
|
|
const analysis = {
|
2025-11-17 23:27:00 +05:30
|
|
|
name: agent.name,
|
|
|
|
|
description: agent.description || 'No description',
|
|
|
|
|
model: agent.model || 'Not specified',
|
|
|
|
|
toolCount: toolsList.length,
|
|
|
|
|
tools: agentTools,
|
|
|
|
|
hasOtherAgents: agentTools.some((t: any) => t.type === 'agent'),
|
|
|
|
|
structure: agent,
|
2025-11-15 01:51:22 +05:30
|
|
|
};
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
success: true,
|
|
|
|
|
filePath: path.relative(BASE_DIR, filePath),
|
|
|
|
|
analysis,
|
|
|
|
|
};
|
|
|
|
|
} catch (error) {
|
|
|
|
|
return {
|
|
|
|
|
success: false,
|
2025-11-17 23:27:00 +05:30
|
|
|
message: `Failed to analyze agent: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
2025-11-15 01:51:22 +05:30
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
|
2025-11-25 20:00:43 +05:30
|
|
|
addMcpServer: {
|
|
|
|
|
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)'),
|
|
|
|
|
}),
|
|
|
|
|
execute: async ({ serverName, serverType, command, args, env, url, headers }: {
|
|
|
|
|
serverName: string;
|
|
|
|
|
serverType: 'stdio' | 'http';
|
|
|
|
|
command?: string;
|
|
|
|
|
args?: string[];
|
|
|
|
|
env?: Record<string, string>;
|
|
|
|
|
url?: string;
|
|
|
|
|
headers?: Record<string, string>;
|
|
|
|
|
}) => {
|
|
|
|
|
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);
|
|
|
|
|
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,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 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');
|
|
|
|
|
|
|
|
|
|
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'}`,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
|
2025-11-15 01:51:22 +05:30
|
|
|
listMcpServers: {
|
|
|
|
|
description: 'List all available MCP servers from the configuration',
|
|
|
|
|
inputSchema: z.object({}),
|
|
|
|
|
execute: async (): Promise<{ success: boolean, servers: any[], count: number, message: string }> => {
|
|
|
|
|
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,
|
|
|
|
|
};
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
success: true,
|
|
|
|
|
servers,
|
|
|
|
|
count: servers.length,
|
|
|
|
|
message: `Found ${servers.length} MCP server(s)`,
|
|
|
|
|
};
|
|
|
|
|
} catch (error) {
|
|
|
|
|
return {
|
|
|
|
|
success: false,
|
|
|
|
|
servers: [],
|
|
|
|
|
count: 0,
|
|
|
|
|
message: `Failed to list MCP servers: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
listMcpTools: {
|
|
|
|
|
description: 'List all available tools from a specific MCP server',
|
|
|
|
|
inputSchema: z.object({
|
|
|
|
|
serverName: z.string().describe('Name of the MCP server to query'),
|
|
|
|
|
}),
|
|
|
|
|
execute: async ({ serverName }: { serverName: 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,
|
|
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
success: true,
|
|
|
|
|
serverName,
|
|
|
|
|
tools,
|
|
|
|
|
count: tools.length,
|
|
|
|
|
message: `Found ${tools.length} tool(s) in MCP server '${serverName}'`,
|
|
|
|
|
};
|
|
|
|
|
} catch (error) {
|
|
|
|
|
return {
|
|
|
|
|
success: false,
|
|
|
|
|
message: `Failed to list MCP tools: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
executeCommand: {
|
|
|
|
|
description: 'Execute a shell command and return the output. Use this to run bash/shell commands.',
|
|
|
|
|
inputSchema: z.object({
|
|
|
|
|
command: z.string().describe('The shell command to execute (e.g., "ls -la", "cat file.txt")'),
|
|
|
|
|
cwd: z.string().optional().describe('Working directory to execute the command in (defaults to .rowboat directory)'),
|
|
|
|
|
}),
|
|
|
|
|
execute: async ({ command, cwd }: { command: string, cwd?: string }) => {
|
|
|
|
|
try {
|
|
|
|
|
const workingDir = cwd ? path.join(BASE_DIR, cwd) : BASE_DIR;
|
|
|
|
|
const result = await executeCommand(command, { cwd: workingDir });
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
success: result.exitCode === 0,
|
|
|
|
|
stdout: result.stdout,
|
|
|
|
|
stderr: result.stderr,
|
|
|
|
|
exitCode: result.exitCode,
|
|
|
|
|
command,
|
|
|
|
|
workingDir,
|
|
|
|
|
};
|
|
|
|
|
} catch (error) {
|
|
|
|
|
return {
|
|
|
|
|
success: false,
|
|
|
|
|
message: `Failed to execute command: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
|
|
|
command,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
},
|
2025-11-16 11:36:50 +05:30
|
|
|
};
|