mirror of
https://github.com/simstudioai/sim.git
synced 2026-02-18 18:25:14 -05:00
* feat(audit-log): add audit events for templates, billing, credentials, env, deployments, passwords * improvement(audit-log): add actorName/actorEmail to all recordAudit calls * fix(audit-log): resolve user for password reset, add CREDENTIAL_SET_INVITATION_RESENT action * fix(audit-log): add workspaceId to deployment activation audit * improvement(audit-log): use better-auth callback for password reset audit, remove cast - Move password reset audit to onPasswordReset callback in auth config instead of coupling to better-auth's verification table internals - Remove ugly double-cast on workflowData.workspaceId in deployment activation * fix(audit-log): add missing actorName/actorEmail to workflow duplicate * improvement(audit-log): add resourceName to credential set invitation accept
251 lines
7.8 KiB
TypeScript
251 lines
7.8 KiB
TypeScript
import { db } from '@sim/db'
|
|
import { mcpServers } from '@sim/db/schema'
|
|
import { createLogger } from '@sim/logger'
|
|
import { and, eq, isNull } from 'drizzle-orm'
|
|
import type { NextRequest } from 'next/server'
|
|
import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log'
|
|
import { McpDomainNotAllowedError, validateMcpDomain } from '@/lib/mcp/domain-check'
|
|
import { getParsedBody, withMcpAuth } from '@/lib/mcp/middleware'
|
|
import { mcpService } from '@/lib/mcp/service'
|
|
import {
|
|
createMcpErrorResponse,
|
|
createMcpSuccessResponse,
|
|
generateMcpServerId,
|
|
} from '@/lib/mcp/utils'
|
|
|
|
const logger = createLogger('McpServersAPI')
|
|
|
|
export const dynamic = 'force-dynamic'
|
|
|
|
/**
|
|
* GET - List all registered MCP servers for the workspace
|
|
*/
|
|
export const GET = withMcpAuth('read')(
|
|
async (request: NextRequest, { userId, workspaceId, requestId }) => {
|
|
try {
|
|
logger.info(`[${requestId}] Listing MCP servers for workspace ${workspaceId}`)
|
|
|
|
const servers = await db
|
|
.select()
|
|
.from(mcpServers)
|
|
.where(and(eq(mcpServers.workspaceId, workspaceId), isNull(mcpServers.deletedAt)))
|
|
|
|
logger.info(
|
|
`[${requestId}] Listed ${servers.length} MCP servers for workspace ${workspaceId}`
|
|
)
|
|
return createMcpSuccessResponse({ servers })
|
|
} catch (error) {
|
|
logger.error(`[${requestId}] Error listing MCP servers:`, error)
|
|
return createMcpErrorResponse(
|
|
error instanceof Error ? error : new Error('Failed to list MCP servers'),
|
|
'Failed to list MCP servers',
|
|
500
|
|
)
|
|
}
|
|
}
|
|
)
|
|
|
|
/**
|
|
* POST - Register a new MCP server for the workspace (requires write permission)
|
|
*
|
|
* Uses deterministic server IDs based on URL hash to ensure that re-adding
|
|
* the same server produces the same ID. This prevents "server not found" errors
|
|
* when workflows reference the old server ID after delete/re-add cycles.
|
|
*
|
|
* If a server with the same ID already exists (same URL in same workspace),
|
|
* it will be updated instead of creating a duplicate.
|
|
*/
|
|
export const POST = withMcpAuth('write')(
|
|
async (request: NextRequest, { userId, userName, userEmail, workspaceId, requestId }) => {
|
|
try {
|
|
const body = getParsedBody(request) || (await request.json())
|
|
|
|
logger.info(`[${requestId}] Registering MCP server:`, {
|
|
name: body.name,
|
|
transport: body.transport,
|
|
workspaceId,
|
|
})
|
|
|
|
if (!body.name || !body.transport) {
|
|
return createMcpErrorResponse(
|
|
new Error('Missing required fields: name or transport'),
|
|
'Missing required fields',
|
|
400
|
|
)
|
|
}
|
|
|
|
try {
|
|
validateMcpDomain(body.url)
|
|
} catch (e) {
|
|
if (e instanceof McpDomainNotAllowedError) {
|
|
return createMcpErrorResponse(e, e.message, 403)
|
|
}
|
|
throw e
|
|
}
|
|
|
|
const serverId = body.url ? generateMcpServerId(workspaceId, body.url) : crypto.randomUUID()
|
|
|
|
const [existingServer] = await db
|
|
.select({ id: mcpServers.id, deletedAt: mcpServers.deletedAt })
|
|
.from(mcpServers)
|
|
.where(and(eq(mcpServers.id, serverId), eq(mcpServers.workspaceId, workspaceId)))
|
|
.limit(1)
|
|
|
|
if (existingServer) {
|
|
logger.info(
|
|
`[${requestId}] Server with ID ${serverId} already exists, updating instead of creating`
|
|
)
|
|
|
|
await db
|
|
.update(mcpServers)
|
|
.set({
|
|
name: body.name,
|
|
description: body.description,
|
|
transport: body.transport,
|
|
url: body.url,
|
|
headers: body.headers || {},
|
|
timeout: body.timeout || 30000,
|
|
retries: body.retries || 3,
|
|
enabled: body.enabled !== false,
|
|
connectionStatus: 'connected',
|
|
lastConnected: new Date(),
|
|
updatedAt: new Date(),
|
|
deletedAt: null,
|
|
})
|
|
.where(eq(mcpServers.id, serverId))
|
|
|
|
await mcpService.clearCache(workspaceId)
|
|
|
|
logger.info(
|
|
`[${requestId}] Successfully updated MCP server: ${body.name} (ID: ${serverId})`
|
|
)
|
|
|
|
return createMcpSuccessResponse({ serverId, updated: true }, 200)
|
|
}
|
|
|
|
await db
|
|
.insert(mcpServers)
|
|
.values({
|
|
id: serverId,
|
|
workspaceId,
|
|
createdBy: userId,
|
|
name: body.name,
|
|
description: body.description,
|
|
transport: body.transport,
|
|
url: body.url,
|
|
headers: body.headers || {},
|
|
timeout: body.timeout || 30000,
|
|
retries: body.retries || 3,
|
|
enabled: body.enabled !== false,
|
|
connectionStatus: 'connected',
|
|
lastConnected: new Date(),
|
|
createdAt: new Date(),
|
|
updatedAt: new Date(),
|
|
})
|
|
.returning()
|
|
|
|
await mcpService.clearCache(workspaceId)
|
|
|
|
logger.info(
|
|
`[${requestId}] Successfully registered MCP server: ${body.name} (ID: ${serverId})`
|
|
)
|
|
|
|
try {
|
|
const { PlatformEvents } = await import('@/lib/core/telemetry')
|
|
PlatformEvents.mcpServerAdded({
|
|
serverId,
|
|
serverName: body.name,
|
|
transport: body.transport,
|
|
workspaceId,
|
|
})
|
|
} catch (_e) {
|
|
// Silently fail
|
|
}
|
|
|
|
recordAudit({
|
|
workspaceId,
|
|
actorId: userId,
|
|
actorName: userName,
|
|
actorEmail: userEmail,
|
|
action: AuditAction.MCP_SERVER_ADDED,
|
|
resourceType: AuditResourceType.MCP_SERVER,
|
|
resourceId: serverId,
|
|
resourceName: body.name,
|
|
description: `Added MCP server "${body.name}"`,
|
|
metadata: { serverName: body.name, transport: body.transport },
|
|
request,
|
|
})
|
|
|
|
return createMcpSuccessResponse({ serverId }, 201)
|
|
} catch (error) {
|
|
logger.error(`[${requestId}] Error registering MCP server:`, error)
|
|
return createMcpErrorResponse(
|
|
error instanceof Error ? error : new Error('Failed to register MCP server'),
|
|
'Failed to register MCP server',
|
|
500
|
|
)
|
|
}
|
|
}
|
|
)
|
|
|
|
/**
|
|
* DELETE - Delete an MCP server from the workspace (requires admin permission)
|
|
*/
|
|
export const DELETE = withMcpAuth('admin')(
|
|
async (request: NextRequest, { userId, userName, userEmail, workspaceId, requestId }) => {
|
|
try {
|
|
const { searchParams } = new URL(request.url)
|
|
const serverId = searchParams.get('serverId')
|
|
|
|
if (!serverId) {
|
|
return createMcpErrorResponse(
|
|
new Error('serverId parameter is required'),
|
|
'Missing required parameter',
|
|
400
|
|
)
|
|
}
|
|
|
|
logger.info(`[${requestId}] Deleting MCP server: ${serverId} from workspace: ${workspaceId}`)
|
|
|
|
const [deletedServer] = await db
|
|
.delete(mcpServers)
|
|
.where(and(eq(mcpServers.id, serverId), eq(mcpServers.workspaceId, workspaceId)))
|
|
.returning()
|
|
|
|
if (!deletedServer) {
|
|
return createMcpErrorResponse(
|
|
new Error('Server not found or access denied'),
|
|
'Server not found',
|
|
404
|
|
)
|
|
}
|
|
|
|
await mcpService.clearCache(workspaceId)
|
|
|
|
logger.info(`[${requestId}] Successfully deleted MCP server: ${serverId}`)
|
|
|
|
recordAudit({
|
|
workspaceId,
|
|
actorId: userId,
|
|
actorName: userName,
|
|
actorEmail: userEmail,
|
|
action: AuditAction.MCP_SERVER_REMOVED,
|
|
resourceType: AuditResourceType.MCP_SERVER,
|
|
resourceId: serverId!,
|
|
resourceName: deletedServer.name,
|
|
description: `Removed MCP server "${deletedServer.name}"`,
|
|
request,
|
|
})
|
|
|
|
return createMcpSuccessResponse({ message: `Server ${serverId} deleted successfully` })
|
|
} catch (error) {
|
|
logger.error(`[${requestId}] Error deleting MCP server:`, error)
|
|
return createMcpErrorResponse(
|
|
error instanceof Error ? error : new Error('Failed to delete MCP server'),
|
|
'Failed to delete MCP server',
|
|
500
|
|
)
|
|
}
|
|
}
|
|
)
|