This commit is contained in:
Siddharth Ganesan
2026-01-31 12:26:06 -08:00
parent 3d57125993
commit 034819a7c1
4 changed files with 434 additions and 31 deletions

View File

@@ -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: \`<blockname.field>\`
Reference environment variables: \`{{ENV_VAR_NAME}}\`
Rules:
- Block names must be lowercase, no spaces, no special characters
- Use dot notation for nested fields: \`<blockname.field.subfield>\`
---
## 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": <plan_object> } }\`
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": <EXACT plan object from step 1> }
})
\`\`\`
**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<NextResponse> {
}))
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,

View File

@@ -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,

View File

@@ -121,7 +121,14 @@ async function executeServerToolDirect(
context: ExecutionContext
): Promise<ToolCallResult> {
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', {

View File

@@ -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<string, string>
} {
const logger = createLogger('EditWorkflowServerTool')
const idMapping = new Map<string, string>()
// 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<string, any> = {}
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<string, any> = {}
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) => {