Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 2 additions & 4 deletions apps/docs/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,8 @@
"next-env.d.ts",
"**/*.ts",
"**/*.tsx",
".next/types/**/*.ts",
"content/docs/execution/index.mdx",
"content/docs/connections/index.mdx",
".next/dev/types/**/*.ts"
"content/docs/connections/index.mdx"
],
"exclude": ["node_modules"]
"exclude": ["node_modules", ".next"]
}
25 changes: 19 additions & 6 deletions apps/sim/app/api/mcp/serve/[serverId]/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import { createLogger } from '@sim/logger'
import { and, eq } from 'drizzle-orm'
import { type NextRequest, NextResponse } from 'next/server'
import { checkHybridAuth } from '@/lib/auth/hybrid'
import { generateInternalToken } from '@/lib/auth/internal'
import { getBaseUrl } from '@/lib/core/utils/urls'

const logger = createLogger('WorkflowMcpServeAPI')
Expand Down Expand Up @@ -52,6 +53,8 @@ async function getServer(serverId: string) {
id: workflowMcpServer.id,
name: workflowMcpServer.name,
workspaceId: workflowMcpServer.workspaceId,
isPublic: workflowMcpServer.isPublic,
createdBy: workflowMcpServer.createdBy,
})
.from(workflowMcpServer)
.where(eq(workflowMcpServer.id, serverId))
Expand Down Expand Up @@ -90,9 +93,11 @@ export async function POST(request: NextRequest, { params }: { params: Promise<R
return NextResponse.json({ error: 'Server not found' }, { status: 404 })
}

const auth = await checkHybridAuth(request, { requireWorkflowId: false })
if (!auth.success || !auth.userId) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
if (!server.isPublic) {
const auth = await checkHybridAuth(request, { requireWorkflowId: false })
if (!auth.success || !auth.userId) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
}

const body = await request.json()
Expand Down Expand Up @@ -138,7 +143,8 @@ export async function POST(request: NextRequest, { params }: { params: Promise<R
id,
serverId,
rpcParams as { name: string; arguments?: Record<string, unknown> },
apiKey
apiKey,
server.isPublic ? server.createdBy : undefined
)

default:
Expand Down Expand Up @@ -200,7 +206,8 @@ async function handleToolsCall(
id: RequestId,
serverId: string,
params: { name: string; arguments?: Record<string, unknown> } | undefined,
apiKey?: string | null
apiKey?: string | null,
publicServerOwnerId?: string
): Promise<NextResponse> {
try {
if (!params?.name) {
Expand Down Expand Up @@ -243,7 +250,13 @@ async function handleToolsCall(

const executeUrl = `${getBaseUrl()}/api/workflows/${tool.workflowId}/execute`
const headers: Record<string, string> = { 'Content-Type': 'application/json' }
if (apiKey) headers['X-API-Key'] = apiKey

if (publicServerOwnerId) {
const internalToken = await generateInternalToken(publicServerOwnerId)
headers.Authorization = `Bearer ${internalToken}`
} else if (apiKey) {
headers['X-API-Key'] = apiKey
}

logger.info(`Executing workflow ${tool.workflowId} via MCP tool ${params.name}`)

Expand Down
4 changes: 4 additions & 0 deletions apps/sim/app/api/mcp/workflow-servers/[id]/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ export const GET = withMcpAuth<RouteParams>('read')(
createdBy: workflowMcpServer.createdBy,
name: workflowMcpServer.name,
description: workflowMcpServer.description,
isPublic: workflowMcpServer.isPublic,
createdAt: workflowMcpServer.createdAt,
updatedAt: workflowMcpServer.updatedAt,
})
Expand Down Expand Up @@ -98,6 +99,9 @@ export const PATCH = withMcpAuth<RouteParams>('write')(
if (body.description !== undefined) {
updateData.description = body.description?.trim() || null
}
if (body.isPublic !== undefined) {
updateData.isPublic = body.isPublic
}

const [updatedServer] = await db
.update(workflowMcpServer)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@ export const GET = withMcpAuth<RouteParams>('read')(

logger.info(`[${requestId}] Getting tool ${toolId} from server ${serverId}`)

// Verify server exists and belongs to workspace
const [server] = await db
.select({ id: workflowMcpServer.id })
.from(workflowMcpServer)
Expand Down Expand Up @@ -72,7 +71,6 @@ export const PATCH = withMcpAuth<RouteParams>('write')(

logger.info(`[${requestId}] Updating tool ${toolId} in server ${serverId}`)

// Verify server exists and belongs to workspace
const [server] = await db
.select({ id: workflowMcpServer.id })
.from(workflowMcpServer)
Expand Down Expand Up @@ -139,7 +137,6 @@ export const DELETE = withMcpAuth<RouteParams>('write')(

logger.info(`[${requestId}] Deleting tool ${toolId} from server ${serverId}`)

// Verify server exists and belongs to workspace
const [server] = await db
.select({ id: workflowMcpServer.id })
.from(workflowMcpServer)
Expand Down
24 changes: 1 addition & 23 deletions apps/sim/app/api/mcp/workflow-servers/[id]/tools/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,24 +6,10 @@ import type { NextRequest } from 'next/server'
import { getParsedBody, withMcpAuth } from '@/lib/mcp/middleware'
import { createMcpErrorResponse, createMcpSuccessResponse } from '@/lib/mcp/utils'
import { sanitizeToolName } from '@/lib/mcp/workflow-tool-schema'
import { loadWorkflowFromNormalizedTables } from '@/lib/workflows/persistence/utils'
import { hasValidStartBlockInState } from '@/lib/workflows/triggers/trigger-utils'
import { hasValidStartBlock } from '@/lib/workflows/triggers/trigger-utils.server'

const logger = createLogger('WorkflowMcpToolsAPI')

/**
* Check if a workflow has a valid start block by loading from database
*/
async function hasValidStartBlock(workflowId: string): Promise<boolean> {
try {
const normalizedData = await loadWorkflowFromNormalizedTables(workflowId)
return hasValidStartBlockInState(normalizedData)
} catch (error) {
logger.warn('Error checking for start block:', error)
return false
}
}

export const dynamic = 'force-dynamic'

interface RouteParams {
Expand All @@ -40,7 +26,6 @@ export const GET = withMcpAuth<RouteParams>('read')(

logger.info(`[${requestId}] Listing tools for workflow MCP server: ${serverId}`)

// Verify server exists and belongs to workspace
const [server] = await db
.select({ id: workflowMcpServer.id })
.from(workflowMcpServer)
Expand All @@ -53,7 +38,6 @@ export const GET = withMcpAuth<RouteParams>('read')(
return createMcpErrorResponse(new Error('Server not found'), 'Server not found', 404)
}

// Get tools with workflow details
const tools = await db
.select({
id: workflowMcpTool.id,
Expand Down Expand Up @@ -107,7 +91,6 @@ export const POST = withMcpAuth<RouteParams>('write')(
)
}

// Verify server exists and belongs to workspace
const [server] = await db
.select({ id: workflowMcpServer.id })
.from(workflowMcpServer)
Expand All @@ -120,7 +103,6 @@ export const POST = withMcpAuth<RouteParams>('write')(
return createMcpErrorResponse(new Error('Server not found'), 'Server not found', 404)
}

// Verify workflow exists and is deployed
const [workflowRecord] = await db
.select({
id: workflow.id,
Expand All @@ -137,7 +119,6 @@ export const POST = withMcpAuth<RouteParams>('write')(
return createMcpErrorResponse(new Error('Workflow not found'), 'Workflow not found', 404)
}

// Verify workflow belongs to the same workspace
if (workflowRecord.workspaceId !== workspaceId) {
return createMcpErrorResponse(
new Error('Workflow does not belong to this workspace'),
Expand All @@ -154,7 +135,6 @@ export const POST = withMcpAuth<RouteParams>('write')(
)
}

// Verify workflow has a valid start block
const hasStartBlock = await hasValidStartBlock(body.workflowId)
if (!hasStartBlock) {
return createMcpErrorResponse(
Expand All @@ -164,7 +144,6 @@ export const POST = withMcpAuth<RouteParams>('write')(
)
}

// Check if tool already exists for this workflow
const [existingTool] = await db
.select({ id: workflowMcpTool.id })
.from(workflowMcpTool)
Expand All @@ -190,7 +169,6 @@ export const POST = withMcpAuth<RouteParams>('write')(
workflowRecord.description ||
`Execute ${workflowRecord.name} workflow`

// Create the tool
const toolId = crypto.randomUUID()
const [tool] = await db
.insert(workflowMcpTool)
Expand Down
75 changes: 68 additions & 7 deletions apps/sim/app/api/mcp/workflow-servers/route.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import { db } from '@sim/db'
import { workflowMcpServer, workflowMcpTool } from '@sim/db/schema'
import { workflow, workflowMcpServer, workflowMcpTool } from '@sim/db/schema'
import { createLogger } from '@sim/logger'
import { eq, inArray, sql } from 'drizzle-orm'
import type { NextRequest } from 'next/server'
import { getParsedBody, withMcpAuth } from '@/lib/mcp/middleware'
import { createMcpErrorResponse, createMcpSuccessResponse } from '@/lib/mcp/utils'
import { sanitizeToolName } from '@/lib/mcp/workflow-tool-schema'
import { hasValidStartBlock } from '@/lib/workflows/triggers/trigger-utils.server'

const logger = createLogger('WorkflowMcpServersAPI')

Expand All @@ -25,18 +27,18 @@ export const GET = withMcpAuth('read')(
createdBy: workflowMcpServer.createdBy,
name: workflowMcpServer.name,
description: workflowMcpServer.description,
isPublic: workflowMcpServer.isPublic,
createdAt: workflowMcpServer.createdAt,
updatedAt: workflowMcpServer.updatedAt,
toolCount: sql<number>`(
SELECT COUNT(*)::int
FROM "workflow_mcp_tool"
SELECT COUNT(*)::int
FROM "workflow_mcp_tool"
WHERE "workflow_mcp_tool"."server_id" = "workflow_mcp_server"."id"
)`.as('tool_count'),
})
.from(workflowMcpServer)
.where(eq(workflowMcpServer.workspaceId, workspaceId))

// Fetch all tools for these servers
const serverIds = servers.map((s) => s.id)
const tools =
serverIds.length > 0
Expand All @@ -49,7 +51,6 @@ export const GET = withMcpAuth('read')(
.where(inArray(workflowMcpTool.serverId, serverIds))
: []

// Group tool names by server
const toolNamesByServer: Record<string, string[]> = {}
for (const tool of tools) {
if (!toolNamesByServer[tool.serverId]) {
Expand All @@ -58,7 +59,6 @@ export const GET = withMcpAuth('read')(
toolNamesByServer[tool.serverId].push(tool.toolName)
}

// Attach tool names to servers
const serversWithToolNames = servers.map((server) => ({
...server,
toolNames: toolNamesByServer[server.id] || [],
Expand Down Expand Up @@ -90,6 +90,7 @@ export const POST = withMcpAuth('write')(
logger.info(`[${requestId}] Creating workflow MCP server:`, {
name: body.name,
workspaceId,
workflowIds: body.workflowIds,
})

if (!body.name) {
Expand All @@ -110,16 +111,76 @@ export const POST = withMcpAuth('write')(
createdBy: userId,
name: body.name.trim(),
description: body.description?.trim() || null,
isPublic: body.isPublic ?? false,
createdAt: new Date(),
updatedAt: new Date(),
})
.returning()

const workflowIds: string[] = body.workflowIds || []
const addedTools: Array<{ workflowId: string; toolName: string }> = []

if (workflowIds.length > 0) {
const workflows = await db
.select({
id: workflow.id,
name: workflow.name,
description: workflow.description,
isDeployed: workflow.isDeployed,
workspaceId: workflow.workspaceId,
})
.from(workflow)
.where(inArray(workflow.id, workflowIds))

for (const workflowRecord of workflows) {
if (workflowRecord.workspaceId !== workspaceId) {
logger.warn(
`[${requestId}] Skipping workflow ${workflowRecord.id} - does not belong to workspace`
)
continue
}

if (!workflowRecord.isDeployed) {
logger.warn(`[${requestId}] Skipping workflow ${workflowRecord.id} - not deployed`)
continue
}

const hasStartBlock = await hasValidStartBlock(workflowRecord.id)
if (!hasStartBlock) {
logger.warn(`[${requestId}] Skipping workflow ${workflowRecord.id} - no start block`)
continue
}

const toolName = sanitizeToolName(workflowRecord.name)
const toolDescription =
workflowRecord.description || `Execute ${workflowRecord.name} workflow`

const toolId = crypto.randomUUID()
await db.insert(workflowMcpTool).values({
id: toolId,
serverId,
workflowId: workflowRecord.id,
toolName,
toolDescription,
parameterSchema: {},
createdAt: new Date(),
updatedAt: new Date(),
})

addedTools.push({ workflowId: workflowRecord.id, toolName })
}

logger.info(
`[${requestId}] Added ${addedTools.length} tools to server ${serverId}:`,
addedTools.map((t) => t.toolName)
)
}

logger.info(
`[${requestId}] Successfully created workflow MCP server: ${body.name} (ID: ${serverId})`
)

return createMcpSuccessResponse({ server }, 201)
return createMcpSuccessResponse({ server, addedTools }, 201)
} catch (error) {
logger.error(`[${requestId}] Error creating workflow MCP server:`, error)
return createMcpErrorResponse(
Expand Down
Loading