Skip to content

Commit cb243ff

Browse files
committed
make MCP servers workspace-scoped
1 parent 398ea74 commit cb243ff

32 files changed

Lines changed: 572 additions & 392 deletions

File tree

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

Lines changed: 40 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
1-
import { and, eq } from 'drizzle-orm'
1+
import { and, eq, isNull } from 'drizzle-orm'
22
import { type NextRequest, NextResponse } from 'next/server'
3-
import { getSession } from '@/lib/auth'
3+
import { checkHybridAuth } from '@/lib/auth/hybrid'
44
import { createLogger } from '@/lib/logs/console/logger'
55
import { mcpService } from '@/lib/mcp/service'
66
import type { McpApiResponse } from '@/lib/mcp/types'
7+
import { getUserEntityPermissions } from '@/lib/permissions/utils'
78
import { generateRequestId } from '@/lib/utils'
89
import { db } from '@/db'
910
import { mcpServers } from '@/db/schema'
@@ -13,41 +14,63 @@ const logger = createLogger('McpServerRefreshAPI')
1314
export const dynamic = 'force-dynamic'
1415

1516
/**
16-
* POST - Refresh an MCP server connection
17+
* POST - Refresh an MCP server connection (requires any workspace permission)
1718
*/
18-
export async function POST(_request: NextRequest, { params }: { params: { id: string } }) {
19+
export async function POST(request: NextRequest, { params }: { params: { id: string } }) {
1920
const requestId = generateRequestId()
2021
const serverId = params.id
2122

2223
try {
23-
logger.info(`[${requestId}] Refreshing MCP server: ${serverId}`)
24-
25-
const session = await getSession()
26-
if (!session) {
24+
const auth = await checkHybridAuth(request, { requireWorkflowId: false })
25+
if (!auth.success || !auth.userId) {
2726
return NextResponse.json(
2827
{
2928
success: false,
30-
error: 'Authentication required',
29+
error: auth.error || 'Authentication required',
3130
},
3231
{ status: 401 }
3332
)
3433
}
3534

36-
const userId = session.user.id
37-
if (!userId) {
35+
const { searchParams } = new URL(request.url)
36+
const workspaceId = searchParams.get('workspaceId')
37+
38+
if (!workspaceId) {
3839
return NextResponse.json(
3940
{
4041
success: false,
41-
error: 'Authentication required',
42+
error: 'workspaceId parameter is required',
4243
},
43-
{ status: 401 }
44+
{ status: 400 }
4445
)
4546
}
4647

48+
// Validate user has permission to refresh MCP servers in this workspace (any permission level)
49+
const hasWorkspaceAccess = await getUserEntityPermissions(auth.userId, 'workspace', workspaceId)
50+
if (!hasWorkspaceAccess) {
51+
return NextResponse.json(
52+
{
53+
success: false,
54+
error: 'Access denied to workspace',
55+
},
56+
{ status: 403 }
57+
)
58+
}
59+
60+
logger.info(`[${requestId}] Refreshing MCP server: ${serverId} in workspace: ${workspaceId}`, {
61+
userId: auth.userId,
62+
})
63+
4764
const [server] = await db
4865
.select()
4966
.from(mcpServers)
50-
.where(and(eq(mcpServers.id, serverId), eq(mcpServers.userId, userId)))
67+
.where(
68+
and(
69+
eq(mcpServers.id, serverId),
70+
eq(mcpServers.workspaceId, workspaceId),
71+
isNull(mcpServers.deletedAt)
72+
)
73+
)
5174
.limit(1)
5275

5376
if (!server) {
@@ -65,7 +88,7 @@ export async function POST(_request: NextRequest, { params }: { params: { id: st
6588
let lastError: string | null = null
6689

6790
try {
68-
const tools = await mcpService.discoverServerTools(userId, serverId, true) // Force refresh
91+
const tools = await mcpService.discoverServerTools(auth.userId, serverId, workspaceId, true) // Force refresh
6992
connectionStatus = 'connected'
7093
toolCount = tools.length
7194
logger.info(
@@ -84,6 +107,7 @@ export async function POST(_request: NextRequest, { params }: { params: { id: st
84107
connectionStatus,
85108
lastError,
86109
lastConnected: connectionStatus === 'connected' ? new Date() : server.lastConnected,
110+
toolCount,
87111
updatedAt: new Date(),
88112
})
89113
.where(eq(mcpServers.id, serverId))
@@ -94,7 +118,7 @@ export async function POST(_request: NextRequest, { params }: { params: { id: st
94118
data: {
95119
status: connectionStatus,
96120
toolCount,
97-
lastConnected: refreshedServer.lastConnected?.toISOString() || null,
121+
lastConnected: refreshedServer?.lastConnected?.toISOString() || null,
98122
error: lastError,
99123
},
100124
}

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

Lines changed: 52 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
1-
import { and, eq } from 'drizzle-orm'
1+
import { and, eq, isNull } from 'drizzle-orm'
22
import { type NextRequest, NextResponse } from 'next/server'
3-
import { getSession } from '@/lib/auth'
3+
import { checkHybridAuth } from '@/lib/auth/hybrid'
44
import { createLogger } from '@/lib/logs/console/logger'
5+
import { mcpService } from '@/lib/mcp/service'
56
import type { McpApiResponse } from '@/lib/mcp/types'
67
import { validateMcpServerUrl } from '@/lib/mcp/url-validator'
8+
import { getUserEntityPermissions } from '@/lib/permissions/utils'
79
import { generateRequestId } from '@/lib/utils'
810
import { db } from '@/db'
911
import { mcpServers } from '@/db/schema'
@@ -13,39 +15,62 @@ const logger = createLogger('McpServerAPI')
1315
export const dynamic = 'force-dynamic'
1416

1517
/**
16-
* PATCH - Update an MCP server for the current user
18+
* PATCH - Update an MCP server in the workspace (requires write or admin permission)
1719
*/
1820
export async function PATCH(request: NextRequest, { params }: { params: { id: string } }) {
1921
const requestId = generateRequestId()
2022
const serverId = params.id
2123

2224
try {
25+
const auth = await checkHybridAuth(request, { requireWorkflowId: false })
26+
if (!auth.success || !auth.userId) {
27+
return NextResponse.json(
28+
{
29+
success: false,
30+
error: auth.error || 'Authentication required',
31+
},
32+
{ status: 401 }
33+
)
34+
}
35+
2336
const body = await request.json()
24-
logger.info(`[${requestId}] Updating MCP server: ${serverId}`)
37+
const { workspaceId } = body
2538

26-
const session = await getSession()
27-
if (!session) {
39+
if (!workspaceId) {
2840
return NextResponse.json(
2941
{
3042
success: false,
31-
error: 'Authentication required',
43+
error: 'workspaceId is required',
3244
},
33-
{ status: 401 }
45+
{ status: 400 }
3446
)
3547
}
3648

37-
const userId = session.user.id
38-
if (!userId) {
49+
// Validate user has write or admin permission to update MCP servers in this workspace
50+
const userPermissions = await getUserEntityPermissions(auth.userId, 'workspace', workspaceId)
51+
if (!userPermissions || (userPermissions !== 'write' && userPermissions !== 'admin')) {
3952
return NextResponse.json(
4053
{
4154
success: false,
42-
error: 'Authentication required',
55+
error:
56+
'Insufficient permissions - write or admin permission required to update MCP servers',
4357
},
44-
{ status: 401 }
58+
{ status: 403 }
4559
)
4660
}
4761

48-
if (body.url && (body.transport === 'http' || body.transport === 'sse')) {
62+
logger.info(`[${requestId}] Updating MCP server: ${serverId} in workspace: ${workspaceId}`, {
63+
userId: auth.userId,
64+
updates: Object.keys(body).filter((k) => k !== 'workspaceId'),
65+
})
66+
67+
// Validate URL if being updated
68+
if (
69+
body.url &&
70+
(body.transport === 'http' ||
71+
body.transport === 'sse' ||
72+
body.transport === 'streamable-http')
73+
) {
4974
const urlValidation = validateMcpServerUrl(body.url)
5075
if (!urlValidation.isValid) {
5176
return NextResponse.json(
@@ -59,13 +84,22 @@ export async function PATCH(request: NextRequest, { params }: { params: { id: st
5984
body.url = urlValidation.normalizedUrl
6085
}
6186

87+
// Remove workspaceId from body to prevent it from being updated
88+
const { workspaceId: _, ...updateData } = body
89+
6290
const [updatedServer] = await db
6391
.update(mcpServers)
6492
.set({
65-
...body,
93+
...updateData,
6694
updatedAt: new Date(),
6795
})
68-
.where(and(eq(mcpServers.id, serverId), eq(mcpServers.userId, userId)))
96+
.where(
97+
and(
98+
eq(mcpServers.id, serverId),
99+
eq(mcpServers.workspaceId, workspaceId),
100+
isNull(mcpServers.deletedAt)
101+
)
102+
)
69103
.returning()
70104

71105
if (!updatedServer) {
@@ -78,6 +112,9 @@ export async function PATCH(request: NextRequest, { params }: { params: { id: st
78112
)
79113
}
80114

115+
// Clear MCP service cache after update
116+
mcpService.clearCache(workspaceId)
117+
81118
const response: McpApiResponse = {
82119
success: true,
83120
data: { server: updatedServer },

0 commit comments

Comments
 (0)