feat: 添加 Admin 管理后台
- 数据库新增 Role 枚举、disabled 字段和 McpCallLog 调用日志表 - 后端新增 requireAdmin 中间件和 /api/admin/* 管理接口(统计、用户、项目、日志) - MCP 工具调用自动记录详细日志(耗时、参数、响应大小、客户端IP、token估算) - 前端新增 /admin 路由区域:仪表盘、用户管理、项目管理、调用日志四个页面 - JWT 携带 role 字段,登录/OAuth 增加禁用账号检查 - nginx 配置补充 X-Forwarded-For 透传真实客户端 IP Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -40,7 +40,9 @@ app.post('/mcp/:projectId', mcpAuth, async (req, res) => {
|
||||
}
|
||||
};
|
||||
|
||||
const server = createMcpServer(projectId);
|
||||
const forwarded = req.headers['x-forwarded-for'] as string | undefined;
|
||||
const clientIp = forwarded?.split(',')[0].trim() || req.socket.remoteAddress || '';
|
||||
const server = createMcpServer(projectId, clientIp);
|
||||
await server.connect(transport);
|
||||
await transport.handleRequest(req, res, req.body);
|
||||
});
|
||||
|
||||
45
packages/mcp/src/lib/call-logger.ts
Normal file
45
packages/mcp/src/lib/call-logger.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import { prisma } from '@agent-fox/shared';
|
||||
|
||||
type CallContext = {
|
||||
projectId: string;
|
||||
toolName: string;
|
||||
requestParams: Record<string, unknown>;
|
||||
clientIp: string;
|
||||
};
|
||||
|
||||
export async function logMcpCall(ctx: CallContext, fn: () => Promise<any>): Promise<any> {
|
||||
const start = Date.now();
|
||||
let success = true;
|
||||
let result: any;
|
||||
|
||||
try {
|
||||
result = await fn();
|
||||
if (result?.isError) success = false;
|
||||
return result;
|
||||
} catch (err) {
|
||||
success = false;
|
||||
throw err;
|
||||
} finally {
|
||||
const durationMs = Date.now() - start;
|
||||
const responseText = result ? JSON.stringify(result) : '';
|
||||
const responseSize = Buffer.byteLength(responseText, 'utf-8');
|
||||
// Rough token estimate: ~4 chars per token
|
||||
const estimatedTokens = Math.ceil(responseText.length / 4);
|
||||
|
||||
// Fire-and-forget: don't block the response
|
||||
prisma.mcpCallLog.create({
|
||||
data: {
|
||||
projectId: ctx.projectId,
|
||||
toolName: ctx.toolName,
|
||||
durationMs,
|
||||
success,
|
||||
requestParams: ctx.requestParams as any,
|
||||
responseSize,
|
||||
clientIp: ctx.clientIp,
|
||||
estimatedTokens,
|
||||
},
|
||||
}).catch((err) => {
|
||||
console.error('Failed to log MCP call:', err);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -5,39 +5,43 @@ import { listModules } from './tools/list-modules.js';
|
||||
import { listEndpoints } from './tools/list-endpoints.js';
|
||||
import { getEndpointDetail } from './tools/get-endpoint-detail.js';
|
||||
import { searchEndpoints } from './tools/search-endpoints.js';
|
||||
import { logMcpCall } from './lib/call-logger.js';
|
||||
|
||||
export function createMcpServer(projectId: string): McpServer {
|
||||
export function createMcpServer(projectId: string, clientIp: string = ''): McpServer {
|
||||
const server = new McpServer({
|
||||
name: 'agent-fox',
|
||||
version: '0.1.0',
|
||||
});
|
||||
|
||||
const ctx = (toolName: string, requestParams: Record<string, unknown> = {}) =>
|
||||
({ projectId, toolName, requestParams, clientIp });
|
||||
|
||||
server.tool(
|
||||
'get_project_overview',
|
||||
'Get an overview of this API project including its name, version, base URL, and a summary of available modules with endpoint counts. Call this first to understand what the API offers.',
|
||||
{},
|
||||
async () => getProjectOverview(projectId),
|
||||
async () => logMcpCall(ctx('get_project_overview'), () => getProjectOverview(projectId)),
|
||||
);
|
||||
|
||||
server.tool(
|
||||
'list_modules',
|
||||
'List all API modules/groups with their descriptions. Each module contains related endpoints. Use this when you need module descriptions to decide which module to explore.',
|
||||
{},
|
||||
async () => listModules(projectId),
|
||||
async () => logMcpCall(ctx('list_modules'), () => listModules(projectId)),
|
||||
);
|
||||
|
||||
server.tool(
|
||||
'list_endpoints',
|
||||
'List all endpoints in a specific module. Returns method, path, and summary for each endpoint. Use get_endpoint_detail to get full information about a specific endpoint.',
|
||||
{ moduleId: z.string().describe('The module ID to list endpoints for. Get module IDs from get_project_overview or list_modules.') },
|
||||
async ({ moduleId }) => listEndpoints(projectId, moduleId),
|
||||
async ({ moduleId }) => logMcpCall(ctx('list_endpoints', { moduleId }), () => listEndpoints(projectId, moduleId)),
|
||||
);
|
||||
|
||||
server.tool(
|
||||
'get_endpoint_detail',
|
||||
'Get complete details for a specific endpoint including parameters, request body schema, response schemas. Use this when you need to understand exactly how to call an endpoint.',
|
||||
{ endpointId: z.string().describe('The endpoint ID. Get endpoint IDs from list_endpoints or search_endpoints.') },
|
||||
async ({ endpointId }) => getEndpointDetail(projectId, endpointId),
|
||||
async ({ endpointId }) => logMcpCall(ctx('get_endpoint_detail', { endpointId }), () => getEndpointDetail(projectId, endpointId)),
|
||||
);
|
||||
|
||||
server.tool(
|
||||
@@ -47,7 +51,7 @@ export function createMcpServer(projectId: string): McpServer {
|
||||
keyword: z.string().describe('Search keyword to match against endpoint path, summary, description, and operationId.'),
|
||||
moduleId: z.string().optional().describe('Optional module ID to limit search scope. Omit to search all modules.'),
|
||||
},
|
||||
async ({ keyword, moduleId }) => searchEndpoints(projectId, keyword, moduleId),
|
||||
async ({ keyword, moduleId }) => logMcpCall(ctx('search_endpoints', { keyword, moduleId }), () => searchEndpoints(projectId, keyword, moduleId)),
|
||||
);
|
||||
|
||||
return server;
|
||||
|
||||
Reference in New Issue
Block a user