diff --git a/apps/sim/app/api/mcp/copilot/route.ts b/apps/sim/app/api/mcp/copilot/route.ts index 6e4d6a9a7..042a4983d 100644 --- a/apps/sim/app/api/mcp/copilot/route.ts +++ b/apps/sim/app/api/mcp/copilot/route.ts @@ -21,6 +21,152 @@ const logger = createLogger('CopilotMcpAPI') export const dynamic = 'force-dynamic' +/** + * MCP Server instructions that guide LLMs on how to use the Sim copilot tools. + * This is included in the initialize response to help external LLMs understand + * the workflow lifecycle and best practices. + */ +const MCP_SERVER_INSTRUCTIONS = ` +## Sim Workflow Copilot - Usage Guide + +You are interacting with Sim's workflow automation platform. These tools orchestrate specialized AI agents that build workflows. Follow these guidelines carefully. + +--- + +## Platform Knowledge + +Sim is a workflow automation platform. Workflows are visual pipelines of blocks. + +### Block Types + +**Core Logic:** +- **Agent** - The heart of Sim (LLM block with tools, memory, structured output, knowledge bases) +- **Function** - JavaScript code execution +- **Condition** - If/else branching +- **Router** - AI-powered content-based routing +- **Loop** - While/do-while iteration +- **Parallel** - Simultaneous execution +- **API** - HTTP requests + +**Integrations (3rd Party):** +- OAuth: Slack, Gmail, Google Calendar, Sheets, Outlook, Linear, GitHub, Notion +- API: Stripe, Twilio, SendGrid, any REST API + +### The Agent Block + +The Agent block is the core of intelligent workflows: +- **Tools** - Add integrations, custom tools, web search to give it capabilities +- **Memory** - Multi-turn conversations with persistent context +- **Structured Output** - JSON schema for reliable parsing +- **Knowledge Bases** - RAG-powered document retrieval + +**Design principle:** Put tools INSIDE agents rather than using standalone tool blocks. + +### Triggers + +| Type | Description | +|------|-------------| +| Manual/Chat | User sends message in UI (start block: input, files, conversationId) | +| API | REST endpoint with custom input schema | +| Webhook | External services POST to trigger URL | +| Schedule | Cron-based (hourly, daily, weekly) | + +### Deployments + +| Type | Trigger | Use Case | +|------|---------|----------| +| API | Start block | REST endpoint for programmatic access | +| Chat | Start block | Managed chat UI with auth options | +| MCP | Start block | Expose as MCP tool for AI agents | +| General | Schedule/Webhook | Activate triggers to run automatically | + +**Undeployed workflows only run in the builder UI.** + +### Variable Syntax + +Reference outputs from previous blocks: \`\` +Reference environment variables: \`{{ENV_VAR_NAME}}\` + +Rules: +- Block names must be lowercase, no spaces, no special characters +- Use dot notation for nested fields: \`\` + +--- + +## Workflow Lifecycle + +1. **Create**: For NEW workflows, FIRST call create_workflow to get a workflowId +2. **Plan**: Use copilot_plan with the workflowId to plan the workflow +3. **Edit**: Use copilot_edit with the workflowId AND the plan to build the workflow +4. **Deploy**: ALWAYS deploy after building using copilot_deploy before testing/running +5. **Test**: Use copilot_test to verify the workflow works correctly +6. **Share**: Provide the user with the workflow URL after completion + +--- + +## CRITICAL: Always Pass workflowId + +- For NEW workflows: Call create_workflow FIRST, then use the returned workflowId +- For EXISTING workflows: Pass the workflowId to all copilot tools +- copilot_plan, copilot_edit, copilot_deploy, copilot_test, copilot_debug all REQUIRE workflowId + +--- + +## CRITICAL: How to Handle Plans + +The copilot_plan tool returns a structured plan object. You MUST: + +1. **Do NOT modify the plan**: Pass the plan object EXACTLY as returned to copilot_edit +2. **Do NOT interpret or summarize the plan**: The edit agent needs the raw plan data +3. **Pass the plan in the context.plan field**: \`{ "context": { "plan": } }\` +4. **Include ALL plan data**: Block configurations, connections, credentials, everything + +Example flow: +\`\`\` +1. copilot_plan({ request: "build a workflow...", workflowId: "abc123" }) + -> Returns: { "plan": { "blocks": [...], "connections": [...], ... } } + +2. copilot_edit({ + workflowId: "abc123", + message: "Execute the plan", + context: { "plan": } + }) +\`\`\` + +**Why this matters**: The plan contains technical details (block IDs, field mappings, API schemas) that the edit agent needs verbatim. Summarizing or rephrasing loses critical information. + +--- + +## CRITICAL: Error Handling + +**If the user says "doesn't work", "broke", "failed", "error" → ALWAYS use copilot_debug FIRST.** + +Don't guess. Don't plan. Debug first to find the actual problem. + +--- + +## Important Rules + +- ALWAYS deploy a workflow before attempting to run or test it +- Workflows must be deployed to have an "active deployment" for execution +- After building, call copilot_deploy with the appropriate deployment type (api, chat, or mcp) +- Return the workflow URL to the user so they can access it in Sim + +--- + +## Quick Operations (use direct tools) +- list_workflows, list_workspaces, list_folders, get_workflow: Fast database queries +- create_workflow: Create new workflow and get workflowId (CALL THIS FIRST for new workflows) +- create_folder: Create new resources + +## Workflow Building (use copilot tools) +- copilot_plan: Plan workflow changes (REQUIRES workflowId) - returns a plan object +- copilot_edit: Execute the plan (REQUIRES workflowId AND plan from copilot_plan) +- copilot_deploy: Deploy workflows (REQUIRES workflowId) +- copilot_test: Test workflow execution (REQUIRES workflowId) +- copilot_debug: Diagnose errors (REQUIRES workflowId) - USE THIS FIRST for issues +` + /** * Direct tools that execute immediately without LLM orchestration. * These are fast database queries that don't need AI reasoning. @@ -91,6 +237,56 @@ const DIRECT_TOOL_DEFS: Array<{ }, }, }, + { + name: 'create_workflow', + toolId: 'create_workflow', + description: 'Create a new workflow. Returns the new workflow ID.', + inputSchema: { + type: 'object', + properties: { + name: { + type: 'string', + description: 'Name for the new workflow.', + }, + workspaceId: { + type: 'string', + description: 'Optional workspace ID. Uses default workspace if not provided.', + }, + folderId: { + type: 'string', + description: 'Optional folder ID to place the workflow in.', + }, + description: { + type: 'string', + description: 'Optional description for the workflow.', + }, + }, + required: ['name'], + }, + }, + { + name: 'create_folder', + toolId: 'create_folder', + description: 'Create a new folder in a workspace.', + inputSchema: { + type: 'object', + properties: { + name: { + type: 'string', + description: 'Name for the new folder.', + }, + workspaceId: { + type: 'string', + description: 'Optional workspace ID. Uses default workspace if not provided.', + }, + parentId: { + type: 'string', + description: 'Optional parent folder ID for nested folders.', + }, + }, + required: ['name'], + }, + }, ] const SUBAGENT_TOOL_DEFS: Array<{ @@ -127,28 +323,71 @@ DO NOT USE (use direct tools instead): { name: 'copilot_plan', agentId: 'plan', - description: 'Plan workflow changes by gathering required information.', + description: `Plan workflow changes by gathering required information. + +USE THIS WHEN: +- Building a new workflow +- Modifying an existing workflow +- You need to understand what blocks and integrations are available +- The workflow requires multiple blocks or connections + +WORKFLOW ID (REQUIRED): +- For NEW workflows: First call create_workflow to get a workflowId, then pass it here +- For EXISTING workflows: Always pass the workflowId parameter + +This tool gathers information about available blocks, credentials, and the current workflow state. + +RETURNS: A plan object containing block configurations, connections, and technical details. +IMPORTANT: Pass the returned plan EXACTLY to copilot_edit - do not modify or summarize it.`, inputSchema: { type: 'object', properties: { - request: { type: 'string' }, - workflowId: { type: 'string' }, + request: { type: 'string', description: 'What you want to build or modify in the workflow.' }, + workflowId: { + type: 'string', + description: 'REQUIRED. The workflow ID. For new workflows, call create_workflow first to get this.', + }, context: { type: 'object' }, }, - required: ['request'], + required: ['request', 'workflowId'], }, }, { name: 'copilot_edit', agentId: 'edit', - description: 'Execute a workflow plan and apply edits.', + description: `Execute a workflow plan and apply edits. + +USE THIS WHEN: +- You have a plan from copilot_plan that needs to be executed +- Building or modifying a workflow based on the plan +- Making changes to blocks, connections, or configurations + +WORKFLOW ID (REQUIRED): +- You MUST provide the workflowId parameter +- For new workflows, get the workflowId from create_workflow first + +PLAN (REQUIRED): +- Pass the EXACT plan object from copilot_plan in the context.plan field +- Do NOT modify, summarize, or interpret the plan - pass it verbatim +- The plan contains technical details the edit agent needs exactly as-is + +IMPORTANT: After copilot_edit completes, you MUST call copilot_deploy before the workflow can be run or tested.`, inputSchema: { type: 'object', properties: { - message: { type: 'string' }, - workflowId: { type: 'string' }, - plan: { type: 'object' }, - context: { type: 'object' }, + message: { type: 'string', description: 'Optional additional instructions for the edit.' }, + workflowId: { + type: 'string', + description: 'REQUIRED. The workflow ID to edit. Get this from create_workflow for new workflows.', + }, + plan: { + type: 'object', + description: 'The plan object from copilot_plan. Pass it EXACTLY as returned, do not modify.', + }, + context: { + type: 'object', + description: 'Additional context. Put the plan in context.plan if not using the plan field directly.', + }, }, required: ['workflowId'], }, @@ -156,29 +395,54 @@ DO NOT USE (use direct tools instead): { name: 'copilot_debug', agentId: 'debug', - description: 'Diagnose errors or unexpected workflow behavior.', + description: `Diagnose errors or unexpected workflow behavior. + +WORKFLOW ID (REQUIRED): Always provide the workflowId of the workflow to debug.`, inputSchema: { type: 'object', properties: { - error: { type: 'string' }, - workflowId: { type: 'string' }, + error: { type: 'string', description: 'The error message or description of the issue.' }, + workflowId: { type: 'string', description: 'REQUIRED. The workflow ID to debug.' }, context: { type: 'object' }, }, - required: ['error'], + required: ['error', 'workflowId'], }, }, { name: 'copilot_deploy', agentId: 'deploy', - description: 'Deploy or manage workflow deployments.', + description: `Deploy or manage workflow deployments. + +CRITICAL: You MUST deploy a workflow after building before it can be run or tested. +Workflows without an active deployment will fail with "no active deployment" error. + +WORKFLOW ID (REQUIRED): +- Always provide the workflowId parameter +- This must match the workflow you built with copilot_edit + +USE THIS: +- After copilot_edit completes to activate the workflow +- To update deployment settings +- To redeploy after making changes + +DEPLOYMENT TYPES: +- "deploy as api" - REST API endpoint +- "deploy as chat" - Chat interface +- "deploy as mcp" - MCP server`, inputSchema: { type: 'object', properties: { - request: { type: 'string' }, - workflowId: { type: 'string' }, + request: { + type: 'string', + description: 'The deployment request, e.g. "deploy as api" or "deploy as chat"', + }, + workflowId: { + type: 'string', + description: 'REQUIRED. The workflow ID to deploy.', + }, context: { type: 'object' }, }, - required: ['request'], + required: ['request', 'workflowId'], }, }, { @@ -277,15 +541,29 @@ DO NOT USE (use direct tools instead): { name: 'copilot_test', agentId: 'test', - description: 'Run workflows and verify outputs.', + description: `Run workflows and verify outputs. + +PREREQUISITE: The workflow MUST be deployed first using copilot_deploy. +Undeployed workflows will fail with "no active deployment" error. + +WORKFLOW ID (REQUIRED): +- Always provide the workflowId parameter + +USE THIS: +- After deploying to verify the workflow works correctly +- To test with sample inputs +- To validate workflow behavior before sharing with user`, inputSchema: { type: 'object', properties: { request: { type: 'string' }, - workflowId: { type: 'string' }, + workflowId: { + type: 'string', + description: 'REQUIRED. The workflow ID to test.', + }, context: { type: 'object' }, }, - required: ['request'], + required: ['request', 'workflowId'], }, }, { @@ -355,7 +633,8 @@ export async function POST(request: NextRequest) { const result: InitializeResult = { protocolVersion: '2024-11-05', capabilities: { tools: {} }, - serverInfo: { name: 'copilot-subagents', version: '1.0.0' }, + serverInfo: { name: 'sim-copilot', version: '1.0.0' }, + instructions: MCP_SERVER_INSTRUCTIONS, } return NextResponse.json(createResponse(id, result)) } @@ -391,9 +670,9 @@ async function handleToolsList(id: RequestId): Promise { })) const subagentTools = SUBAGENT_TOOL_DEFS.map((tool) => ({ - name: tool.name, - description: tool.description, - inputSchema: tool.inputSchema, + name: tool.name, + description: tool.description, + inputSchema: tool.inputSchema, })) const result: ListToolsResult = { @@ -416,17 +695,17 @@ async function handleToolsCall( return handleDirectToolCall(id, directTool, args, userId) } - // Check if this is a subagent tool (slower, uses LLM) + // Check if this is a subagent tool (uses LLM orchestration) const subagentTool = SUBAGENT_TOOL_DEFS.find((tool) => tool.name === params.name) if (subagentTool) { return handleSubagentToolCall(id, subagentTool, args, userId) } - return NextResponse.json( - createError(id, ErrorCode.MethodNotFound, `Tool not found: ${params.name}`), - { status: 404 } - ) - } + return NextResponse.json( + createError(id, ErrorCode.MethodNotFound, `Tool not found: ${params.name}`), + { status: 404 } + ) +} async function handleDirectToolCall( id: RequestId, @@ -494,6 +773,9 @@ async function handleSubagentToolCall( workspaceId: args.workspaceId, context, model, + // Signal to the copilot backend that this is a headless request + // so it can enforce workflowId requirements on tools + headless: true, }, { userId, diff --git a/apps/sim/lib/copilot/orchestrator/sse-handlers.ts b/apps/sim/lib/copilot/orchestrator/sse-handlers.ts index d65da8e89..c738674be 100644 --- a/apps/sim/lib/copilot/orchestrator/sse-handlers.ts +++ b/apps/sim/lib/copilot/orchestrator/sse-handlers.ts @@ -65,6 +65,20 @@ async function executeToolAndReport( toolCall.error = result.error toolCall.endTime = Date.now() + // If create_workflow was successful, update the execution context with the new workflowId + // This ensures subsequent tools in the same stream have access to the workflowId + if ( + toolCall.name === 'create_workflow' && + result.success && + result.output?.workflowId && + !execContext.workflowId + ) { + execContext.workflowId = result.output.workflowId + if (result.output.workspaceId) { + execContext.workspaceId = result.output.workspaceId + } + } + await markToolComplete( toolCall.id, toolCall.name, diff --git a/apps/sim/lib/copilot/orchestrator/tool-executor.ts b/apps/sim/lib/copilot/orchestrator/tool-executor.ts index e2611c90d..e7ec7717a 100644 --- a/apps/sim/lib/copilot/orchestrator/tool-executor.ts +++ b/apps/sim/lib/copilot/orchestrator/tool-executor.ts @@ -121,7 +121,14 @@ async function executeServerToolDirect( context: ExecutionContext ): Promise { try { - const result = await routeExecution(toolName, params, { userId: context.userId }) + // Inject workflowId from context if not provided in params + // This is needed for tools like set_environment_variables that require workflowId + const enrichedParams = { ...params } + if (!enrichedParams.workflowId && context.workflowId) { + enrichedParams.workflowId = context.workflowId + } + + const result = await routeExecution(toolName, enrichedParams, { userId: context.userId }) return { success: true, output: result } } catch (error) { logger.error('Server tool execution failed', { diff --git a/apps/sim/lib/copilot/tools/server/workflow/edit-workflow.ts b/apps/sim/lib/copilot/tools/server/workflow/edit-workflow.ts index 3e5d63662..dcb3baf80 100644 --- a/apps/sim/lib/copilot/tools/server/workflow/edit-workflow.ts +++ b/apps/sim/lib/copilot/tools/server/workflow/edit-workflow.ts @@ -1401,6 +1401,101 @@ function filterDisallowedTools( return allowedTools } +/** + * Normalizes block IDs in operations to ensure they are valid UUIDs. + * The LLM may generate human-readable IDs like "web_search" or "research_agent" + * which need to be converted to proper UUIDs for database compatibility. + * + * Returns the normalized operations and a mapping from old IDs to new UUIDs. + */ +function normalizeBlockIdsInOperations(operations: EditWorkflowOperation[]): { + normalizedOperations: EditWorkflowOperation[] + idMapping: Map +} { + const logger = createLogger('EditWorkflowServerTool') + const idMapping = new Map() + + // First pass: collect all non-UUID block_ids from add/insert operations + for (const op of operations) { + if (op.operation_type === 'add' || op.operation_type === 'insert_into_subflow') { + if (op.block_id && !UUID_REGEX.test(op.block_id)) { + const newId = crypto.randomUUID() + idMapping.set(op.block_id, newId) + logger.debug('Normalizing block ID', { oldId: op.block_id, newId }) + } + } + } + + if (idMapping.size === 0) { + return { normalizedOperations: operations, idMapping } + } + + logger.info('Normalizing block IDs in operations', { + normalizedCount: idMapping.size, + mappings: Object.fromEntries(idMapping), + }) + + // Helper to replace an ID if it's in the mapping + const replaceId = (id: string | undefined): string | undefined => { + if (!id) return id + return idMapping.get(id) ?? id + } + + // Second pass: update all references to use new UUIDs + const normalizedOperations = operations.map((op) => { + const normalized: EditWorkflowOperation = { + ...op, + block_id: replaceId(op.block_id) ?? op.block_id, + } + + if (op.params) { + normalized.params = { ...op.params } + + // Update subflowId references (for insert_into_subflow) + if (normalized.params.subflowId) { + normalized.params.subflowId = replaceId(normalized.params.subflowId) + } + + // Update connection references + if (normalized.params.connections) { + const normalizedConnections: Record = {} + for (const [handle, targets] of Object.entries(normalized.params.connections)) { + if (typeof targets === 'string') { + normalizedConnections[handle] = replaceId(targets) + } else if (Array.isArray(targets)) { + normalizedConnections[handle] = targets.map((t) => { + if (typeof t === 'string') return replaceId(t) + if (t && typeof t === 'object' && t.block) { + return { ...t, block: replaceId(t.block) } + } + return t + }) + } else if (targets && typeof targets === 'object' && (targets as any).block) { + normalizedConnections[handle] = { ...targets, block: replaceId((targets as any).block) } + } else { + normalizedConnections[handle] = targets + } + } + normalized.params.connections = normalizedConnections + } + + // Update nestedNodes block IDs + if (normalized.params.nestedNodes) { + const normalizedNestedNodes: Record = {} + for (const [childId, childBlock] of Object.entries(normalized.params.nestedNodes)) { + const newChildId = replaceId(childId) ?? childId + normalizedNestedNodes[newChildId] = childBlock + } + normalized.params.nestedNodes = normalizedNestedNodes + } + } + + return normalized + }) + + return { normalizedOperations, idMapping } +} + /** * Apply operations directly to the workflow JSON state */ @@ -1420,6 +1515,11 @@ function applyOperationsToWorkflowState( // Log initial state const logger = createLogger('EditWorkflowServerTool') + + // Normalize block IDs to UUIDs before processing + const { normalizedOperations } = normalizeBlockIdsInOperations(operations) + operations = normalizedOperations + logger.info('Applying operations to workflow:', { totalOperations: operations.length, operationTypes: operations.reduce((acc: any, op) => {