import { db } from '@sim/db' import { mcpServers } from '@sim/db/schema' import { createLogger } from '@sim/logger' import { toError } from '@sim/utils/errors' import { and, eq, isNull } from 'drizzle-orm' import type { NextRequest } from 'next/server' import { createMcpServerBodySchema, deleteMcpServerByQuerySchema } from '@/lib/api/contracts/mcp' import { validationErrorResponse } from '@/lib/api/server' import { withRouteHandler } from '@/lib/core/utils/with-route-handler' import { mcpBodyReadErrorResponse, readMcpJsonBodyWithLimit, withMcpAuth, } from '@/lib/mcp/middleware' import { performCreateMcpServer, performDeleteMcpServer } from '@/lib/mcp/orchestration' import { createMcpErrorResponse, createMcpSuccessResponse, mcpOrchestrationStatus, } 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 = withRouteHandler( withMcpAuth('read')(async (request: NextRequest, { userId, workspaceId, requestId }) => { try { logger.info(`[${requestId}] Listing MCP servers for workspace ${workspaceId}`) const rows = await db .select() .from(mcpServers) .where(and(eq(mcpServers.workspaceId, workspaceId), isNull(mcpServers.deletedAt))) const servers = rows.map(({ oauthClientSecret: _secret, ...rest }) => ({ ...rest, hasOauthClientSecret: !!_secret, })) 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(toError(error), 'Failed to list MCP servers', 500) } }) ) /** * POST - Register a new MCP server for the workspace (requires write permission) */ export const POST = withRouteHandler( withMcpAuth('write')( async (request: NextRequest, { userId, userName, userEmail, workspaceId, requestId }) => { try { const rawBody = await readMcpJsonBodyWithLimit(request) const parsedBody = createMcpServerBodySchema.safeParse(rawBody) if (!parsedBody.success) { return createMcpErrorResponse(parsedBody.error, 'Invalid request format', 400) } const body = parsedBody.data logger.info(`[${requestId}] Registering MCP server:`, { name: body.name, transport: body.transport, workspaceId, }) const sourceParam = body.source as string | undefined const source = sourceParam === 'settings' || sourceParam === 'tool_input' ? sourceParam : undefined if (!body.url) { return createMcpErrorResponse( new Error('url is required'), 'Missing required parameter', 400 ) } const result = await performCreateMcpServer({ workspaceId, userId, actorName: userName, actorEmail: userEmail, name: body.name, description: body.description, transport: body.transport, url: body.url, headers: body.headers, timeout: body.timeout, retries: body.retries, enabled: body.enabled, source, authType: body.authType, oauthClientId: body.oauthClientId || null, oauthClientIdProvided: body.oauthClientId !== undefined, oauthClientSecret: body.oauthClientSecret, oauthClientSecretProvided: body.oauthClientSecret !== undefined, request, }) if (!result.success || !result.serverId) { return createMcpErrorResponse( new Error(result.error || 'Failed to register MCP server'), result.error || 'Failed to register MCP server', mcpOrchestrationStatus(result.errorCode) ) } logger.info( `[${requestId}] Successfully registered MCP server: ${body.name} (ID: ${result.serverId})` ) return createMcpSuccessResponse( result.updated ? { serverId: result.serverId, updated: true, authType: result.authType } : { serverId: result.serverId, authType: result.authType }, result.updated ? 200 : 201 ) } catch (error) { const bodyErrorResponse = mcpBodyReadErrorResponse(error, request) if (bodyErrorResponse) return bodyErrorResponse logger.error(`[${requestId}] Error registering MCP server:`, error) return createMcpErrorResponse(toError(error), 'Failed to register MCP server', 500) } } ) ) /** * DELETE - Delete an MCP server from the workspace (requires write permission) */ export const DELETE = withRouteHandler( withMcpAuth('write')( async (request: NextRequest, { userId, userName, userEmail, workspaceId, requestId }) => { try { const { searchParams } = new URL(request.url) const queryValidation = deleteMcpServerByQuerySchema.safeParse( Object.fromEntries(searchParams) ) if (!queryValidation.success) return validationErrorResponse(queryValidation.error) const query = queryValidation.data const serverId = query.serverId const sourceParam = query.source const source = sourceParam === 'settings' || sourceParam === 'tool_input' ? sourceParam : undefined 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 result = await performDeleteMcpServer({ workspaceId, userId, actorName: userName, actorEmail: userEmail, serverId, source, request, }) if (!result.success || !result.server) { return createMcpErrorResponse( new Error(result.error || 'Failed to delete MCP server'), result.error || 'Failed to delete MCP server', mcpOrchestrationStatus(result.errorCode) ) } logger.info(`[${requestId}] Successfully deleted MCP server: ${serverId}`) return createMcpSuccessResponse({ message: `Server ${serverId} deleted successfully` }) } catch (error) { logger.error(`[${requestId}] Error deleting MCP server:`, error) return createMcpErrorResponse(toError(error), 'Failed to delete MCP server', 500) } } ) )