index.ts•18 kB
#!/usr/bin/env node
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import {
CallToolRequestSchema,
ListToolsRequestSchema,
Tool
} from '@modelcontextprotocol/sdk/types.js';
import { ApifoxClient } from './apifox-client.js';
import { readFileSync } from 'fs';
import { fileURLToPath } from 'url';
import { dirname, join } from 'path';
// 获取 package.json 路径
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const packageJsonPath = join(__dirname, '../package.json');
const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf-8'));
/**
* 解析命令行参数
*/
function parseArgs() {
const args = process.argv.slice(2);
const config: Record<string, string> = {};
for (let i = 0; i < args.length; i++) {
const arg = args[i];
if (arg.startsWith('--')) {
const key = arg.slice(2);
const value = args[i + 1];
if (value && !value.startsWith('--')) {
config[key] = value;
i++;
}
}
}
return config;
}
/**
* 从命令行参数或环境变量获取配置
* 优先级:命令行参数 > 环境变量
*/
function getConfig() {
const cliArgs = parseArgs();
const token = cliArgs['token'] || process.env.APIFOX_TOKEN;
const projectId = cliArgs['project-id'] || process.env.APIFOX_PROJECT_ID;
const baseUrl = cliArgs['base-url'] || process.env.APIFOX_BASE_URL;
if (!token || !projectId) {
console.error('\n❌ 缺少必要的配置参数!\n');
console.error('请通过以下任一方式提供配置:\n');
console.error('方式 1: 命令行参数');
console.error(' node dist/index.js --token "your-token" --project-id "your-project-id"\n');
console.error('方式 2: 环境变量');
console.error(' export APIFOX_TOKEN="your-token"');
console.error(' export APIFOX_PROJECT_ID="your-project-id"');
console.error(' node dist/index.js\n');
console.error('可选参数:');
console.error(' --base-url "https://api.apifox.com" # Apifox API 基础 URL\n');
process.exit(1);
}
return { token, projectId, baseUrl };
}
/**
* 定义 MCP 工具
*/
const tools: Tool[] = [
{
name: 'import_openapi',
description: '维护 Apifox 接口文档:将 OpenAPI/Swagger 规范导入到 Apifox 项目。支持 OpenAPI 3.0/3.1 和 Swagger 2.0 格式。\n\n⭐ 最佳实践(强烈推荐):\n1. 【分批导入】按模块分批导入(如先导入 /api/users,再导入 /api/products),而非一次性导入所有接口\n2. 【智能范围检测】每次只导入一个功能模块的完整规范,系统会自动识别范围\n3. 【启用废弃标记】设置 markDeprecatedEndpoints: true,自动保留并标记已删除的接口\n\n分批导入的好处:更安全、更可控、支持模块独立维护、避免大规模误操作。',
inputSchema: {
type: 'object',
properties: {
spec: {
type: 'object',
description: 'OpenAPI/Swagger 规范的 JSON 对象。必须包含 openapi 或 swagger 字段、info 字段、paths 字段'
},
options: {
type: 'object',
description: '导入选项(可选)',
properties: {
endpointOverwriteBehavior: {
type: 'string',
enum: ['OVERWRITE_EXISTING', 'AUTO_MERGE', 'KEEP_EXISTING', 'CREATE_NEW'],
description: '接口覆盖行为,默认为 OVERWRITE_EXISTING'
},
schemaOverwriteBehavior: {
type: 'string',
enum: ['OVERWRITE_EXISTING', 'AUTO_MERGE', 'KEEP_EXISTING', 'CREATE_NEW'],
description: '数据模型覆盖行为,默认为 OVERWRITE_EXISTING'
},
markDeprecatedEndpoints: {
type: 'boolean',
description: '是否自动标记废弃的接口(推荐启用)。启用后会:1) 导出现有接口;2) 智能检测导入范围(如只导入 /api/marketing 模块);3) 只对比该范围内的接口;4) 标记已删除的接口为 deprecated。支持部分模块导入,不会误标记其他模块。默认为 false'
},
confirmHighDeprecation: {
type: 'boolean',
description: '确认高比例废弃操作。当废弃比例超过 50% 时,必须设置为 true 才能继续。这是一个安全机制,防止误操作导致大量接口被标记为废弃。如果不设置此参数,操作会被阻止并返回错误信息。'
}
}
}
},
required: ['spec']
}
},
{
name: 'export_openapi',
description: '查看 Apifox 接口文档:从 Apifox 项目导出接口信息。重要:在导入新接口前,务必先使用 summary 模式查看现有目录结构和接口列表,以便将新接口放入合适的目录中,保持项目结构一致性。',
inputSchema: {
type: 'object',
properties: {
mode: {
type: 'string',
enum: ['summary', 'full'],
description: '导出模式:summary=仅导出目录结构和接口列表(推荐,节省上下文),full=导出完整的 OpenAPI 规范。默认为 summary'
},
oasVersion: {
type: 'string',
enum: ['2.0', '3.0', '3.1'],
description: 'OpenAPI 规范版本,默认为 3.0(仅 full 模式有效)'
},
exportFormat: {
type: 'string',
enum: ['JSON', 'YAML'],
description: '导出格式,默认为 JSON(仅 full 模式有效)'
},
pathFilter: {
type: 'string',
description: '路径过滤器,只导出匹配的接口路径(支持前缀匹配),如 "/api/user" 只导出用户相关接口'
}
}
}
}
];
/**
* 主函数
*/
async function main() {
// 检查特殊参数(在获取配置之前)
const args = process.argv.slice(2);
if (args.includes('--version') || args.includes('-v')) {
console.log(`apifox-openapi-mcp v${packageJson.version}`);
process.exit(0);
}
if (args.includes('--help') || args.includes('-h')) {
console.log(`
Apifox OpenAPI MCP Server v${packageJson.version}
Model Context Protocol server for Apifox API documentation management.
Usage:
apifox-mcp --token <token> --project-id <project-id> [options]
Required:
--token <token> Apifox API token
--project-id <project-id> Apifox project ID
Options:
--base-url <url> Apifox API base URL (default: https://api.apifox.com)
--version, -v Show version number
--help, -h Show this help message
Environment Variables:
APIFOX_TOKEN Apifox API token
APIFOX_PROJECT_ID Apifox project ID
APIFOX_BASE_URL Apifox API base URL
Examples:
# Using command line arguments
apifox-mcp --token "your-token" --project-id "your-project-id"
# Using environment variables
export APIFOX_TOKEN="your-token"
export APIFOX_PROJECT_ID="your-project-id"
apifox-mcp
Documentation:
https://github.com/warren/apifox-mcp
`);
process.exit(0);
}
// 获取配置
const config = getConfig();
const apifoxClient = new ApifoxClient(config);
// 创建 MCP 服务器
const server = new Server(
{
name: 'apifox-mcp-server',
version: '1.0.0'
},
{
capabilities: {
tools: {}
}
}
);
// 处理工具列表请求
server.setRequestHandler(ListToolsRequestSchema, async () => {
return { tools };
});
// 处理工具调用请求
server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { name, arguments: args } = request.params;
try {
switch (name) {
case 'import_openapi': {
const { spec, options } = args as { spec: any; options?: any };
// 验证 OpenAPI 规范的基本结构
if (!spec || typeof spec !== 'object') {
throw new Error('spec 参数必须是一个对象');
}
if (!spec.openapi && !spec.swagger) {
throw new Error('spec 必须包含 openapi 或 swagger 字段');
}
if (!spec.info) {
throw new Error('spec 必须包含 info 字段');
}
if (!spec.paths) {
throw new Error('spec 必须包含 paths 字段');
}
const result = await apifoxClient.importOpenApi(spec, options);
// 格式化导入结果
let resultText = '✅ OpenAPI 规范导入完成!\n\n';
// 显示警告信息(如果有)
if (result._warnings && result._warnings.length > 0) {
resultText += '⚠️ 重要提示:\n';
result._warnings.forEach((warning: string) => {
resultText += ` ${warning}\n`;
});
resultText += '\n';
}
// 显示废弃接口统计(如果有)
if (result._deprecatedInfo) {
const { count, ratio, scope } = result._deprecatedInfo;
resultText += '🔖 废弃接口标记:\n';
resultText += ` - 标记数量: ${count} 个\n`;
resultText += ` - 废弃比例: ${ratio.toFixed(1)}%\n`;
if (scope.length > 0) {
resultText += ` - 影响范围: ${scope.join(', ')}\n`;
}
resultText += '\n';
}
if (result.data && result.data.counters) {
const counters = result.data.counters;
// 接口统计
resultText += '📋 接口导入统计:\n';
resultText += ` - 创建: ${counters.endpointCreated || 0}\n`;
resultText += ` - 更新: ${counters.endpointUpdated || 0}\n`;
resultText += ` - 失败: ${counters.endpointFailed || 0}\n`;
resultText += ` - 忽略: ${counters.endpointIgnored || 0}\n`;
// 数据模型统计
resultText += '\n📦 数据模型导入统计:\n';
resultText += ` - 创建: ${counters.schemaCreated || 0}\n`;
resultText += ` - 更新: ${counters.schemaUpdated || 0}\n`;
resultText += ` - 失败: ${counters.schemaFailed || 0}\n`;
resultText += ` - 忽略: ${counters.schemaIgnored || 0}\n`;
// 错误信息
if (result.data.errors && result.data.errors.length > 0) {
resultText += '\n⚠️ 错误信息:\n';
result.data.errors.forEach((error: any) => {
resultText += ` - [${error.code}] ${error.message}\n`;
});
}
// 总结
const totalCreated = counters.endpointCreated + counters.schemaCreated;
const totalFailed = counters.endpointFailed + counters.schemaFailed;
if (totalCreated === 0 && totalFailed === 0) {
resultText += '\n💡 提示:没有新增或修改任何内容,可能是因为项目中已存在相同的 API。\n';
} else if (totalFailed > 0) {
resultText += `\n⚠️ 警告:有 ${totalFailed} 个项导入失败,请检查错误信息。\n`;
} else {
resultText += `\n🎉 成功导入 ${totalCreated} 个项!\n`;
}
}
return {
content: [
{
type: 'text',
text: resultText
}
]
};
}
case 'export_openapi': {
const { mode, oasVersion, exportFormat, pathFilter } = args as {
mode?: 'summary' | 'full';
oasVersion?: '2.0' | '3.0' | '3.1';
exportFormat?: 'JSON' | 'YAML';
pathFilter?: string;
};
const exportMode = mode || 'summary';
const result = await apifoxClient.exportOpenApi({
oasVersion: oasVersion || '3.0',
exportFormat: exportFormat || 'JSON'
});
// 路径过滤
let filteredPaths = result.paths;
if (pathFilter) {
filteredPaths = {};
Object.keys(result.paths).forEach(path => {
if (path.startsWith(pathFilter)) {
filteredPaths[path] = result.paths[path];
}
});
}
// Summary 模式:只返回目录结构和接口列表
if (exportMode === 'summary') {
let resultText = '✅ 接口文档概览(Summary 模式)\n\n';
// 统计信息
const pathsCount = Object.keys(filteredPaths).length;
const totalCount = Object.keys(result.paths).length;
resultText += `📊 统计信息:\n`;
resultText += ` - 项目标题: ${result.info?.title || '未命名'}\n`;
if (pathFilter) {
resultText += ` - 过滤条件: ${pathFilter}\n`;
resultText += ` - 匹配接口: ${pathsCount} / ${totalCount}\n`;
} else {
resultText += ` - 总接口数: ${pathsCount}\n`;
}
resultText += '\n';
// 按路径前缀分组(模拟目录结构)
const groups: { [key: string]: any[] } = {};
Object.keys(filteredPaths).forEach(path => {
const pathObj = filteredPaths[path];
const methods = Object.keys(pathObj).filter(m => m !== 'parameters');
methods.forEach(method => {
const operation = pathObj[method];
const tags = operation.tags || ['未分类'];
const tag = tags[0] || '未分类';
if (!groups[tag]) {
groups[tag] = [];
}
groups[tag].push({
path,
method: method.toUpperCase(),
summary: operation.summary || '无描述'
});
});
});
// 显示目录结构
resultText += '📁 目录结构和接口列表:\n\n';
const sortedGroups = Object.keys(groups).sort();
sortedGroups.forEach(groupName => {
resultText += `📂 ${groupName}\n`;
groups[groupName].forEach(api => {
resultText += ` └─ [${api.method}] ${api.path}\n`;
resultText += ` ${api.summary}\n`;
});
resultText += '\n';
});
resultText += '\n💡 提示:\n';
resultText += ' - 导入新接口时,请参考上述目录结构\n';
resultText += ' - 将相关接口放入对应的目录(使用 tags 字段)\n';
resultText += ' - 保持接口路径命名风格一致\n';
if (!pathFilter) {
resultText += ' - 如需查看特定接口详情,使用 pathFilter 参数过滤\n';
resultText += ' - 如需完整规范,使用 mode: "full"\n';
}
return {
content: [
{
type: 'text',
text: resultText
}
]
};
}
// Full 模式:返回完整 OpenAPI 规范
let resultText = '✅ OpenAPI 规范导出成功(Full 模式)\n\n';
const pathsCount = Object.keys(filteredPaths).length;
const schemasCount = result.components?.schemas ? Object.keys(result.components.schemas).length : 0;
resultText += `📊 导出统计:\n`;
resultText += ` - OpenAPI 版本: ${result.openapi || result.swagger}\n`;
resultText += ` - 项目标题: ${result.info?.title || '未命名'}\n`;
resultText += ` - 接口数量: ${pathsCount}\n`;
resultText += ` - 数据模型数量: ${schemasCount}\n`;
if (pathsCount > 0) {
resultText += '\n📋 接口列表:\n';
const paths = Object.keys(filteredPaths);
paths.slice(0, 10).forEach(path => {
const methods = Object.keys(filteredPaths[path]).filter(m => m !== 'parameters');
resultText += ` - ${path} [${methods.join(', ').toUpperCase()}]\n`;
});
if (paths.length > 10) {
resultText += ` ... 还有 ${paths.length - 10} 个接口\n`;
}
}
// 如果有过滤,返回过滤后的规范
let exportedSpec = result;
if (pathFilter) {
exportedSpec = {
...result,
paths: filteredPaths
};
}
return {
content: [
{
type: 'text',
text: resultText
},
{
type: 'text',
text: `\n📄 完整 OpenAPI 规范:\n\`\`\`json\n${JSON.stringify(exportedSpec, null, 2)}\n\`\`\``
}
]
};
}
default:
throw new Error(`未知的工具: ${name}`);
}
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
return {
content: [
{
type: 'text',
text: `❌ 操作失败:${errorMessage}`
}
],
isError: true
};
}
});
// 启动服务器
const transport = new StdioServerTransport();
await server.connect(transport);
console.error('✅ Apifox MCP 服务器已启动');
console.error(`📦 项目 ID: ${config.projectId}`);
console.error(`🔗 API 地址: ${config.baseUrl || 'https://api.apifox.com'}`);
console.error('\n🚀 可用工具:');
console.error(' - import_openapi: 批量导入 OpenAPI/Swagger 规范到 Apifox');
console.error(' - export_openapi: 从 Apifox 导出 OpenAPI/Swagger 规范');
}
// 运行主函数
main().catch((error) => {
console.error('服务器启动失败:', error);
process.exit(1);
});