mirror of
https://github.com/memohai/Memoh.git
synced 2026-04-25 07:00:48 +09:00
feat: mcp-connection
This commit is contained in:
@@ -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(
|
||||
|
||||
@@ -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'
|
||||
@@ -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)
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
})
|
||||
@@ -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'
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user