Skip to content

Commit 8d9ceca

Browse files
committed
improvement(deployed-mcp): added the ability to make the visibility for deployed mcp tools public, updated UX
1 parent fa63af9 commit 8d9ceca

File tree

16 files changed

+11346
-204
lines changed

16 files changed

+11346
-204
lines changed

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: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,6 @@ export const GET = withMcpAuth<RouteParams>('read')(
4040

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

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

56-
// Get tools with workflow details
5755
const tools = await db
5856
.select({
5957
id: workflowMcpTool.id,
@@ -107,7 +105,6 @@ export const POST = withMcpAuth<RouteParams>('write')(
107105
)
108106
}
109107

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

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

140-
// Verify workflow belongs to the same workspace
141136
if (workflowRecord.workspaceId !== workspaceId) {
142137
return createMcpErrorResponse(
143138
new Error('Workflow does not belong to this workspace'),
@@ -154,7 +149,6 @@ export const POST = withMcpAuth<RouteParams>('write')(
154149
)
155150
}
156151

157-
// Verify workflow has a valid start block
158152
const hasStartBlock = await hasValidStartBlock(body.workflowId)
159153
if (!hasStartBlock) {
160154
return createMcpErrorResponse(
@@ -164,7 +158,6 @@ export const POST = withMcpAuth<RouteParams>('write')(
164158
)
165159
}
166160

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

193-
// Create the tool
194186
const toolId = crypto.randomUUID()
195187
const [tool] = await db
196188
.insert(workflowMcpTool)

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

Lines changed: 88 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
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 { loadWorkflowFromNormalizedTables } from '@/lib/workflows/persistence/utils'
10+
import { hasValidStartBlockInState } from '@/lib/workflows/triggers/trigger-utils'
811

912
const logger = createLogger('WorkflowMcpServersAPI')
1013

@@ -25,18 +28,18 @@ export const GET = withMcpAuth('read')(
2528
createdBy: workflowMcpServer.createdBy,
2629
name: workflowMcpServer.name,
2730
description: workflowMcpServer.description,
31+
isPublic: workflowMcpServer.isPublic,
2832
createdAt: workflowMcpServer.createdAt,
2933
updatedAt: workflowMcpServer.updatedAt,
3034
toolCount: sql<number>`(
31-
SELECT COUNT(*)::int
32-
FROM "workflow_mcp_tool"
35+
SELECT COUNT(*)::int
36+
FROM "workflow_mcp_tool"
3337
WHERE "workflow_mcp_tool"."server_id" = "workflow_mcp_server"."id"
3438
)`.as('tool_count'),
3539
})
3640
.from(workflowMcpServer)
3741
.where(eq(workflowMcpServer.workspaceId, workspaceId))
3842

39-
// Fetch all tools for these servers
4043
const serverIds = servers.map((s) => s.id)
4144
const tools =
4245
serverIds.length > 0
@@ -49,7 +52,6 @@ export const GET = withMcpAuth('read')(
4952
.where(inArray(workflowMcpTool.serverId, serverIds))
5053
: []
5154

52-
// Group tool names by server
5355
const toolNamesByServer: Record<string, string[]> = {}
5456
for (const tool of tools) {
5557
if (!toolNamesByServer[tool.serverId]) {
@@ -58,7 +60,6 @@ export const GET = withMcpAuth('read')(
5860
toolNamesByServer[tool.serverId].push(tool.toolName)
5961
}
6062

61-
// Attach tool names to servers
6263
const serversWithToolNames = servers.map((server) => ({
6364
...server,
6465
toolNames: toolNamesByServer[server.id] || [],
@@ -79,6 +80,19 @@ export const GET = withMcpAuth('read')(
7980
}
8081
)
8182

83+
/**
84+
* Check if a workflow has a valid start block by loading from database
85+
*/
86+
async function hasValidStartBlock(workflowId: string): Promise<boolean> {
87+
try {
88+
const normalizedData = await loadWorkflowFromNormalizedTables(workflowId)
89+
return hasValidStartBlockInState(normalizedData)
90+
} catch (error) {
91+
logger.warn('Error checking for start block:', error)
92+
return false
93+
}
94+
}
95+
8296
/**
8397
* POST - Create a new workflow MCP server
8498
*/
@@ -90,6 +104,7 @@ export const POST = withMcpAuth('write')(
90104
logger.info(`[${requestId}] Creating workflow MCP server:`, {
91105
name: body.name,
92106
workspaceId,
107+
workflowIds: body.workflowIds,
93108
})
94109

95110
if (!body.name) {
@@ -110,16 +125,82 @@ export const POST = withMcpAuth('write')(
110125
createdBy: userId,
111126
name: body.name.trim(),
112127
description: body.description?.trim() || null,
128+
isPublic: body.isPublic ?? false,
113129
createdAt: new Date(),
114130
updatedAt: new Date(),
115131
})
116132
.returning()
117133

134+
// If workflowIds are provided, create tools for each workflow
135+
const workflowIds: string[] = body.workflowIds || []
136+
const addedTools: Array<{ workflowId: string; toolName: string }> = []
137+
138+
if (workflowIds.length > 0) {
139+
// Fetch all workflows in one query
140+
const workflows = await db
141+
.select({
142+
id: workflow.id,
143+
name: workflow.name,
144+
description: workflow.description,
145+
isDeployed: workflow.isDeployed,
146+
workspaceId: workflow.workspaceId,
147+
})
148+
.from(workflow)
149+
.where(inArray(workflow.id, workflowIds))
150+
151+
// Create tools for each valid workflow
152+
for (const workflowRecord of workflows) {
153+
// Skip if workflow doesn't belong to this workspace
154+
if (workflowRecord.workspaceId !== workspaceId) {
155+
logger.warn(
156+
`[${requestId}] Skipping workflow ${workflowRecord.id} - does not belong to workspace`
157+
)
158+
continue
159+
}
160+
161+
// Skip if workflow is not deployed
162+
if (!workflowRecord.isDeployed) {
163+
logger.warn(`[${requestId}] Skipping workflow ${workflowRecord.id} - not deployed`)
164+
continue
165+
}
166+
167+
// Skip if workflow doesn't have a start block
168+
const hasStartBlock = await hasValidStartBlock(workflowRecord.id)
169+
if (!hasStartBlock) {
170+
logger.warn(`[${requestId}] Skipping workflow ${workflowRecord.id} - no start block`)
171+
continue
172+
}
173+
174+
const toolName = sanitizeToolName(workflowRecord.name)
175+
const toolDescription =
176+
workflowRecord.description || `Execute ${workflowRecord.name} workflow`
177+
178+
const toolId = crypto.randomUUID()
179+
await db.insert(workflowMcpTool).values({
180+
id: toolId,
181+
serverId,
182+
workflowId: workflowRecord.id,
183+
toolName,
184+
toolDescription,
185+
parameterSchema: {},
186+
createdAt: new Date(),
187+
updatedAt: new Date(),
188+
})
189+
190+
addedTools.push({ workflowId: workflowRecord.id, toolName })
191+
}
192+
193+
logger.info(
194+
`[${requestId}] Added ${addedTools.length} tools to server ${serverId}:`,
195+
addedTools.map((t) => t.toolName)
196+
)
197+
}
198+
118199
logger.info(
119200
`[${requestId}] Successfully created workflow MCP server: ${body.name} (ID: ${serverId})`
120201
)
121202

122-
return createMcpSuccessResponse({ server }, 201)
203+
return createMcpSuccessResponse({ server, addedTools }, 201)
123204
} catch (error) {
124205
logger.error(`[${requestId}] Error creating workflow MCP server:`, error)
125206
return createMcpErrorResponse(

apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/mcp/components/form-field/form-field.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,17 @@ import { Label } from '@/components/emcn'
33
interface FormFieldProps {
44
label: string
55
children: React.ReactNode
6+
optional?: boolean
67
}
78

8-
export function FormField({ label, children }: FormFieldProps) {
9+
export function FormField({ label, children, optional }: FormFieldProps) {
910
return (
1011
<div className='flex items-center justify-between gap-[12px]'>
1112
<Label className='w-[100px] shrink-0 font-medium text-[13px] text-[var(--text-secondary)]'>
1213
{label}
14+
{optional && (
15+
<span className='ml-1 font-normal text-[11px] text-[var(--text-muted)]'>(optional)</span>
16+
)}
1317
</Label>
1418
<div className='relative flex-1'>{children}</div>
1519
</div>

0 commit comments

Comments
 (0)