feat: mcp-connection

This commit is contained in:
Acbox
2026-01-13 23:53:25 +08:00
parent dad44a4c36
commit 83b9e4f09b
8 changed files with 542 additions and 5 deletions
+12 -3
View File
@@ -1,8 +1,16 @@
import { Elysia } from 'elysia'
import { corsMiddleware, errorMiddleware } from './middlewares'
import { agentModule, authModule, modelModule, scheduleModule, settingsModule, userModule } from './modules'
import { memoryModule } from './modules/memory'
import { platformModule } from './modules/platform'
import {
agentModule,
authModule,
modelModule,
scheduleModule,
settingsModule,
userModule,
platformModule,
memoryModule,
mcpModule,
} from './modules'
import openapi from '@elysiajs/openapi'
const port = process.env.API_SERVER_PORT || 7002
@@ -19,6 +27,7 @@ export const app = new Elysia()
.use(settingsModule)
.use(userModule)
.use(platformModule)
.use(mcpModule)
.listen(port)
console.log(
+5 -1
View File
@@ -3,4 +3,8 @@ export * from './auth'
export * from './model'
export * from './schedule'
export * from './settings'
export * from './user'
export * from './user'
export * from './mcp'
export * from './platform'
export * from './schedule'
export * from './memory'
+152
View File
@@ -0,0 +1,152 @@
import Elysia from 'elysia'
import { authMiddleware } from '../../middlewares/auth'
import {
CreateMCPConnectionModel,
UpdateMCPConnectionModel,
GetMCPConnectionByIdModel,
DeleteMCPConnectionModel,
GetMCPConnectionsModel,
} from './model'
import {
getMCPConnections,
getMCPConnection,
createMCPConnection,
updateMCPConnection,
deleteMCPConnection,
} from './service'
export const mcpModule = new Elysia({ prefix: '/mcp' })
.use(authMiddleware)
// Get all MCP connections for current user
.get('/', async ({ user, query }) => {
try {
const page = parseInt(query.page as string) || 1
const limit = parseInt(query.limit as string) || 10
const sortOrder = (query.sortOrder as string) || 'desc'
const result = await getMCPConnections(user.userId, {
page,
limit,
sortOrder: sortOrder as 'asc' | 'desc',
})
return {
success: true,
...result,
}
} catch (error) {
return {
success: false,
error: error instanceof Error ? error.message : 'Failed to fetch MCP connections',
}
}
}, GetMCPConnectionsModel)
// Get MCP connection by ID
.get('/:id', async ({ user, params, set }) => {
try {
const connection = await getMCPConnection(params.id)
if (!connection) {
set.status = 404
return {
success: false,
error: 'MCP connection not found',
}
}
if (connection.user !== user.userId) {
set.status = 403
return {
success: false,
error: 'Forbidden: You do not have permission to access this MCP connection',
}
}
return {
success: true,
data: connection,
}
} catch (error) {
set.status = 500
return {
success: false,
error: error instanceof Error ? error.message : 'Failed to fetch MCP connection',
}
}
}, GetMCPConnectionByIdModel)
// Create new MCP connection
.post('/', async ({ user, body, set }) => {
try {
const newConnection = await createMCPConnection(user.userId, body)
set.status = 201
return {
success: true,
data: newConnection,
}
} catch (error) {
set.status = 500
return {
success: false,
error: error instanceof Error ? error.message : 'Failed to create MCP connection',
}
}
}, CreateMCPConnectionModel)
// Update MCP connection
.put('/:id', async ({ user, params, body, set }) => {
try {
const updatedConnection = await updateMCPConnection(params.id, user.userId, body)
if (!updatedConnection) {
set.status = 404
return {
success: false,
error: 'MCP connection not found',
}
}
return {
success: true,
data: updatedConnection,
}
} catch (error) {
if (error instanceof Error && error.message.includes('Forbidden')) {
set.status = 403
} else {
set.status = 500
}
return {
success: false,
error: error instanceof Error ? error.message : 'Failed to update MCP connection',
}
}
}, UpdateMCPConnectionModel)
// Delete MCP connection
.delete('/:id', async ({ user, params, set }) => {
try {
const deletedConnection = await deleteMCPConnection(params.id, user.userId)
if (!deletedConnection) {
set.status = 404
return {
success: false,
error: 'MCP connection not found',
}
}
return {
success: true,
data: deletedConnection,
}
} catch (error) {
if (error instanceof Error && error.message.includes('Forbidden')) {
set.status = 403
} else {
set.status = 500
}
return {
success: false,
error: error instanceof Error ? error.message : 'Failed to delete MCP connection',
}
}
}, DeleteMCPConnectionModel)
+90
View File
@@ -0,0 +1,90 @@
import { z } from 'zod'
// Stdio MCP 连接配置
const StdioMCPConnectionSchema = z.object({
type: z.literal('stdio'),
name: z.string().min(1, 'Name is required').max(100),
active: z.boolean(),
command: z.string().min(1, 'Command is required'),
args: z.array(z.string()),
env: z.record(z.string(), z.string()),
cwd: z.string(),
})
// HTTP MCP 连接配置
const HTTPMCPConnectionSchema = z.object({
type: z.literal('http'),
name: z.string().min(1, 'Name is required').max(100),
active: z.boolean(),
url: z.string().url('Invalid URL'),
headers: z.record(z.string(), z.string()),
})
// SSE MCP 连接配置
const SSEMCPConnectionSchema = z.object({
type: z.literal('sse'),
name: z.string().min(1, 'Name is required').max(100),
active: z.boolean(),
url: z.string().url('Invalid URL'),
headers: z.record(z.string(), z.string()),
})
// 联合类型
const MCPConnectionConfigSchema = z.union([
StdioMCPConnectionSchema,
HTTPMCPConnectionSchema,
SSEMCPConnectionSchema,
])
// 创建 MCP 连接的 Schema
const CreateMCPConnectionSchema = z.object({
name: z.string().min(1, 'Name is required').max(100),
config: MCPConnectionConfigSchema,
active: z.boolean().default(true),
})
// 更新 MCP 连接的 Schema
const UpdateMCPConnectionSchema = z.object({
name: z.string().min(1).max(100).optional(),
config: MCPConnectionConfigSchema.optional(),
active: z.boolean().optional(),
})
// 查询参数 Schema
const GetMCPConnectionsQuerySchema = z.object({
page: z.string().optional(),
limit: z.string().optional(),
sortOrder: z.enum(['asc', 'desc']).optional(),
})
export type CreateMCPConnectionInput = z.infer<typeof CreateMCPConnectionSchema>
export type UpdateMCPConnectionInput = z.infer<typeof UpdateMCPConnectionSchema>
export type GetMCPConnectionsQuery = z.infer<typeof GetMCPConnectionsQuerySchema>
export const CreateMCPConnectionModel = {
body: CreateMCPConnectionSchema,
}
export const UpdateMCPConnectionModel = {
params: z.object({
id: z.string().uuid('Invalid MCP connection ID format'),
}),
body: UpdateMCPConnectionSchema,
}
export const GetMCPConnectionByIdModel = {
params: z.object({
id: z.string().uuid('Invalid MCP connection ID format'),
}),
}
export const DeleteMCPConnectionModel = {
params: z.object({
id: z.string().uuid('Invalid MCP connection ID format'),
}),
}
export const GetMCPConnectionsModel = {
query: GetMCPConnectionsQuerySchema,
}
+239
View File
@@ -0,0 +1,239 @@
import { db } from '@memoh/db'
import { mcpConnection } from '@memoh/db/schema'
import { eq, desc, asc, sql } from 'drizzle-orm'
import { calculateOffset, createPaginatedResult, type PaginatedResult } from '../../utils/pagination'
import type { CreateMCPConnectionInput, UpdateMCPConnectionInput } from './model'
/**
* MCP Connection 列表返回类型
*/
type MCPConnectionListItem = {
id: string
type: string
name: string
config: unknown
active: boolean
user: string
createdAt: Date
updatedAt: Date
}
/**
* 获取用户的所有 MCP 连接(支持分页)
*/
export const getMCPConnections = async (
userId: string,
params?: {
limit?: number
page?: number
sortOrder?: 'asc' | 'desc'
}
): Promise<PaginatedResult<MCPConnectionListItem>> => {
const limit = params?.limit || 10
const page = params?.page || 1
const sortOrder = params?.sortOrder || 'desc'
const offset = calculateOffset(page, limit)
// 获取总数
const [{ count }] = await db
.select({ count: sql<number>`count(*)` })
.from(mcpConnection)
.where(eq(mcpConnection.user, userId))
// 获取分页数据
const orderFn = sortOrder === 'desc' ? desc : asc
const connections = await db
.select()
.from(mcpConnection)
.where(eq(mcpConnection.user, userId))
.orderBy(orderFn(mcpConnection.id))
.limit(limit)
.offset(offset)
// 类型转换
const formattedConnections = connections.map(conn => ({
id: conn.id,
type: conn.type,
name: conn.name,
config: conn.config,
active: conn.active,
user: conn.user,
createdAt: new Date(),
updatedAt: new Date(),
}))
return createPaginatedResult(formattedConnections, Number(count), page, limit)
}
/**
* 获取用户的所有活跃 MCP 连接
*/
export const getActiveMCPConnections = async (
userId: string
) => {
const connections = await db
.select()
.from(mcpConnection)
.where(eq(mcpConnection.user, userId))
.orderBy(desc(mcpConnection.id))
return connections.filter(conn => conn.active).map(conn => ({
id: conn.id,
type: conn.type,
name: conn.name,
config: conn.config,
active: conn.active,
user: conn.user,
}))
}
/**
* 根据 ID 获取单个 MCP 连接
*/
export const getMCPConnection = async (
connectionId: string
) => {
const [result] = await db
.select()
.from(mcpConnection)
.where(eq(mcpConnection.id, connectionId))
if (!result) {
return null
}
return {
id: result.id,
type: result.type,
name: result.name,
config: result.config,
active: result.active,
user: result.user,
}
}
/**
* 创建新的 MCP 连接
*/
export const createMCPConnection = async (
userId: string,
data: CreateMCPConnectionInput
) => {
const [newConnection] = await db
.insert(mcpConnection)
.values({
user: userId,
type: data.config.type,
name: data.name,
config: data.config,
active: data.active,
})
.returning()
return {
id: newConnection.id,
type: newConnection.type,
name: newConnection.name,
config: newConnection.config,
active: newConnection.active,
user: newConnection.user,
}
}
/**
* 更新 MCP 连接
*/
export const updateMCPConnection = async (
connectionId: string,
userId: string,
data: UpdateMCPConnectionInput
) => {
// 检查 MCP 连接是否存在且属于该用户
const existingConnection = await getMCPConnection(connectionId)
if (!existingConnection) {
return null
}
if (existingConnection.user !== userId) {
throw new Error('Forbidden: You do not have permission to update this MCP connection')
}
const updateData: {
name?: string
config?: unknown
type?: string
active?: boolean
} = {}
if (data.name !== undefined) {
updateData.name = data.name
}
if (data.config !== undefined) {
updateData.config = data.config
updateData.type = data.config.type
}
if (data.active !== undefined) {
updateData.active = data.active
}
const [updatedConnection] = await db
.update(mcpConnection)
.set(updateData)
.where(eq(mcpConnection.id, connectionId))
.returning()
return {
id: updatedConnection.id,
type: updatedConnection.type,
name: updatedConnection.name,
config: updatedConnection.config,
active: updatedConnection.active,
user: updatedConnection.user,
}
}
/**
* 删除 MCP 连接
*/
export const deleteMCPConnection = async (
connectionId: string,
userId: string
) => {
// 检查 MCP 连接是否存在且属于该用户
const existingConnection = await getMCPConnection(connectionId)
if (!existingConnection) {
return null
}
if (existingConnection.user !== userId) {
throw new Error('Forbidden: You do not have permission to delete this MCP connection')
}
const [deletedConnection] = await db
.delete(mcpConnection)
.where(eq(mcpConnection.id, connectionId))
.returning()
return {
id: deletedConnection.id,
type: deletedConnection.type,
name: deletedConnection.name,
config: deletedConnection.config,
active: deletedConnection.active,
user: deletedConnection.user,
}
}
/**
* 设置 MCP 连接的活跃状态
*/
export const setMCPConnectionActive = async (
connectionId: string,
active: boolean
) => {
await db
.update(mcpConnection)
.set({ active })
.where(eq(mcpConnection.id, connectionId))
}
+11
View File
@@ -0,0 +1,11 @@
import { boolean, jsonb, pgTable, text, uuid } from 'drizzle-orm/pg-core'
import { users } from './users'
export const mcpConnection = pgTable('mcp_connection', {
id: uuid('id').primaryKey().defaultRandom(),
type: text('type').notNull(),
name: text('name').notNull(),
config: jsonb('config').notNull(),
active: boolean('active').notNull().default(true),
user: uuid('user').notNull().references(() => users.id),
})
+2 -1
View File
@@ -3,4 +3,5 @@ export * from './model'
export * from './settings'
export * from './schedule'
export * from './users'
export * from './platform'
export * from './platform'
export * from './mcp-connection'
+31
View File
@@ -0,0 +1,31 @@
export interface BaseMCPConnection {
type: string
name: string
active: boolean
}
export interface StdioMCPConnection extends BaseMCPConnection {
type: 'stdio'
command: string
args: string[]
env: Record<string, string>
cwd: string
}
export interface BaseHTTPMCPConnection extends BaseMCPConnection {
url: string
headers: Record<string, string>
}
export interface HTTPMCPConnection extends BaseHTTPMCPConnection {
type: 'http'
}
export interface SSEMCPConnection extends BaseHTTPMCPConnection {
type: 'sse'
}
export type MCPConnection =
| StdioMCPConnection
| HTTPMCPConnection
| SSEMCPConnection