Files
wehub-resource-sync d25d482dc2
Publish CLI Package / publish-npm (push) Waiting to run
Publish Python SDK / publish-pypi (push) Waiting to run
Publish TypeScript SDK / publish-npm (push) Waiting to run
CI / Migrate Dev DB (push) Has been skipped
CI / Detect Version (push) Has been cancelled
CI / Migrate DB (push) Has been cancelled
CI / Build Dev ECR (./docker/app.Dockerfile, ECR_APP) (push) Has been cancelled
CI / Build Dev ECR (./docker/db.Dockerfile, ECR_MIGRATIONS) (push) Has been cancelled
CI / Build Dev ECR (./docker/pii.Dockerfile, ECR_PII) (push) Has been cancelled
CI / Build Dev ECR (./docker/realtime.Dockerfile, ECR_REALTIME) (push) Has been cancelled
CI / Deploy Trigger.dev (Dev) (push) Has been cancelled
CI / Build AMD64 (./docker/app.Dockerfile, ECR_APP, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build AMD64 (./docker/db.Dockerfile, ECR_MIGRATIONS, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build AMD64 (./docker/pii.Dockerfile, ECR_PII, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build AMD64 (./docker/realtime.Dockerfile, ECR_REALTIME, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/app.Dockerfile, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/db.Dockerfile, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/pii.Dockerfile, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/realtime.Dockerfile, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Check Docs Changes (push) Has been cancelled
CI / Process Docs (push) Has been cancelled
CI / Create GitHub Release (push) Has been cancelled
CI / Test and Build (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:20:55 +08:00

193 lines
6.6 KiB
TypeScript

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)
}
}
)
)