Skip to content

Commit ce3ddb6

Browse files
authored
improvement(deployed-mcp): added the ability to make the visibility for deployed mcp tools public, updated UX (#2853)
* improvement(deployed-mcp): added the ability to make the visibility for deployed mcp tools public, updated UX * use reactquery * migrated chats to use reactquery, upgraded entire deploymodal to use reactquery instead of manual state management * added hooks for chat chats and updated callers to all use reactquery * fix * updated comments * consolidated utils
1 parent 8361931 commit ce3ddb6

File tree

40 files changed

+12761
-1176
lines changed

40 files changed

+12761
-1176
lines changed

apps/docs/tsconfig.json

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,8 @@
1111
"next-env.d.ts",
1212
"**/*.ts",
1313
"**/*.tsx",
14-
".next/types/**/*.ts",
1514
"content/docs/execution/index.mdx",
16-
"content/docs/connections/index.mdx",
17-
".next/dev/types/**/*.ts"
15+
"content/docs/connections/index.mdx"
1816
],
19-
"exclude": ["node_modules"]
17+
"exclude": ["node_modules", ".next"]
2018
}

apps/sim/app/api/mcp/serve/[serverId]/route.ts

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import { createLogger } from '@sim/logger'
2020
import { and, eq } from 'drizzle-orm'
2121
import { type NextRequest, NextResponse } from 'next/server'
2222
import { checkHybridAuth } from '@/lib/auth/hybrid'
23+
import { generateInternalToken } from '@/lib/auth/internal'
2324
import { getBaseUrl } from '@/lib/core/utils/urls'
2425

