feat: comprehensive refactor of index.js and documentation update

This commit is contained in:
zhangguoyun committed 2026-01-15 11:09:13 +08:00
1 parent 1ea6e0e9f0
commit dd1d86706c
12 files changed
+4520 -68

No files matched your search

+206 -63
View File
@@ -118,78 +118,221 @@ server.registerTool(
)
// 启动服务器,使用标准输入/输出(stdio)与客户端通信
async function main() {
/**
* 解析命令行参数
*/
function parseArguments() {
const args = process.argv.slice(2);
const transportArgIndex = args.indexOf('--transport') !== -1 ? args.indexOf('--transport') : args.indexOf('-t');
const transportType = transportArgIndex !== -1 ? args[transportArgIndex + 1] : 'stdio';
const portArgIndex = args.indexOf('--port') !== -1 ? args.indexOf('--port') : args.indexOf('-p');
const port = portArgIndex !== -1 ? parseInt(args[portArgIndex + 1]) : 8889;
if (transportType === 'http') {
const transport = new StreamableHTTPServerTransport({});
await server.connect(transport);
const httpServer = http.createServer(async (req, res) => {
// 设置 CORS 头
res.setHeader('Access-Control-Allow-Origin', '*');
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization, mcp-session-id');
if (req.method === 'OPTIONS') {
res.writeHead(204);
res.end();
return;
}
if (req.method === 'GET' && req.url === '/health') {
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ status: 'ok' }));
return;
}
const authHeader = req.headers['authorization'];
const token = authHeader && authHeader.startsWith('Bearer ') ? authHeader.slice(7) : null;
const projectId = req.headers['x-apifox-project-id'];
const context = {};
if (token) context.token = token;
if (projectId) context.projectId = projectId;
requestContext.run(context, async () => {
try {
await transport.handleRequest(req, res);
} catch (error) {
console.error("Error handling request:", error);
if (!res.headersSent) {
res.writeHead(500);
res.end("Internal Server Error");
}
}
});
});
httpServer.listen(port, () => {
console.error(`Apifox MCP 服务器已通过 http 启动,监听端口 ${port}`);
console.error(`MCP 服务 endpoint: http://localhost:${port}/sse`);
console.error(`MCP 消息 endpoint: http://localhost:${port}/messages`);
});
} else {
const transport = new StdioServerTransport();
await server.connect(transport);
console.error("Apifox MCP 服务器已通过 stdio 启动"); // 日志输出到 stderr
// 检查是否需要帮助信息
if (args.includes('--help') || args.includes('-h')) {
printHelp();
process.exit(0);
}
// 解析传输方式
const transportArgIndex = args.indexOf('--transport') !== -1
? args.indexOf('--transport')
: args.indexOf('-t');
const transportType = transportArgIndex !== -1
? args[transportArgIndex + 1]
: 'stdio';
// 验证传输方式
if (!['stdio', 'http'].includes(transportType)) {
console.error(`错误: 不支持的传输方式 "${transportType}". 支持的方式: stdio, http`);
process.exit(1);
}
// 解析端口号
const portArgIndex = args.indexOf('--port') !== -1
? args.indexOf('--port')
: args.indexOf('-p');
const port = portArgIndex !== -1
? parseInt(args[portArgIndex + 1])
: 8889;
// 验证端口号
if (isNaN(port) || port < 1 || port > 65535) {
console.error(`错误: 无效的端口号 "${args[portArgIndex + 1]}". 端口号必须在 1-65535 之间`);
process.exit(1);
}
return { transportType, port };
}
/**
* 打印帮助信息
*/
function printHelp() {
console.log(`
Apifox MCP 服务器 - Model Context Protocol 服务器实现
用法:
node index.js [选项]
选项:
-t, --transport <type> 传输方式 (stdio|http) [默认: stdio]
-p, --port <number> HTTP 服务器端口号 [默认: 8889]
-h, --help 显示帮助信息
示例:
# 使用 stdio 传输方式启动 (用于 Claude Desktop 等客户端)
node index.js
node index.js --transport stdio
# 使用 HTTP 传输方式启动 (用于远程访问或调试)
node index.js --transport http
node index.js --transport http --port 3000
环境变量:
APIFOX_TOKEN Apifox 个人访问令牌 (必需)
APIFOX_PROJECT_ID 默认的 Apifox 项目 ID (可选)
HTTP 模式端点:
GET /health 健康检查
POST /sse MCP 服务端点 (Server-Sent Events)
POST /messages MCP 消息端点
HTTP 模式请求头:
APIFOX_TOKEN token (可选,覆盖 APIFOX_TOKEN)
APIFOX_PROJECT_ID 项目 ID (可选,覆盖 APIFOX_PROJECT_ID)
`);
}
/**
* 启动 stdio 传输方式
*/
async function startStdioTransport() {
const transport = new StdioServerTransport();
await server.connect(transport);
// stdio 模式下,日志必须输出到 stderr,避免干扰 stdout 上的 JSON-RPC 消息
console.error("========================================");
console.error("Apifox MCP 服务器已启动");
console.error("传输方式: stdio");
console.error("========================================");
}
/**
* 启动 HTTP 传输方式
*/
async function startHttpTransport(port) {
const transport = new StreamableHTTPServerTransport({});
await server.connect(transport);
const httpServer = http.createServer(async (req, res) => {
// 设置 CORS 头
res.setHeader('Access-Control-Allow-Origin', '*');
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, APIFOX_TOKEN, APIFOX_PROJECT_ID, mcp-session-id');
// 提取认证信息
const token = req.headers['APIFOX_TOKEN'];
const projectId = req.headers['APIFOX_PROJECT_ID'];
// 记录请求日志
const tokenStatus = token ? (token.substring(0, 4) + '***') : 'Missing';
console.log(`[HTTP] ${new Date().toISOString()} | ${req.method} ${req.url} | ProjectId: ${projectId || 'N/A'} | Token: ${tokenStatus}`);
// 处理 CORS 预检请求
if (req.method === 'OPTIONS') {
res.writeHead(204);
res.end();
return;
}
// 健康检查端点
if (req.method === 'GET' && req.url === '/health') {
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({
status: 'ok',
server: 'apifox-mcp-server',
version: '1.0.0',
transport: 'http'
}));
return;
}
// 设置请求上下文
const context = {};
if (token) context.token = token;
if (projectId) context.projectId = projectId;
// 在上下文中处理 MCP 请求
requestContext.run(context, async () => {
try {
await transport.handleRequest(req, res);
console.log(`[HTTP] ${new Date().toISOString()} | Success: ${req.method} ${req.url}`);
} catch (error) {
console.error(`[HTTP] ${new Date().toISOString()} | Error: ${req.method} ${req.url}`, error);
if (!res.headersSent) {
res.writeHead(500, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({
error: 'Internal Server Error',
message: error.message
}));
}
}
});
});
// 启动 HTTP 服务器
return new Promise((resolve, reject) => {
httpServer.listen(port, (err) => {
if (err) {
reject(err);
} else {
console.log("========================================");
console.log("Apifox MCP 服务器已启动");
console.log(`传输方式: HTTP`);
console.log(`监听端口: ${port}`);
console.log(`MCP 服务端点: http://localhost:${port}/sse`);
console.log(`MCP 消息端点: http://localhost:${port}/messages`);
console.log(`健康检查: http://localhost:${port}/health`);
console.log("========================================");
resolve();
}
});
// 处理服务器错误
httpServer.on('error', (error) => {
if (error.code === 'EADDRINUSE') {
console.error(`错误: 端口 ${port} 已被占用`);
} else {
console.error('HTTP 服务器错误:', error);
}
process.exit(1);
});
});
}
/**
* 主函数 - 启动 MCP 服务器
*/
async function main() {
// 解析命令行参数
const { transportType, port } = parseArguments();
// 验证必需的环境变量
if (!process.env.APIFOX_TOKEN) {
console.error("错误: 未设置环境变量 APIFOX_TOKEN");
console.error("请设置 Apifox 个人访问令牌:");
console.error(" export APIFOX_TOKEN=your_token_here");
process.exit(1);
}
// 根据传输方式启动服务器
if (transportType === 'http') {
await startHttpTransport(port);
} else {
await startStdioTransport();
}
}
// 运行主函数,并捕获可能的错误
main().catch((error) => {
console.error("服务器运行出现致命错误:", error);
console.error("服务器启动失败:", error);
process.exit(1);
});