diff --git a/apps/cli/src/application/assistant/instructions.ts b/apps/cli/src/application/assistant/instructions.ts index aca6a8a5..8321c6b0 100644 --- a/apps/cli/src/application/assistant/instructions.ts +++ b/apps/cli/src/application/assistant/instructions.ts @@ -38,11 +38,16 @@ Always consult this catalog first so you load the right skills before taking act - \`deleteFile\`, \`createFile\`, \`updateFile\`, \`readFile\` - File operations - \`listFiles\`, \`exploreDirectory\` - Directory exploration - \`analyzeAgent\` - Agent analysis -- \`listMcpServers\`, \`listMcpTools\` - MCP server management +- \`addMcpServer\`, \`listMcpServers\`, \`listMcpTools\` - MCP server management - \`loadSkill\` - Skill loading These tools work directly and are NOT filtered by \`.rowboat/config/security.json\`. +**CRITICAL: MCP Server Configuration** +- ALWAYS use the \`addMcpServer\` builtin tool to add or update MCP servers—it validates the configuration before saving +- NEVER manually edit \`config/mcp.json\` using \`createFile\` or \`updateFile\` for MCP servers +- Invalid MCP configs will prevent the agent from starting with validation errors + **Only \`executeCommand\` (shell/bash commands) is filtered** by the security allowlist. If you need to delete a file, use the \`deleteFile\` builtin tool, not \`executeCommand\` with \`rm\`. If you need to create a file, use \`createFile\`, not \`executeCommand\` with \`touch\` or \`echo >\`. The security allowlist in \`security.json\` only applies to shell commands executed via \`executeCommand\`, not to Rowboat's internal builtin tools. diff --git a/apps/cli/src/application/assistant/skills/mcp-integration/skill.ts b/apps/cli/src/application/assistant/skills/mcp-integration/skill.ts index 1f3aa313..f8aa0846 100644 --- a/apps/cli/src/application/assistant/skills/mcp-integration/skill.ts +++ b/apps/cli/src/application/assistant/skills/mcp-integration/skill.ts @@ -1,22 +1,225 @@ export const skill = String.raw` # MCP Integration Guidance -Load this skill whenever a user asks about external tools, MCP servers, or how to extend an agent’s capabilities. +Load this skill whenever a user asks about external tools, MCP servers, or how to extend an agent's capabilities. ## Key concepts - MCP servers expose tools (web scraping, APIs, databases, etc.) declared in \`config/mcp.json\`. - Agents reference MCP tools through the \`"tools"\` block by specifying \`type\`, \`name\`, \`description\`, \`mcpServerName\`, and a full \`inputSchema\`. - Tool schemas can include optional property descriptions; only include \`"required"\` when parameters are mandatory. +## CRITICAL: Adding MCP Servers + +**ALWAYS use the \`addMcpServer\` builtin tool** to add or update MCP server configurations. This tool validates the configuration before saving and prevents startup errors. + +**NEVER manually create or edit \`config/mcp.json\`** using \`createFile\` or \`updateFile\` for MCP servers—this bypasses validation and will cause errors. + +### MCP Server Configuration Schema + +There are TWO types of MCP servers: + +#### 1. STDIO (Command-based) Servers +For servers that run as local processes (Node.js, Python, etc.): + +**Required fields:** +- \`command\`: string (e.g., "npx", "node", "python", "uvx") + +**Optional fields:** +- \`args\`: array of strings (command arguments) +- \`env\`: object with string key-value pairs (environment variables) +- \`type\`: "stdio" (optional, inferred from presence of \`command\`) + +**Schema:** +\`\`\`json +{ + "type": "stdio", + "command": "string (REQUIRED)", + "args": ["string", "..."], + "env": { + "KEY": "value" + } +} +\`\`\` + +**Valid STDIO examples:** +\`\`\`json +{ + "command": "npx", + "args": ["-y", "@modelcontextprotocol/server-filesystem", "/path/to/data"] +} +\`\`\` + +\`\`\`json +{ + "command": "python", + "args": ["-m", "mcp_server_git"], + "env": { + "GIT_REPO_PATH": "/path/to/repo" + } +} +\`\`\` + +\`\`\`json +{ + "command": "uvx", + "args": ["mcp-server-fetch"] +} +\`\`\` + +#### 2. HTTP/SSE Servers +For servers that expose HTTP or Server-Sent Events endpoints: + +**Required fields:** +- \`url\`: string (complete URL including protocol and path) + +**Optional fields:** +- \`headers\`: object with string key-value pairs (HTTP headers) +- \`type\`: "http" (optional, inferred from presence of \`url\`) + +**Schema:** +\`\`\`json +{ + "type": "http", + "url": "string (REQUIRED)", + "headers": { + "Authorization": "Bearer token", + "Custom-Header": "value" + } +} +\`\`\` + +**Valid HTTP examples:** +\`\`\`json +{ + "url": "http://localhost:3000/sse" +} +\`\`\` + +\`\`\`json +{ + "url": "https://api.example.com/mcp", + "headers": { + "Authorization": "Bearer sk-1234567890" + } +} +\`\`\` + +### Common Validation Errors to Avoid + +❌ **WRONG - Missing required field:** +\`\`\`json +{ + "args": ["some-arg"] +} +\`\`\` +Error: Missing \`command\` for stdio OR \`url\` for http + +❌ **WRONG - Empty object:** +\`\`\`json +{} +\`\`\` +Error: Must have either \`command\` (stdio) or \`url\` (http) + +❌ **WRONG - Mixed types:** +\`\`\`json +{ + "command": "npx", + "url": "http://localhost:3000" +} +\`\`\` +Error: Cannot have both \`command\` and \`url\` + +✅ **CORRECT - Minimal stdio:** +\`\`\`json +{ + "command": "npx", + "args": ["-y", "@modelcontextprotocol/server-time"] +} +\`\`\` + +✅ **CORRECT - Minimal http:** +\`\`\`json +{ + "url": "http://localhost:3000/sse" +} +\`\`\` + +### Using addMcpServer Tool + +**Example 1: Add stdio server** +\`\`\`json +{ + "serverName": "filesystem", + "serverType": "stdio", + "command": "npx", + "args": ["-y", "@modelcontextprotocol/server-filesystem", "/Users/me/data"] +} +\`\`\` + +**Example 2: Add HTTP server** +\`\`\`json +{ + "serverName": "custom-api", + "serverType": "http", + "url": "https://api.example.com/mcp", + "headers": { + "Authorization": "Bearer token123" + } +} +\`\`\` + +**Example 3: Add Python MCP server** +\`\`\`json +{ + "serverName": "github", + "serverType": "stdio", + "command": "python", + "args": ["-m", "mcp_server_github"], + "env": { + "GITHUB_TOKEN": "ghp_xxxxx" + } +} +\`\`\` + ## Operator actions 1. Use \`listMcpServers\` to enumerate configured servers. -2. Use \`listMcpTools\` for a server to understand the available operations and schemas. -3. Explain which MCP tools match the user’s needs before editing agent definitions. -4. When adding a tool to an agent, document what it does and ensure the schema mirrors the MCP definition. +2. Use \`addMcpServer\` to add or update MCP server configurations (with validation). +3. Use \`listMcpTools\` for a server to understand the available operations and schemas. +4. Explain which MCP tools match the user's needs before editing agent definitions. +5. When adding a tool to an agent, document what it does and ensure the schema mirrors the MCP definition. -## Example snippets to reference -- Firecrawl search (required param): +## Adding MCP Tools to Agents + +Once an MCP server is configured, add its tools to agent definitions: + +### MCP Tool Format in Agent +\`\`\`json +"tools": { + "descriptive_key": { + "type": "mcp", + "name": "actual_tool_name_from_server", + "description": "What the tool does", + "mcpServerName": "server_name_from_config", + "inputSchema": { + "type": "object", + "properties": { + "param1": {"type": "string", "description": "What param1 means"} + }, + "required": ["param1"] + } + } +} \`\`\` + +### Tool Schema Rules +- Use \`listMcpTools\` to get the exact \`inputSchema\` from the server +- Copy the schema exactly as provided by the MCP server +- Only include \`"required"\` array if parameters are truly mandatory +- Add descriptions to help the agent understand parameter usage + +### Example snippets to reference +- Firecrawl search (required param): +\`\`\`json "tools": { "search": { "type": "mcp", @@ -34,8 +237,9 @@ Load this skill whenever a user asks about external tools, MCP servers, or how t } } \`\`\` + - ElevenLabs text-to-speech (no required array): -\`\`\` +\`\`\`json "tools": { "text_to_speech": { "type": "mcp", @@ -52,9 +256,31 @@ Load this skill whenever a user asks about external tools, MCP servers, or how t } \`\`\` +- Filesystem operations: +\`\`\`json +"tools": { + "read_file": { + "type": "mcp", + "name": "read_file", + "description": "Read file contents", + "mcpServerName": "filesystem", + "inputSchema": { + "type": "object", + "properties": { + "path": {"type": "string", "description": "File path to read"} + }, + "required": ["path"] + } + } +} +\`\`\` + ## Safety reminders -- Only recommend MCP tools that are actually configured. -- Clarify any missing details (required parameters, server names) before modifying files. +- ALWAYS use \`addMcpServer\` to configure MCP servers—never manually edit config files +- Only recommend MCP tools that are actually configured (use \`listMcpServers\` first) +- Clarify any missing details (required parameters, server names) before modifying files +- Test server connection with \`listMcpTools\` after adding a new server +- Invalid MCP configs prevent agents from starting—validation is critical `; export default skill; diff --git a/apps/cli/src/application/assistant/skills/workflow-authoring/skill.ts b/apps/cli/src/application/assistant/skills/workflow-authoring/skill.ts index dd6dfc0e..098b39ae 100644 --- a/apps/cli/src/application/assistant/skills/workflow-authoring/skill.ts +++ b/apps/cli/src/application/assistant/skills/workflow-authoring/skill.ts @@ -19,7 +19,45 @@ Load this skill whenever a user wants to inspect, create, or update agents insid 3. The orchestrator calls other agents as tools when needed 4. Data flows through tool call parameters and responses -## Agent format +## Agent File Schema + +Agent files MUST conform to this exact schema. Invalid agents will fail to load. + +### Complete Agent Schema +\`\`\`json +{ + "name": "string (REQUIRED, must match filename without .json)", + "description": "string (REQUIRED, what this agent does)", + "instructions": "string (REQUIRED, detailed instructions for the agent)", + "model": "string (OPTIONAL, e.g., 'gpt-5.1', 'claude-sonnet-4-5')", + "provider": "string (OPTIONAL, provider alias from models.json)", + "tools": { + "descriptive_key": { + "type": "builtin | mcp | agent (REQUIRED)", + "name": "string (REQUIRED)", + // Additional fields depend on type - see below + } + } +} +\`\`\` + +### Required Fields +- \`name\`: Agent identifier (must exactly match the filename without .json) +- \`description\`: Brief description of agent's purpose +- \`instructions\`: Detailed instructions for how the agent should behave + +### Optional Fields +- \`model\`: Model to use (defaults to model config if not specified) +- \`provider\`: Provider alias from models.json (optional) +- \`tools\`: Object containing tool definitions (can be empty or omitted) + +### Naming Rules +- Agent filename MUST match the \`name\` field exactly +- Example: If \`name\` is "summariser_agent", file must be "summariser_agent.json" +- Use lowercase with underscores for multi-word names +- No spaces or special characters in names + +### Agent Format Example \`\`\`json { "name": "agent_name", @@ -43,9 +81,26 @@ Load this skill whenever a user wants to inspect, create, or update agents insid } \`\`\` -## Tool types +## Tool Types & Schemas -### Builtin tools +Tools in agents must follow one of three types. Each has specific required fields. + +### 1. Builtin Tools +Internal Rowboat tools (executeCommand, file operations, MCP queries, etc.) + +**Schema:** +\`\`\`json +{ + "type": "builtin", + "name": "tool_name" +} +\`\`\` + +**Required fields:** +- \`type\`: Must be "builtin" +- \`name\`: Builtin tool name (e.g., "executeCommand", "readFile") + +**Example:** \`\`\`json "bash": { "type": "builtin", @@ -53,7 +108,42 @@ Load this skill whenever a user wants to inspect, create, or update agents insid } \`\`\` -### MCP tools +**Available builtin tools:** +- \`executeCommand\` - Execute shell commands +- \`readFile\`, \`createFile\`, \`updateFile\`, \`deleteFile\` - File operations +- \`listFiles\`, \`exploreDirectory\` - Directory operations +- \`analyzeAgent\` - Analyze agent structure +- \`addMcpServer\`, \`listMcpServers\`, \`listMcpTools\` - MCP management +- \`loadSkill\` - Load skill guidance + +### 2. MCP Tools +Tools from external MCP servers (APIs, databases, web scraping, etc.) + +**Schema:** +\`\`\`json +{ + "type": "mcp", + "name": "tool_name_from_server", + "description": "What the tool does", + "mcpServerName": "server_name_from_config", + "inputSchema": { + "type": "object", + "properties": { + "param": {"type": "string", "description": "Parameter description"} + }, + "required": ["param"] + } +} +\`\`\` + +**Required fields:** +- \`type\`: Must be "mcp" +- \`name\`: Exact tool name from MCP server +- \`description\`: What the tool does (helps agent understand when to use it) +- \`mcpServerName\`: Server name from config/mcp.json +- \`inputSchema\`: Full JSON Schema object for tool parameters + +**Example:** \`\`\`json "search": { "type": "mcp", @@ -70,17 +160,40 @@ Load this skill whenever a user wants to inspect, create, or update agents insid } \`\`\` -### Agent tools (for chaining agents) +**Important:** +- Use \`listMcpTools\` to get the exact inputSchema from the server +- Copy the schema exactly—don't modify property types or structure +- Only include \`"required"\` array if parameters are mandatory + +### 3. Agent Tools (for chaining agents) +Reference other agents as tools to build multi-agent workflows + +**Schema:** +\`\`\`json +{ + "type": "agent", + "name": "target_agent_name" +} +\`\`\` + +**Required fields:** +- \`type\`: Must be "agent" +- \`name\`: Name of the target agent (must exist in agents/ directory) + +**Example:** \`\`\`json "summariser": { "type": "agent", "name": "summariser_agent" } \`\`\` + +**How it works:** - Use \`"type": "agent"\` to call other agents as tools - The target agent will be invoked with the parameters you pass - Results are returned as tool output - This is how you build multi-agent workflows +- The referenced agent file must exist (e.g., agents/summariser_agent.json) ## Complete Multi-Agent Workflow Example @@ -156,13 +269,88 @@ Load this skill whenever a user wants to inspect, create, or update agents insid 5. **Tool naming**: Use descriptive tool keys (e.g., "summariser", "fetch_data", "analyze") 6. **Orchestration**: Create a top-level agent that coordinates the workflow +## Validation & Best Practices + +### CRITICAL: Schema Compliance +- Agent files MUST have \`name\`, \`description\`, and \`instructions\` fields +- Agent filename MUST exactly match the \`name\` field +- Tools MUST have valid \`type\` ("builtin", "mcp", or "agent") +- MCP tools MUST have all required fields: name, description, mcpServerName, inputSchema +- Agent tools MUST reference existing agent files +- Invalid agents will fail to load and prevent workflow execution + +### File Creation/Update Process +1. When creating an agent, use \`createFile\` with complete, valid JSON +2. When updating an agent, read it first with \`readFile\`, modify, then use \`updateFile\` +3. Validate JSON syntax before writing—malformed JSON breaks the agent +4. Test agent loading after creation/update by using \`analyzeAgent\` + +### Common Validation Errors to Avoid + +❌ **WRONG - Missing required fields:** +\`\`\`json +{ + "name": "my_agent" + // Missing description and instructions +} +\`\`\` + +❌ **WRONG - Filename mismatch:** +- File: agents/my_agent.json +- Content: {"name": "myagent", ...} + +❌ **WRONG - Invalid tool type:** +\`\`\`json +"tool1": { + "type": "custom", // Invalid type + "name": "something" +} +\`\`\` + +❌ **WRONG - MCP tool missing required fields:** +\`\`\`json +"search": { + "type": "mcp", + "name": "firecrawl_search" + // Missing: description, mcpServerName, inputSchema +} +\`\`\` + +✅ **CORRECT - Minimal valid agent:** +\`\`\`json +{ + "name": "simple_agent", + "description": "A simple agent", + "instructions": "Do simple tasks" +} +\`\`\` + +✅ **CORRECT - Complete MCP tool:** +\`\`\`json +"search": { + "type": "mcp", + "name": "firecrawl_search", + "description": "Search the web", + "mcpServerName": "firecrawl", + "inputSchema": { + "type": "object", + "properties": { + "query": {"type": "string"} + } + } +} +\`\`\` + ## Capabilities checklist 1. Explore \`agents/\` directory to understand existing agents before editing -2. Update files carefully to maintain schema validity -3. When creating multi-agent workflows, create an orchestrator agent -4. Add other agents as tools with \`"type": "agent"\` for chaining -5. List and explore MCP servers/tools when users need new capabilities -6. Confirm work done and outline next steps once changes are complete +2. Read existing agents with \`readFile\` before making changes +3. Validate all required fields are present before creating/updating agents +4. Ensure filename matches the \`name\` field exactly +5. Use \`analyzeAgent\` to verify agent structure after creation/update +6. When creating multi-agent workflows, create an orchestrator agent +7. Add other agents as tools with \`"type": "agent"\` for chaining +8. Use \`listMcpServers\` and \`listMcpTools\` when adding MCP integrations +9. Confirm work done and outline next steps once changes are complete `; export default skill; diff --git a/apps/cli/src/application/lib/builtin-tools.ts b/apps/cli/src/application/lib/builtin-tools.ts index 5c4ae64b..80158d77 100644 --- a/apps/cli/src/application/lib/builtin-tools.ts +++ b/apps/cli/src/application/lib/builtin-tools.ts @@ -8,6 +8,7 @@ import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/ 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"; const BuiltinToolsSchema = z.record(z.string(), z.object({ description: z.string(), @@ -305,6 +306,118 @@ export const BuiltinTools: z.infer = { }, }, + 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; + url?: string; + headers?: Record; + }) => { + 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 = { 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'}`, + }; + } + }, + }, + listMcpServers: { description: 'List all available MCP servers from the configuration', inputSchema: z.object({}),