2526
const logger = createLogger('WorkflowMcpServeAPI')
@@ -52,6 +53,8 @@ async function getServer(serverId: string) {
5253
id: workflowMcpServer.id,
5354
name: workflowMcpServer.name,
5455
workspaceId: workflowMcpServer.workspaceId,
56+
isPublic: workflowMcpServer.isPublic,
57+
createdBy: workflowMcpServer.createdBy,
5558
})
5659
.from(workflowMcpServer)
5760
.where(eq(workflowMcpServer.id, serverId))
@@ -90,9 +93,11 @@ export async function POST(request: NextRequest, { params }: { params: Promise<R
9093
return NextResponse.json({ error: 'Server not found' }, { status: 404 })
9194
}
9295

93-
const auth = await checkHybridAuth(request, { requireWorkflowId: false })
94-
if (!auth.success || !auth.userId) {
95-
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
96+
if (!server.isPublic) {
97+
const auth = await checkHybridAuth(request, { requireWorkflowId: false })
98+
if (!auth.success || !auth.userId) {
99+
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
100+
}
96101
}
97102

98103
const body = await request.json()
@@ -138,7 +143,8 @@ export async function POST(request: NextRequest, { params }: { params: Promise<R
138143
id,
139144
serverId,
140145
rpcParams as { name: string; arguments?: Record<string, unknown> },
141-
apiKey
146+
apiKey,
147+
server.isPublic ? server.createdBy : undefined
142148
)
143149

144150
default:
@@ -200,7 +206,8 @@ async function handleToolsCall(
200206
id: RequestId,
201207
serverId: string,
202208
params: { name: string; arguments?: Record<string, unknown> } | undefined,
203-
apiKey?: string | null
209+
apiKey?: string | null,
210+
publicServerOwnerId?: string
204211
): Promise<NextResponse> {
205212
try {
206213
if (!params?.name) {
@@ -243,7 +250,13 @@ async function handleToolsCall(
243250

244251
const executeUrl = `${getBaseUrl()}/api/workflows/${tool.workflowId}/execute`
245252
const headers: Record<string, string> = { 'Content-Type': 'application/json' }
246-
if (apiKey) headers['X-API-Key'] = apiKey
253+
254+
if (publicServerOwnerId) {
255+
const internalToken = await generateInternalToken(publicServerOwnerId)
256+
headers.Authorization = `Bearer ${internalToken}`
257+
} else if (apiKey) {
258+
headers['X-API-Key'] = apiKey
259+
}
247260

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

apps/sim/app/api/mcp/workflow-servers/[id]/route.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ export const GET = withMcpAuth<RouteParams>('read')(
3131
createdBy: workflowMcpServer.createdBy,
3232
name: workflowMcpServer.name,
3333
description: workflowMcpServer.description,
34+
isPublic: workflowMcpServer.isPublic,
3435
createdAt: workflowMcpServer.createdAt,
3536
updatedAt: workflowMcpServer.updatedAt,
3637
})
@@ -98,6 +99,9 @@ export const PATCH = withMcpAuth<RouteParams>('write')(
9899
if (body.description !== undefined) {
99100
updateData.description = body.description?.trim() || null
100101
}
102+
if (body.isPublic !== undefined) {
103+
updateData.isPublic = body.isPublic
104+
}
101105

102106
const [updatedServer] = await db
103107
.update(workflowMcpServer)

apps/sim/app/api/mcp/workflow-servers/[id]/tools/[toolId]/route.ts

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,6 @@ export const GET = withMcpAuth<RouteParams>('read')(
2626

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

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

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

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

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

142-
// Verify server exists and belongs to workspace
143140
const [server] = await db
144141
.select({ id: workflowMcpServer.id })
145142
.from(workflowMcpServer)

apps/sim/app/api/mcp/workflow-servers/[id]/tools/route.ts

Lines changed: 1 addition & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -6,24 +6,10 @@ import type { NextRequest } from 'next/server'
66
import { getParsedBody, withMcpAuth } from '@/lib/mcp/middleware'
77
import { createMcpErrorResponse, createMcpSuccessResponse } from '@/lib/mcp/utils'
88
import { sanitizeToolName } from '@/lib/mcp/workflow-tool-schema'
9-
import { loadWorkflowFromNormalizedTables } from '@/lib/workflows/persistence/utils'
10-
import { hasValidStartBlockInState } from '@/lib/workflows/triggers/trigger-utils'
9+
import { hasValidStartBlock } from '@/lib/workflows/triggers/trigger-utils.server'
1110

1211
const logger = createLogger('WorkflowMcpToolsAPI')
1312

14-
/**
15-
* Check if a workflow has a valid start block by loading from database
16-
*/
17-
async function hasValidStartBlock(workflowId: string): Promise<boolean> {
18-
try {
19-
const normalizedData = await loadWorkflowFromNormalizedTables(workflowId)
20-
return hasValidStartBlockInState(normalizedData)
21-
} catch (error) {
22-
logger.warn('Error checking for start block:', error)
23-
return false
24-
}
25-
}
26-
2713
export const dynamic = 'force-dynamic'
2814

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

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

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

56-
// Get tools with workflow details
5741
const tools = await db
5842
.select({
5943
id: workflowMcpTool.id,
@@ -107,7 +91,6 @@ export const POST = withMcpAuth<RouteParams>('write')(
10791
)
10892
}
10993

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

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

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

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

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

193-
// Create the tool
194172
const toolId = crypto.randomUUID()
195173
const [tool] = await db
196174
.insert(workflowMcpTool)

apps/sim/app/api/mcp/workflow-servers/route.ts

Lines changed: 68 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
import { db } from '@sim/db'
2-
import { workflowMcpServer, workflowMcpTool } from '@sim/db/schema'
2+
import { workflow, workflowMcpServer, workflowMcpTool } from '@sim/db/schema'
33
import { createLogger } from '@sim/logger'
44
import { eq, inArray, sql } from 'drizzle-orm'
55
import type { NextRequest } from 'next/server'
66
import { getParsedBody, withMcpAuth } from '@/lib/mcp/middleware'
77
import { createMcpErrorResponse, createMcpSuccessResponse } from '@/lib/mcp/utils'
8+
import { sanitizeToolName } from '@/lib/mcp/workflow-tool-schema'
9+
import { hasValidStartBlock } from '@/lib/workflows/triggers/trigger-utils.server'
810

911
const logger = createLogger('WorkflowMcpServersAPI')
1012

@@ -25,18 +27,18 @@ export const GET = withMcpAuth('read')(
2527
createdBy: workflowMcpServer.createdBy,
2628
name: workflowMcpServer.name,
2729
description: workflowMcpServer.description,
30+
isPublic: workflowMcpServer.isPublic,
2831
createdAt: workflowMcpServer.createdAt,
2932
updatedAt: workflowMcpServer.updatedAt,
3033
toolCount: sql<number>`(
31-
SELECT COUNT(*)::int
32-
FROM "workflow_mcp_tool"
34+
SELECT COUNT(*)::int
35+
FROM "workflow_mcp_tool"
3336
WHERE "workflow_mcp_tool"."server_id" = "workflow_mcp_server"."id"
3437
)`.as('tool_count'),
3538
})
3639
.from(workflowMcpServer)
3740
.where(eq(workflowMcpServer.workspaceId, workspaceId))
3841

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

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

61-
// Attach tool names to servers
6262
const serversWithToolNames = servers.map((server) => ({
6363
...server,
6464
toolNames: toolNamesByServer[server.id] || [],
@@ -90,6 +90,7 @@ export const POST = withMcpAuth('write')(
9090
logger.info(`[${requestId}] Creating workflow MCP server:`, {
9191
name: body.name,
9292
workspaceId,
93+
workflowIds: body.workflowIds,
9394
})
9495

9596
if (!body.name) {
@@ -110,16 +111,76 @@ export const POST = withMcpAuth('write')(
110111
createdBy: userId,
111112
name: body.name.trim(),
112113
description: body.description?.trim() || null,
114+
isPublic: body.isPublic ?? false,
113115
createdAt: new Date(),
114116
updatedAt: new Date(),
115117
})
116118
.returning()
117119

120+
const workflowIds: string[] = body.workflowIds || []
121+
const addedTools: Array<{ workflowId: string; toolName: string }> = []
122+
123+
if (workflowIds.length > 0) {
124+
const workflows = await db
125+
.select({
126+
id: workflow.id,
127+
name: workflow.name,
128+
description: workflow.description,
129+
isDeployed: workflow.isDeployed,
130+
workspaceId: workflow.workspaceId,
131+
})
132+
.from(workflow)
133+
.where(inArray(workflow.id, workflowIds))
134+
135+
for (const workflowRecord of workflows) {
136+
if (workflowRecord.workspaceId !== workspaceId) {
137+
logger.warn(
138+
`[${requestId}] Skipping workflow ${workflowRecord.id} - does not belong to workspace`
139+
)
140+
continue
141+
}
142+
143+
if (!workflowRecord.isDeployed) {
144+
logger.warn(`[${requestId}] Skipping workflow ${workflowRecord.id} - not deployed`)
145+
continue
146+
}
147+
148+
const hasStartBlock = await hasValidStartBlock(workflowRecord.id)
149+
if (!hasStartBlock) {
150+
logger.warn(`[${requestId}] Skipping workflow ${workflowRecord.id} - no start block`)
151+
continue
152+
}
153+
154+
const toolName = sanitizeToolName(workflowRecord.name)
155+
const toolDescription =
156+
workflowRecord.description || `Execute ${workflowRecord.name} workflow`
157+
158+
const toolId = crypto.randomUUID()
159+
await db.insert(workflowMcpTool).values({
160+
id: toolId,
161+
serverId,
162+
workflowId: workflowRecord.id,
163+
toolName,
164+
toolDescription,
165+
parameterSchema: {},
166+
createdAt: new Date(),
167+
updatedAt: new Date(),
168+
})
169+
170+
addedTools.push({ workflowId: workflowRecord.id, toolName })
171+
}
172+
173+
logger.info(
174+
`[${requestId}] Added ${addedTools.length} tools to server ${serverId}:`,
175+
addedTools.map((t) => t.toolName)
176+
)
177+
}
178+
118179
logger.info(
119180
`[${requestId}] Successfully created workflow MCP server: ${body.name} (ID: ${serverId})`
120181
)
121182

122-
return createMcpSuccessResponse({ server }, 201)
183+
return createMcpSuccessResponse({ server, addedTools }, 201)
123184
} catch (error) {
124185
logger.error(`[${requestId}] Error creating workflow MCP server:`, error)
125186
return createMcpErrorResponse(

0 commit comments

Comments
 (0)