339 lines
9.5 KiB
JavaScript
339 lines
9.5 KiB
JavaScript
#!/usr/bin/env node
|
|
// 引入必要的模块
|
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
import http from "http";
|
|
import { z } from "zod";
|
|
import { AsyncLocalStorage } from 'async_hooks';
|
|
import { ApifoxService } from './apifox.js'
|
|
|
|
const requestContext = new AsyncLocalStorage();
|
|
|
|
// 创建 MCP 服务器实例
|
|
// 为其设置一个名字和版本
|
|
const server = new McpServer(
|
|
{
|
|
name: "apifox-mcp-server",
|
|
version: "1.0.0",
|
|
},
|
|
{
|
|
capabilities: {
|
|
prompts: {},
|
|
tools: {},
|
|
},
|
|
}
|
|
);
|
|
|
|
function getApifoxService() {
|
|
const store = requestContext.getStore();
|
|
const token = store?.token || process.env.APIFOX_TOKEN;
|
|
const projectId = store?.projectId ? Number(store.projectId) : Number(process.env.APIFOX_PROJECT_ID);
|
|
|
|
if (!token) {
|
|
throw new Error("Apifox Token not configured (env: APIFOX_TOKEN or header: Authorization)");
|
|
}
|
|
|
|
return new ApifoxService({
|
|
projectId,
|
|
token
|
|
});
|
|
}
|
|
|
|
server.registerTool(
|
|
'get_api_by_path',
|
|
{
|
|
description: "根据接口path获取Apifox接口定义数据",
|
|
inputSchema: z.object({
|
|
path: z.string().describe("接口路径"),
|
|
projectId: z.number().optional().describe("Apifox项目ID"),
|
|
}),
|
|
},
|
|
|
|
async ({ path, projectId }) => {
|
|
try {
|
|
|
|
const apifoxService = getApifoxService();
|
|
const apiData = await apifoxService.getApiDetailByPath(path, projectId);
|
|
|
|
return {
|
|
content: [
|
|
{
|
|
type: "text",
|
|
text: JSON.stringify(apiData, null, 2),
|
|
},
|
|
],
|
|
};
|
|
|
|
} catch (error) {
|
|
return {
|
|
content: [
|
|
{
|
|
type: "text",
|
|
text: `获取接口详情失败: ${error.message}`,
|
|
},
|
|
],
|
|
isError: true,
|
|
};
|
|
}
|
|
}
|
|
)
|
|
server.registerTool(
|
|
'get_apis_by_folder',
|
|
{
|
|
description: "根据接口目录获取Apifox目录下所有接口定义数据",
|
|
inputSchema: z.object({
|
|
folderName: z.string().describe("接口路径"),
|
|
projectId: z.number().optional().describe("Apifox项目ID"),
|
|
}),
|
|
},
|
|
|
|
async ({ folderName, projectId }) => {
|
|
try {
|
|
|
|
const apifoxService = getApifoxService();
|
|
const apiData = await apifoxService.getApiListByFolderName(folderName, projectId);
|
|
|
|
return {
|
|
content: [
|
|
{
|
|
type: "text",
|
|
text: JSON.stringify(apiData, null, 2),
|
|
},
|
|
],
|
|
};
|
|
|
|
} catch (error) {
|
|
return {
|
|
content: [
|
|
{
|
|
type: "text",
|
|
text: `获取接口数据失败: ${error.message}`,
|
|
},
|
|
],
|
|
isError: true,
|
|
};
|
|
}
|
|
}
|
|
)
|
|
|
|
|
|
/**
|
|
* 解析命令行参数
|
|
*/
|
|
function parseArguments() {
|
|
const args = process.argv.slice(2);
|
|
|
|
// 检查是否需要帮助信息
|
|
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);
|
|
process.exit(1);
|
|
});
|
|
|
|
export { server }; |