feat: refactor for multi-tenant HTTP support and global caching
This commit is contained in:
1 parent
30d4c8a9da
commit
1ea6e0e9f0
5 files changed
+737
-639
No files matched your search
+5
-1
@@ -1 +1,5 @@
|
|||||||
node_modules
|
node_modules
|
||||||
|
|
||||||
|
ecosystem.config.js
|
||||||
|
*.log
|
||||||
|
_data
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
# Apifox MCP 服务上下文
|
||||||
|
|
||||||
|
## 项目概述
|
||||||
|
本项目是一个针对 **Apifox** 的 **Model Context Protocol (MCP) 服务器**。它充当大型语言模型 (LLM)(如 Claude 或 Gemini)与 Apifox API 文档之间的桥梁。它允许 AI 助手直接从 Apifox 项目中检索强类型的 API 定义 (Schema),从而生成准确的 API 请求代码。
|
||||||
|
|
||||||
|
## 架构
|
||||||
|
- **`index.js`**: 主入口点。它设置 MCP 服务器,注册工具,并处理传输(Stdio 或 HTTP/SSE)。
|
||||||
|
- **`apifox.js`**: 包含 `ApifoxService` 类。它封装了与 Apifox API 交互的逻辑,处理身份验证、缓存,并递归解析 JSON Schema 引用 (`$ref`) 以提供自包含的 API 定义。
|
||||||
|
|
||||||
|
## 构建与运行
|
||||||
|
|
||||||
|
### 前置要求
|
||||||
|
- Node.js (v18+)
|
||||||
|
- Apifox 账号及项目
|
||||||
|
|
||||||
|
### 环境变量
|
||||||
|
需要以下环境变量:
|
||||||
|
- `APIFOX_PROJECT_ID`: 目标 Apifox 项目 ID (数字)。
|
||||||
|
- `APIFOX_TOKEN`: Apifox 个人访问令牌 (Bearer Token)。
|
||||||
|
|
||||||
|
### 命令
|
||||||
|
- **安装依赖**:
|
||||||
|
```bash
|
||||||
|
npm install
|
||||||
|
```
|
||||||
|
- **启动 (Stdio 模式)** - *默认,用于与 Claude Desktop 集成*:
|
||||||
|
```bash
|
||||||
|
node index.js
|
||||||
|
# 或者显式指定:
|
||||||
|
node index.js --transport stdio
|
||||||
|
```
|
||||||
|
- **启动 (HTTP/SSE 模式)** - *用于远程部署或检查*:
|
||||||
|
```bash
|
||||||
|
node index.js --transport http --port 8889
|
||||||
|
```
|
||||||
|
- **端点**: `http://localhost:8889/sse`
|
||||||
|
- **健康检查**: `http://localhost:8889/health`
|
||||||
|
|
||||||
|
## 关键文件
|
||||||
|
|
||||||
|
- **`index.js`**:
|
||||||
|
- 初始化 `McpServer`。
|
||||||
|
- 注册工具: `get_api_by_path`, `get_apis_by_folder`。
|
||||||
|
- 处理传输选择的命令行参数。
|
||||||
|
- **`apifox.js`**:
|
||||||
|
- `ApifoxService` 类实现。
|
||||||
|
- 从 `https://apifox.gzmiyuan.com/api/v1` 获取数据。
|
||||||
|
- 使用 `node-cache` 实现缓存策略。
|
||||||
|
- 包含扁平化和解析复杂 JSON schema 以供 LLM 使用的逻辑。
|
||||||
|
|
||||||
|
## 可用工具
|
||||||
|
|
||||||
|
服务器向 MCP 客户端公开以下工具:
|
||||||
|
|
||||||
|
1. **`get_api_by_path`**:
|
||||||
|
- **描述**: 根据路径检索详细的 API 定义 schema。
|
||||||
|
- **输入**: `path` (字符串), `projectId` (数字, 可选)。
|
||||||
|
2. **`get_apis_by_folder`**:
|
||||||
|
- **描述**: 检索特定文件夹内的所有 API 定义。
|
||||||
|
- **输入**: `folderName` (字符串), `projectId` (数字, 可选)。
|
||||||
|
|
||||||
|
## 开发约定
|
||||||
|
- **ES Modules**: 项目使用 ES 模块(`package.json` 中的 `"type": "module"`)。
|
||||||
|
- **错误处理**: 工具返回带有 `isError: true` 的结构化错误响应,而不是崩溃。
|
||||||
|
- **日志记录**: Stdio 传输使用 `stderr` 进行日志记录,以避免干扰 `stdout` 上的 JSON-RPC 消息。
|
||||||
@@ -1,376 +1,443 @@
|
|||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import NodeCache from 'node-cache';
|
import NodeCache from 'node-cache';
|
||||||
import fs from 'fs';
|
import fs from 'fs';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
|
|
||||||
const APIFOX_API_BASE = "https://apifox.gzmiyuan.com/api/v1";
|
const APIFOX_API_BASE = "https://apifox.gzmiyuan.com/api/v1";
|
||||||
|
|
||||||
export class ApifoxService {
|
// 初始化node-cache,设置默认5分钟过期时间
|
||||||
constructor(config = {}) {
|
// 全局共享缓存,避免每次实例化清空缓存
|
||||||
const { projectId, token, baseUrl, logFilePath } = config;
|
const globalCache = new NodeCache({
|
||||||
this.projectId = projectId;
|
stdTTL: 5 * 60, // 5分钟,单位秒
|
||||||
this.branchId = null;
|
checkperiod: 60, // 每分钟检查一次过期缓存
|
||||||
this.apiTreeList = null;
|
useClones: false // 创建缓存对象的克隆以避免引用问题
|
||||||
this.logFilePath = logFilePath || path.join(process.cwd(), 'apifox-service.log');
|
});
|
||||||
this.http = axios.create({
|
|
||||||
baseURL: baseUrl || APIFOX_API_BASE,
|
export class ApifoxService {
|
||||||
headers: {
|
constructor(config = {}) {
|
||||||
"X-Client-Mode": "web",
|
const { projectId, token, baseUrl, logFilePath } = config;
|
||||||
"X-Client-Version": "2.7.19",
|
this.defaultProjectId = projectId;
|
||||||
"X-Device-Id": "bd94aea0-7b65-48ba-9274-4b1177897954",
|
this.projectBranchIds = new Map(); // Cache branchIds: projectId -> branchId
|
||||||
"Authorization": `Bearer ${token}`
|
this.apiTreeList = null;
|
||||||
}
|
this.logFilePath = logFilePath || path.join(process.cwd(), 'apifox-service.log');
|
||||||
});
|
this.http = axios.create({
|
||||||
|
baseURL: baseUrl || APIFOX_API_BASE,
|
||||||
// 初始化node-cache,设置默认5分钟过期时间
|
headers: {
|
||||||
this.cache = new NodeCache({
|
"X-Client-Mode": "web",
|
||||||
stdTTL: 5 * 60, // 5分钟,单位秒
|
"X-Client-Version": "2.7.19",
|
||||||
checkperiod: 60, // 每分钟检查一次过期缓存
|
"X-Device-Id": "bd94aea0-7b65-48ba-9274-4b1177897954",
|
||||||
useClones: false // 创建缓存对象的克隆以避免引用问题
|
"Authorization": `Bearer ${token}`
|
||||||
});
|
}
|
||||||
|
});
|
||||||
this.init();
|
|
||||||
}
|
this.cache = globalCache;
|
||||||
|
|
||||||
/**
|
this.init();
|
||||||
* 清除特定缓存
|
}
|
||||||
* @param {string} cacheKey - 缓存键名
|
|
||||||
*/
|
/**
|
||||||
clearCache(cacheKey) {
|
* 清除特定缓存
|
||||||
if (cacheKey) {
|
* @param {string} cacheKey - 缓存键名
|
||||||
this.cache.del(cacheKey);
|
*/
|
||||||
} else {
|
clearCache(cacheKey) {
|
||||||
// 清除所有缓存
|
if (cacheKey) {
|
||||||
this.cache.flushAll();
|
this.cache.del(cacheKey);
|
||||||
}
|
} else {
|
||||||
}
|
// 清除所有缓存
|
||||||
|
this.cache.flushAll();
|
||||||
/**
|
}
|
||||||
* 写入日志到文件
|
}
|
||||||
* @param {string} level - 日志级别 (INFO, ERROR, WARN, DEBUG)
|
|
||||||
* @param {string} message - 日志消息
|
/**
|
||||||
* @param {object} data - 可选的额外数据
|
* 写入日志到文件
|
||||||
*/
|
* @param {string} level - 日志级别 (INFO, ERROR, WARN, DEBUG)
|
||||||
logToFile(level, message, data = null) {
|
* @param {string} message - 日志消息
|
||||||
try {
|
* @param {object} data - 可选的额外数据
|
||||||
const timestamp = new Date().toLocaleString('zh-CN', {
|
*/
|
||||||
year: 'numeric',
|
logToFile(level, message, data = null) {
|
||||||
month: '2-digit',
|
try {
|
||||||
day: '2-digit',
|
const timestamp = new Date().toLocaleString('zh-CN', {
|
||||||
hour: '2-digit',
|
year: 'numeric',
|
||||||
minute: '2-digit',
|
month: '2-digit',
|
||||||
second: '2-digit'
|
day: '2-digit',
|
||||||
});
|
hour: '2-digit',
|
||||||
|
minute: '2-digit',
|
||||||
let logLine = `[${timestamp}] ${level.toUpperCase()}: ${message}`;
|
second: '2-digit'
|
||||||
|
});
|
||||||
if (data) {
|
|
||||||
if (typeof data === 'object') {
|
let logLine = `[${timestamp}] ${level.toUpperCase()}: ${message}`;
|
||||||
logLine += `\n 数据: ${JSON.stringify(data, null, 2)}`;
|
|
||||||
} else {
|
if (data) {
|
||||||
logLine += `\n 数据: ${data}`;
|
if (typeof data === 'object') {
|
||||||
}
|
logLine += `\n 数据: ${JSON.stringify(data, null, 2)}`;
|
||||||
}
|
} else {
|
||||||
|
logLine += `\n 数据: ${data}`;
|
||||||
logLine += '\n';
|
}
|
||||||
|
}
|
||||||
// 确保日志目录存在
|
|
||||||
const logDir = path.dirname(this.logFilePath);
|
logLine += '\n';
|
||||||
if (!fs.existsSync(logDir)) {
|
|
||||||
fs.mkdirSync(logDir, { recursive: true });
|
// 确保日志目录存在
|
||||||
}
|
const logDir = path.dirname(this.logFilePath);
|
||||||
|
if (!fs.existsSync(logDir)) {
|
||||||
// 追加写入日志文件
|
fs.mkdirSync(logDir, { recursive: true });
|
||||||
fs.appendFileSync(this.logFilePath, logLine);
|
}
|
||||||
|
|
||||||
// 同时输出到控制台
|
// 追加写入日志文件
|
||||||
} catch (error) {
|
fs.appendFileSync(this.logFilePath, logLine);
|
||||||
console.error('写入日志失败:', error);
|
|
||||||
}
|
// 同时输出到控制台
|
||||||
}
|
} catch (error) {
|
||||||
|
console.error('写入日志失败:', error);
|
||||||
/**
|
}
|
||||||
* 初始化服务,获取branchId
|
}
|
||||||
* @returns {Promise<void>}
|
|
||||||
*/
|
async init() {
|
||||||
async init(projectId) {
|
// Pre-fetch branchId for default project if set
|
||||||
if (projectId) {
|
if (this.defaultProjectId) {
|
||||||
this.projectId = projectId;
|
await this.getBranchId(this.defaultProjectId);
|
||||||
this.branchId = await this.getBranchId();
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
/**
|
||||||
/**
|
* 发送GET请求
|
||||||
* 发送GET请求
|
* @param {string} url - 请求的URL路径
|
||||||
* @param {string} url - 请求的URL路径
|
* @param {object} config - axios配置对象 (contains headers with projectId/branchId)
|
||||||
* @param {object} config - axios配置对象
|
* @returns {Promise} 返回响应数据中的data字段
|
||||||
* @returns {Promise} 返回响应数据中的data字段
|
*/
|
||||||
*/
|
async get(url, config = {}) {
|
||||||
async get(url, config = {}) {
|
try {
|
||||||
try {
|
const response = await this.http.get(url, config);
|
||||||
const response = await this.http.get(url, {
|
return response.data;
|
||||||
...config,
|
} catch (error) {
|
||||||
headers: {
|
// Enhance error logging if needed
|
||||||
...config.headers,
|
throw error;
|
||||||
"X-Project-Id": this.projectId,
|
}
|
||||||
"X-Branch-Id": this.branchId,
|
}
|
||||||
},
|
|
||||||
});
|
/**
|
||||||
return response.data;
|
* 发送POST请求
|
||||||
} catch (error) {
|
* @param {string} url - 请求的URL路径
|
||||||
throw error;
|
* @param {object} data - 请求体数据
|
||||||
}
|
* @param {object} config - axios配置对象
|
||||||
}
|
* @returns {Promise} 返回响应数据中的data字段
|
||||||
|
*/
|
||||||
/**
|
async post(url, data = {}, config = {}) {
|
||||||
* 发送POST请求
|
try {
|
||||||
* @param {string} url - 请求的URL路径
|
const response = await this.http.post(url, data, config);
|
||||||
* @param {object} data - 请求体数据
|
return response.data;
|
||||||
* @param {object} config - axios配置对象
|
} catch (error) {
|
||||||
* @returns {Promise} 返回响应数据中的data字段
|
throw error;
|
||||||
*/
|
}
|
||||||
async post(url, data = {}, config = {}) {
|
}
|
||||||
try {
|
|
||||||
const response = await this.http.post(url, data, {
|
async getRequestConfig(projectId) {
|
||||||
...config,
|
const pid = projectId || this.defaultProjectId;
|
||||||
headers: {
|
if (!pid) throw new Error("No Project ID provided");
|
||||||
...config.headers,
|
|
||||||
"X-Project-Id": this.projectId,
|
const branchId = await this.getBranchId(pid);
|
||||||
"X-Branch-Id": this.branchId,
|
return {
|
||||||
},
|
headers: {
|
||||||
});
|
"X-Project-Id": pid,
|
||||||
return response.data;
|
"X-Branch-Id": branchId,
|
||||||
} catch (error) {
|
}
|
||||||
throw error;
|
};
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
|
async getBranchId(projectId) {
|
||||||
async getBranchId() {
|
if (this.projectBranchIds.has(projectId)) {
|
||||||
const branches = await this.get(`/projects/${this.projectId}/api-versions`);
|
return this.projectBranchIds.get(projectId);
|
||||||
return branches.data[0].id;
|
}
|
||||||
}
|
|
||||||
|
// We need to fetch branch ID.
|
||||||
async getApiTreeList() {
|
// BUT fetching branch ID requires Project ID header usually?
|
||||||
const apiTree = await this.get(`/projects/${this.projectId}/api-tree-list`);
|
// Let's check the original implementation:
|
||||||
this.apiTreeList = apiTree;
|
// const branches = await this.get(`/projects/${this.projectId}/api-versions`);
|
||||||
return apiTree;
|
// It used `this.http` which had `this.projectId` in headers?
|
||||||
}
|
// WAIT, original `get` implementation injected headers dynamically based on `this.projectId`.
|
||||||
|
|
||||||
async getFolderList() {
|
// So here we must manually construct headers for this specific request to avoid circular dependency
|
||||||
// 定义缓存键
|
// or just pass X-Project-Id.
|
||||||
const cacheKey = 'folderList';
|
|
||||||
|
try {
|
||||||
// 检查缓存是否有效
|
const response = await this.http.get(`/projects/${projectId}/api-versions`, {
|
||||||
const cachedData = this.cache.get(cacheKey);
|
headers: {
|
||||||
if (cachedData) {
|
"X-Project-Id": projectId
|
||||||
return cachedData;
|
}
|
||||||
}
|
});
|
||||||
|
|
||||||
const {data: folderList} = await this.get(`/projects/${this.projectId}/api-detail-folders`);
|
const branchId = response.data.data[0].id;
|
||||||
|
this.projectBranchIds.set(projectId, branchId);
|
||||||
// 设置缓存
|
return branchId;
|
||||||
this.cache.set(cacheKey, folderList);
|
} catch (error) {
|
||||||
return folderList;
|
console.error(`Failed to get branch ID for project ${projectId}`, error);
|
||||||
}
|
throw error;
|
||||||
|
}
|
||||||
async getFolderByName(folderName) {
|
}
|
||||||
const folderList = await this.getFolderList();
|
|
||||||
const folder = folderList.find((item) => item.name === folderName);
|
async getApiTreeList(projectId) {
|
||||||
if (!folder) {
|
const pid = projectId || this.defaultProjectId;
|
||||||
throw new Error(`Folder with name ${folderName} not found`);
|
const cacheKey = `${pid}:apiTreeList`;
|
||||||
}
|
|
||||||
return folder;
|
const cachedData = this.cache.get(cacheKey);
|
||||||
}
|
if (cachedData) {
|
||||||
async getDataSchema() {
|
return cachedData;
|
||||||
// 定义缓存键
|
}
|
||||||
const cacheKey = 'dataSchema';
|
|
||||||
|
const config = await this.getRequestConfig(pid);
|
||||||
// 检查缓存是否有效
|
const apiTree = await this.get(`/projects/${pid}/api-tree-list`, config);
|
||||||
const cachedData = this.cache.get(cacheKey);
|
|
||||||
if (cachedData) {
|
this.cache.set(cacheKey, apiTree);
|
||||||
return cachedData;
|
return apiTree;
|
||||||
}
|
}
|
||||||
|
|
||||||
const {data:schemasTreeList} = await this.get(`/projects/${this.projectId}/data-schemas?locale=zh-CN`);
|
async getFolderList(projectId) {
|
||||||
|
const pid = projectId || this.defaultProjectId;
|
||||||
// 设置缓存
|
// 定义缓存键
|
||||||
this.cache.set(cacheKey, schemasTreeList);
|
const cacheKey = `${pid}:folderList`;
|
||||||
return schemasTreeList;
|
|
||||||
}
|
// 检查缓存是否有效
|
||||||
|
const cachedData = this.cache.get(cacheKey);
|
||||||
async getApiDetails() {
|
if (cachedData) {
|
||||||
// 定义缓存键
|
return cachedData;
|
||||||
const cacheKey = 'apiDetails';
|
}
|
||||||
|
|
||||||
// 检查缓存是否有效
|
const config = await this.getRequestConfig(pid);
|
||||||
const cachedData = this.cache.get(cacheKey);
|
const {data: folderList} = await this.get(`/projects/${pid}/api-detail-folders`, config);
|
||||||
if (cachedData) {
|
|
||||||
return cachedData;
|
// 设置缓存
|
||||||
}
|
this.cache.set(cacheKey, folderList);
|
||||||
|
return folderList;
|
||||||
const {data:apiDetails} = await this.get(`/api-details?locale=zh-CN`, {});
|
}
|
||||||
|
|
||||||
// 设置缓存
|
async getFolderByName(folderName, projectId) {
|
||||||
this.cache.set(cacheKey, apiDetails);
|
const folderList = await this.getFolderList(projectId);
|
||||||
return apiDetails;
|
const folder = folderList.find((item) => item.name === folderName);
|
||||||
}
|
if (!folder) {
|
||||||
|
throw new Error(`Folder with name ${folderName} not found`);
|
||||||
getSchemaRefId(obj) {
|
}
|
||||||
if (!obj || !obj.$ref) {
|
return folder;
|
||||||
return;
|
}
|
||||||
}
|
async getDataSchema(projectId) {
|
||||||
const schemaName = obj.$ref.split('/').pop();
|
const pid = projectId || this.defaultProjectId;
|
||||||
return schemaName;
|
// 定义缓存键
|
||||||
}
|
const cacheKey = `${pid}:dataSchema`;
|
||||||
|
|
||||||
/**
|
// 检查缓存是否有效
|
||||||
* 根据schema ID获取完整的schema定义,递归替换所有$ref引用
|
const cachedData = this.cache.get(cacheKey);
|
||||||
* @param {number|string} schemaId - schema的ID
|
if (cachedData) {
|
||||||
* @param {Array} allSchemas - 所有schema数据数组(可选,用于递归)
|
return cachedData;
|
||||||
* @param {Set} visitedIds - 已访问的ID集合,用于防止循环引用
|
}
|
||||||
* @returns {Promise<object>} 返回完整解析后的schema对象
|
|
||||||
*/
|
const config = await this.getRequestConfig(pid);
|
||||||
async getSchemaById(schemaId, allSchemas = null, visitedIds = new Set()) {
|
const {data:schemasTreeList} = await this.get(`/projects/${pid}/data-schemas?locale=zh-CN`, config);
|
||||||
if (!schemaId) {
|
|
||||||
return null;
|
// 设置缓存
|
||||||
}
|
this.cache.set(cacheKey, schemasTreeList);
|
||||||
// 如果没有传入allSchemas,则获取所有schema数据
|
return schemasTreeList;
|
||||||
if (!allSchemas) {
|
}
|
||||||
allSchemas = await this.getDataSchema();
|
|
||||||
}
|
async getApiDetails(projectId) {
|
||||||
|
const pid = projectId || this.defaultProjectId;
|
||||||
// 查找对应ID的schema
|
// 定义缓存键
|
||||||
const schema = allSchemas.find((item) => item.id.toString() === schemaId.toString());
|
const cacheKey = `${pid}:apiDetails`;
|
||||||
if (!schema) {
|
|
||||||
throw new Error(`Schema with id ${schemaId} not found`);
|
// 检查缓存是否有效
|
||||||
}
|
const cachedData = this.cache.get(cacheKey);
|
||||||
|
if (cachedData) {
|
||||||
// 防止循环引用
|
return cachedData;
|
||||||
if (visitedIds.has(schemaId.toString())) {
|
}
|
||||||
return { type: 'object', description: `循环引用: ${schema.name}` };
|
|
||||||
}
|
const config = await this.getRequestConfig(pid);
|
||||||
visitedIds.add(schemaId.toString());
|
const {data:apiDetails} = await this.get(`/api-details?locale=zh-CN`, config);
|
||||||
|
|
||||||
// 深拷贝schema以避免修改原始数据
|
// 设置缓存
|
||||||
const resolvedSchema = JSON.parse(JSON.stringify(schema));
|
this.cache.set(cacheKey, apiDetails);
|
||||||
|
return apiDetails;
|
||||||
// 递归替换jsonSchema中的所有$ref
|
}
|
||||||
resolvedSchema.jsonSchema = await this.resolveRefs(
|
|
||||||
resolvedSchema.jsonSchema,
|
getSchemaRefId(obj) {
|
||||||
allSchemas,
|
if (!obj || !obj.$ref) {
|
||||||
new Set(visitedIds)
|
return;
|
||||||
);
|
}
|
||||||
|
const schemaName = obj.$ref.split('/').pop();
|
||||||
// 移除不需要的字段
|
return schemaName;
|
||||||
const { description, folderId, projectId, moduleId, ordering, creatorId, editorId, createdAt, updatedAt, visibility, ...cleanSchema } = resolvedSchema;
|
}
|
||||||
|
|
||||||
return cleanSchema;
|
/**
|
||||||
}
|
* 根据schema ID获取完整的schema定义,递归替换所有$ref引用
|
||||||
|
* @param {number|string} schemaId - schema的ID
|
||||||
/**
|
* @param {object} context - 上下文对象 { projectId, allSchemas, visitedIds }
|
||||||
* 递归解析对象中的所有$ref引用
|
* @returns {Promise<object>} 返回完整解析后的schema对象
|
||||||
* @param {object} obj - 要解析的对象
|
*/
|
||||||
* @param {Array} allSchemas - 所有schema数据数组
|
async getSchemaById(schemaId, context = {}) {
|
||||||
* @param {Set} visitedIds - 已访问的ID集合
|
if (!schemaId) {
|
||||||
* @returns {Promise<object>} 返回解析后的对象
|
return null;
|
||||||
*/
|
}
|
||||||
async resolveRefs(obj, allSchemas, visitedIds) {
|
|
||||||
if (!obj || typeof obj !== 'object') {
|
let { projectId, allSchemas, visitedIds } = context;
|
||||||
return obj;
|
visitedIds = visitedIds || new Set();
|
||||||
}
|
|
||||||
|
// 如果没有传入allSchemas,则获取所有schema数据
|
||||||
// 如果是数组,递归处理每个元素
|
if (!allSchemas) {
|
||||||
if (Array.isArray(obj)) {
|
allSchemas = await this.getDataSchema(projectId);
|
||||||
const resolved = [];
|
context.allSchemas = allSchemas; // Update context for recursion
|
||||||
for (const item of obj) {
|
}
|
||||||
resolved.push(await this.resolveRefs(item, allSchemas, visitedIds));
|
|
||||||
}
|
// 查找对应ID的schema
|
||||||
return resolved;
|
const schema = allSchemas.find((item) => item.id.toString() === schemaId.toString());
|
||||||
}
|
if (!schema) {
|
||||||
|
throw new Error(`Schema with id ${schemaId} not found`);
|
||||||
// 如果对象包含$ref,替换为实际的schema定义
|
}
|
||||||
if (obj.$ref) {
|
|
||||||
const refId = this.getSchemaRefId(obj);
|
// 防止循环引用
|
||||||
const referencedSchema = await this.getSchemaById(refId, allSchemas, visitedIds);
|
if (visitedIds.has(schemaId.toString())) {
|
||||||
// 返回引用的jsonSchema内容
|
return { type: 'object', description: `循环引用: ${schema.name}` };
|
||||||
return referencedSchema.jsonSchema;
|
}
|
||||||
}
|
visitedIds.add(schemaId.toString());
|
||||||
|
|
||||||
// 递归处理对象的所有属性
|
// Update context with new visitedIds for recursion (create new Set for branch safety if needed, but sharing Set prevents cycles across branches which is usually desired for DAGs)
|
||||||
const resolved = {};
|
// Actually, usually visitedIds should be cloned for separate branches if we want to allow same ref in different parts, but for circular detection, shared set is stricter.
|
||||||
for (const [key, value] of Object.entries(obj)) {
|
// However, existing code passed `new Set(visitedIds)` to resolveRefs.
|
||||||
resolved[key] = await this.resolveRefs(value, allSchemas, visitedIds);
|
|
||||||
}
|
// 深拷贝schema以避免修改原始数据
|
||||||
return resolved;
|
const resolvedSchema = JSON.parse(JSON.stringify(schema));
|
||||||
}
|
|
||||||
|
// 递归替换jsonSchema中的所有$ref
|
||||||
async getApiDetailResp(apiDetail) {
|
resolvedSchema.jsonSchema = await this.resolveRefs(
|
||||||
const dataSchema = await this.getDataSchema();
|
resolvedSchema.jsonSchema,
|
||||||
const requestSchemaId = this.getSchemaRefId(apiDetail.requestBody.jsonSchema);
|
{ ...context, visitedIds: new Set(visitedIds) }
|
||||||
const responseSchema = Array.isArray(apiDetail.responses) ? apiDetail.responses[0].jsonSchema : apiDetail.responses.jsonSchema;
|
);
|
||||||
const responseSchemaId = this.getSchemaRefId(responseSchema);
|
|
||||||
const responseDataSchema = dataSchema.find((item) => item.id.toString() === responseSchemaId).jsonSchema.properties.data;
|
// 移除不需要的字段
|
||||||
|
const { description, folderId, projectId: pid, moduleId, ordering, creatorId, editorId, createdAt, updatedAt, visibility, ...cleanSchema } = resolvedSchema;
|
||||||
let respDataSchemaId;
|
|
||||||
const isArray = responseDataSchema.type === 'array';
|
return cleanSchema;
|
||||||
// 处理数组和对象两种情况
|
}
|
||||||
if (isArray && responseDataSchema.items && responseDataSchema.items.$ref) {
|
|
||||||
// 数组情况:从 items.$ref 中提取 schema ID
|
/**
|
||||||
respDataSchemaId = this.getSchemaRefId(responseDataSchema.items);
|
* 递归解析对象中的所有$ref引用
|
||||||
} else if (responseDataSchema.$ref) {
|
* @param {object} obj - 要解析的对象
|
||||||
// 对象情况:直接从 $ref 中提取 schema ID
|
* @param {object} context - 上下文对象 { projectId, allSchemas, visitedIds }
|
||||||
respDataSchemaId = this.getSchemaRefId(responseDataSchema);
|
* @returns {Promise<object>} 返回解析后的对象
|
||||||
} else {
|
*/
|
||||||
// 如果都没有 $ref,记录警告并返回 null
|
async resolveRefs(obj, context) {
|
||||||
this.logToFile('WARN', `无法从 responseDataSchema 中提取 schema ID`, responseDataSchema);
|
if (!obj || typeof obj !== 'object') {
|
||||||
respDataSchemaId = null;
|
return obj;
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
// 如果是数组,递归处理每个元素
|
||||||
path: apiDetail.path,
|
if (Array.isArray(obj)) {
|
||||||
name: apiDetail.name,
|
const resolved = [];
|
||||||
method: apiDetail.method,
|
for (const item of obj) {
|
||||||
url: apiDetail.url,
|
resolved.push(await this.resolveRefs(item, context));
|
||||||
requestSchema: await this.getSchemaById(requestSchemaId),
|
}
|
||||||
responseSchema: isArray ? {
|
return resolved;
|
||||||
type: 'array',
|
}
|
||||||
items: respDataSchemaId ? await this.getSchemaById(respDataSchemaId) : null,
|
|
||||||
} : await this.getSchemaById(respDataSchemaId),
|
// 如果对象包含$ref,替换为实际的schema定义
|
||||||
};
|
if (obj.$ref) {
|
||||||
}
|
const refId = this.getSchemaRefId(obj);
|
||||||
|
// Calls getSchemaById with context
|
||||||
async getApiDetailByPath(path, projectId) {
|
const referencedSchema = await this.getSchemaById(refId, context);
|
||||||
if (projectId) {
|
// 返回引用的jsonSchema内容
|
||||||
await this.init(projectId);
|
return referencedSchema.jsonSchema;
|
||||||
}
|
}
|
||||||
const apiDetails = await this.getApiDetails();
|
|
||||||
const apiDetail = apiDetails.find((item) => item.path === path);
|
// 递归处理对象的所有属性
|
||||||
if (!apiDetail) {
|
const resolved = {};
|
||||||
throw new Error(`API with path ${path} not found`);
|
for (const [key, value] of Object.entries(obj)) {
|
||||||
}
|
resolved[key] = await this.resolveRefs(value, context);
|
||||||
return this.getApiDetailResp(apiDetail);
|
}
|
||||||
}
|
return resolved;
|
||||||
|
}
|
||||||
async getApiDetailsById(apiId) {
|
|
||||||
const apiDetails = await this.getApiDetails();
|
async getApiDetailResp(apiDetail, projectId) {
|
||||||
const apiDetail = apiDetails.find((item) => item.id === apiId);
|
const pid = projectId || this.defaultProjectId;
|
||||||
if (!apiDetail) {
|
const dataSchema = await this.getDataSchema(pid);
|
||||||
throw new Error(`API with id ${id} not found`);
|
const context = { projectId: pid, allSchemas: dataSchema };
|
||||||
}
|
|
||||||
return this.getApiDetailResp(apiDetail);
|
const requestSchemaId = this.getSchemaRefId(apiDetail.requestBody.jsonSchema);
|
||||||
}
|
const responseSchema = Array.isArray(apiDetail.responses) ? apiDetail.responses[0].jsonSchema : apiDetail.responses.jsonSchema;
|
||||||
|
const responseSchemaId = this.getSchemaRefId(responseSchema);
|
||||||
|
|
||||||
// TODO:
|
// Find the response data schema property
|
||||||
async getApiListByFolderName(folderName, projectId) {
|
let responseDataSchema;
|
||||||
if (projectId) {
|
try {
|
||||||
await this.init(projectId);
|
const schema = dataSchema.find((item) => item.id.toString() === responseSchemaId);
|
||||||
}
|
if (schema && schema.jsonSchema && schema.jsonSchema.properties) {
|
||||||
const folder = await this.getFolderByName(folderName);
|
responseDataSchema = schema.jsonSchema.properties.data;
|
||||||
const apiDetails = await this.getApiDetails();
|
}
|
||||||
const apiList = apiDetails.filter((item) => item.folderId === folder.id);
|
} catch (e) {
|
||||||
return Promise.all(apiList.map((api) => this.getApiDetailResp(api)));
|
// Fallback or ignore if structure not found
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
if (!responseDataSchema) {
|
||||||
|
// handle case where structure is different or missing
|
||||||
|
// For now, let's assume if we can't find it, we might just try to return null for response schema or log warning
|
||||||
|
this.logToFile('WARN', `无法找到 responseDataSchema`, { responseSchemaId });
|
||||||
|
// Proceeding with null might be safer than crashing below
|
||||||
|
}
|
||||||
|
|
||||||
|
let respDataSchemaId;
|
||||||
|
if (responseDataSchema) {
|
||||||
|
const isArray = responseDataSchema.type === 'array';
|
||||||
|
// 处理数组和对象两种情况
|
||||||
|
if (isArray && responseDataSchema.items && responseDataSchema.items.$ref) {
|
||||||
|
// 数组情况:从 items.$ref 中提取 schema ID
|
||||||
|
respDataSchemaId = this.getSchemaRefId(responseDataSchema.items);
|
||||||
|
} else if (responseDataSchema.$ref) {
|
||||||
|
// 对象情况:直接从 $ref 中提取 schema ID
|
||||||
|
respDataSchemaId = this.getSchemaRefId(responseDataSchema);
|
||||||
|
} else {
|
||||||
|
// 如果都没有 $ref,记录警告并返回 null
|
||||||
|
this.logToFile('WARN', `无法从 responseDataSchema 中提取 schema ID`, responseDataSchema);
|
||||||
|
respDataSchemaId = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
path: apiDetail.path,
|
||||||
|
name: apiDetail.name,
|
||||||
|
method: apiDetail.method,
|
||||||
|
url: apiDetail.url,
|
||||||
|
requestSchema: await this.getSchemaById(requestSchemaId, context),
|
||||||
|
responseSchema: (responseDataSchema && responseDataSchema.type === 'array') ? {
|
||||||
|
type: 'array',
|
||||||
|
items: respDataSchemaId ? await this.getSchemaById(respDataSchemaId, context) : null,
|
||||||
|
} : await this.getSchemaById(respDataSchemaId, context),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async getApiDetailByPath(path, projectId) {
|
||||||
|
const pid = projectId || this.defaultProjectId;
|
||||||
|
const apiDetails = await this.getApiDetails(pid);
|
||||||
|
const apiDetail = apiDetails.find((item) => item.path === path);
|
||||||
|
if (!apiDetail) {
|
||||||
|
throw new Error(`API with path ${path} not found`);
|
||||||
|
}
|
||||||
|
return this.getApiDetailResp(apiDetail, pid);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getApiDetailsById(apiId, projectId) {
|
||||||
|
const pid = projectId || this.defaultProjectId;
|
||||||
|
const apiDetails = await this.getApiDetails(pid);
|
||||||
|
const apiDetail = apiDetails.find((item) => item.id === apiId);
|
||||||
|
if (!apiDetail) {
|
||||||
|
throw new Error(`API with id ${apiId} not found`);
|
||||||
|
}
|
||||||
|
return this.getApiDetailResp(apiDetail, pid);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// TODO:
|
||||||
|
async getApiListByFolderName(folderName, projectId) {
|
||||||
|
const pid = projectId || this.defaultProjectId;
|
||||||
|
const folder = await this.getFolderByName(folderName, pid);
|
||||||
|
const apiDetails = await this.getApiDetails(pid);
|
||||||
|
const apiList = apiDetails.filter((item) => item.folderId === folder.id);
|
||||||
|
return Promise.all(apiList.map((api) => this.getApiDetailResp(api, pid)));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -1,234 +1,196 @@
|
|||||||
#!/usr/bin/env node
|
#!/usr/bin/env node
|
||||||
// 引入必要的模块
|
// 引入必要的模块
|
||||||
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
||||||
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
||||||
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
||||||
import http from "http";
|
import http from "http";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { ApifoxService } from './apifox.js'
|
import { AsyncLocalStorage } from 'async_hooks';
|
||||||
|
import { ApifoxService } from './apifox.js'
|
||||||
// 创建 MCP 服务器实例
|
|
||||||
// 为其设置一个名字和版本
|
const requestContext = new AsyncLocalStorage();
|
||||||
const server = new McpServer(
|
|
||||||
{
|
// 创建 MCP 服务器实例
|
||||||
name: "apifox-mcp-server",
|
// 为其设置一个名字和版本
|
||||||
version: "1.0.0",
|
const server = new McpServer(
|
||||||
},
|
{
|
||||||
{
|
name: "apifox-mcp-server",
|
||||||
capabilities: {
|
version: "1.0.0",
|
||||||
prompts: {},
|
},
|
||||||
tools: {},
|
{
|
||||||
},
|
capabilities: {
|
||||||
}
|
prompts: {},
|
||||||
);
|
tools: {},
|
||||||
|
},
|
||||||
|
}
|
||||||
const apifoxService = new ApifoxService({
|
);
|
||||||
projectId: Number(process.env.APIFOX_PROJECT_ID),
|
|
||||||
token: process.env.APIFOX_TOKEN
|
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);
|
||||||
server.registerTool(
|
|
||||||
'get_api_by_path',
|
if (!token) {
|
||||||
{
|
throw new Error("Apifox Token not configured (env: APIFOX_TOKEN or header: Authorization)");
|
||||||
description: "根据接口path获取Apifox接口定义数据",
|
}
|
||||||
inputSchema: z.object({
|
|
||||||
path: z.string().describe("接口路径"),
|
return new ApifoxService({
|
||||||
projectId: z.number().optional().describe("Apifox项目ID"),
|
projectId,
|
||||||
}),
|
token
|
||||||
},
|
});
|
||||||
|
}
|
||||||
async ({ path, projectId }) => {
|
|
||||||
try {
|
server.registerTool(
|
||||||
|
'get_api_by_path',
|
||||||
const apiData = await apifoxService.getApiDetailByPath(path, projectId);
|
{
|
||||||
|
description: "根据接口path获取Apifox接口定义数据",
|
||||||
return {
|
inputSchema: z.object({
|
||||||
content: [
|
path: z.string().describe("接口路径"),
|
||||||
{
|
projectId: z.number().optional().describe("Apifox项目ID"),
|
||||||
type: "text",
|
}),
|
||||||
text: JSON.stringify(apiData, null, 2),
|
},
|
||||||
},
|
|
||||||
],
|
async ({ path, projectId }) => {
|
||||||
};
|
try {
|
||||||
|
|
||||||
} catch (error) {
|
const apifoxService = getApifoxService();
|
||||||
return {
|
const apiData = await apifoxService.getApiDetailByPath(path, projectId);
|
||||||
content: [
|
|
||||||
{
|
return {
|
||||||
type: "text",
|
content: [
|
||||||
text: `获取接口详情失败: ${error.message}`,
|
{
|
||||||
},
|
type: "text",
|
||||||
],
|
text: JSON.stringify(apiData, null, 2),
|
||||||
isError: true,
|
},
|
||||||
};
|
],
|
||||||
}
|
};
|
||||||
}
|
|
||||||
)
|
} catch (error) {
|
||||||
server.registerTool(
|
return {
|
||||||
'get_apis_by_folder',
|
content: [
|
||||||
{
|
{
|
||||||
description: "根据接口目录获取Apifox目录下所有接口定义数据",
|
type: "text",
|
||||||
inputSchema: z.object({
|
text: `获取接口详情失败: ${error.message}`,
|
||||||
folderName: z.string().describe("接口路径"),
|
},
|
||||||
projectId: z.number().optional().describe("Apifox项目ID"),
|
],
|
||||||
}),
|
isError: true,
|
||||||
},
|
};
|
||||||
|
}
|
||||||
async ({ folderName, projectId }) => {
|
}
|
||||||
try {
|
)
|
||||||
|
server.registerTool(
|
||||||
const apiData = await apifoxService.getApiListByFolderName(folderName, projectId);
|
'get_apis_by_folder',
|
||||||
|
{
|
||||||
return {
|
description: "根据接口目录获取Apifox目录下所有接口定义数据",
|
||||||
content: [
|
inputSchema: z.object({
|
||||||
{
|
folderName: z.string().describe("接口路径"),
|
||||||
type: "text",
|
projectId: z.number().optional().describe("Apifox项目ID"),
|
||||||
text: JSON.stringify(apiData, null, 2),
|
}),
|
||||||
},
|
},
|
||||||
],
|
|
||||||
};
|
async ({ folderName, projectId }) => {
|
||||||
|
try {
|
||||||
} catch (error) {
|
|
||||||
return {
|
const apifoxService = getApifoxService();
|
||||||
content: [
|
const apiData = await apifoxService.getApiListByFolderName(folderName, projectId);
|
||||||
{
|
|
||||||
type: "text",
|
return {
|
||||||
text: `获取接口数据失败: ${error.message}`,
|
content: [
|
||||||
},
|
{
|
||||||
],
|
type: "text",
|
||||||
isError: true,
|
text: JSON.stringify(apiData, null, 2),
|
||||||
};
|
},
|
||||||
}
|
],
|
||||||
}
|
};
|
||||||
)
|
|
||||||
|
} catch (error) {
|
||||||
|
return {
|
||||||
|
content: [
|
||||||
// 定义 prompts
|
{
|
||||||
server.registerPrompt(
|
type: "text",
|
||||||
'gen-api-by-path',
|
text: `获取接口数据失败: ${error.message}`,
|
||||||
{
|
},
|
||||||
title: 'Generate API by Path',
|
],
|
||||||
description: '根据输入的接口路径获取Apifox接口文档定义,然后根据定义生成 api 接口代码定义',
|
isError: true,
|
||||||
},
|
};
|
||||||
() => ({
|
}
|
||||||
messages: [
|
}
|
||||||
{
|
)
|
||||||
role: "user",
|
|
||||||
content: {
|
|
||||||
type: "text",
|
// 启动服务器,使用标准输入/输出(stdio)与客户端通信
|
||||||
text: `请根据以下步骤获取Apifox的接口定义并生成 api 定义代码。
|
async function main() {
|
||||||
|
const args = process.argv.slice(2);
|
||||||
步骤:
|
const transportArgIndex = args.indexOf('--transport') !== -1 ? args.indexOf('--transport') : args.indexOf('-t');
|
||||||
1. 使用 get_api_by_path 工具获取对应的接口定义schema
|
const transportType = transportArgIndex !== -1 ? args[transportArgIndex + 1] : 'stdio';
|
||||||
2. 根据返回的 path,method, requestSchema, responseSchema 和其他必要字段,生成 api 定义代码
|
|
||||||
3. 接口的参数和返回值的类型定义,可以分别参考 requestSchema 和 responseSchema 里面的jsonSchema定义,可以根据description要补充相关的注释
|
const portArgIndex = args.indexOf('--port') !== -1 ? args.indexOf('--port') : args.indexOf('-p');
|
||||||
4. 所有字段先定义为可空类型
|
const port = portArgIndex !== -1 ? parseInt(args[portArgIndex + 1]) : 8889;
|
||||||
5. 请根据当前项目的项目接口和代码风格生成 api 接口代码
|
|
||||||
|
|
||||||
请开始执行这些步骤。`,
|
|
||||||
},
|
if (transportType === 'http') {
|
||||||
},
|
const transport = new StreamableHTTPServerTransport({});
|
||||||
],
|
await server.connect(transport);
|
||||||
})
|
|
||||||
);
|
const httpServer = http.createServer(async (req, res) => {
|
||||||
|
// 设置 CORS 头
|
||||||
server.registerPrompt(
|
res.setHeader('Access-Control-Allow-Origin', '*');
|
||||||
'gen-api-by-folder',
|
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
|
||||||
{
|
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization, mcp-session-id');
|
||||||
title: 'Generate API by Folder',
|
|
||||||
description: '根据输入的接口文件夹获取Apifox相关文件夹下所有的接口文档定义,然后根据定义生成 api 接口代码定义',
|
if (req.method === 'OPTIONS') {
|
||||||
},
|
res.writeHead(204);
|
||||||
() => ({
|
res.end();
|
||||||
messages: [
|
return;
|
||||||
{
|
}
|
||||||
role: "user",
|
|
||||||
content: {
|
if (req.method === 'GET' && req.url === '/health') {
|
||||||
type: "text",
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||||
text: `请根据以下步骤获取Apifox的接口定义并生成 api 定义代码。
|
res.end(JSON.stringify({ status: 'ok' }));
|
||||||
|
return;
|
||||||
步骤:
|
}
|
||||||
1. 使用 get_api_by_folder 工具获取对应的接口定义schema,可能会返回多条接口的数据
|
|
||||||
2. 根据返回的 path,method, requestSchema, responseSchema 和其他必要字段,生成 api 定义代码
|
const authHeader = req.headers['authorization'];
|
||||||
3. 接口的参数和返回值的类型定义,可以分别参考 requestSchema 和 responseSchema 里面的jsonSchema定义,可以根据description要补充相关的注释
|
const token = authHeader && authHeader.startsWith('Bearer ') ? authHeader.slice(7) : null;
|
||||||
4. 所有字段先定义为可空类型
|
const projectId = req.headers['x-apifox-project-id'];
|
||||||
5. 请根据当前项目的项目接口和代码风格生成 api 接口代码
|
|
||||||
|
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) {
|
||||||
// 启动服务器,使用标准输入/输出(stdio)与客户端通信
|
res.writeHead(500);
|
||||||
|
res.end("Internal Server Error");
|
||||||
// 启动服务器,使用标准输入/输出(stdio)与客户端通信
|
}
|
||||||
async function main() {
|
}
|
||||||
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';
|
|
||||||
|
httpServer.listen(port, () => {
|
||||||
const portArgIndex = args.indexOf('--port') !== -1 ? args.indexOf('--port') : args.indexOf('-p');
|
console.error(`Apifox MCP 服务器已通过 http 启动,监听端口 ${port}`);
|
||||||
const port = portArgIndex !== -1 ? parseInt(args[portArgIndex + 1]) : 8889;
|
console.error(`MCP 服务 endpoint: http://localhost:${port}/sse`);
|
||||||
|
console.error(`MCP 消息 endpoint: http://localhost:${port}/messages`);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
if (transportType === 'http') {
|
|
||||||
const transport = new StreamableHTTPServerTransport({});
|
const transport = new StdioServerTransport();
|
||||||
await server.connect(transport);
|
await server.connect(transport);
|
||||||
|
console.error("Apifox MCP 服务器已通过 stdio 启动"); // 日志输出到 stderr
|
||||||
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') {
|
main().catch((error) => {
|
||||||
res.writeHead(204);
|
console.error("服务器运行出现致命错误:", error);
|
||||||
res.end();
|
process.exit(1);
|
||||||
return;
|
});
|
||||||
}
|
|
||||||
|
|
||||||
if (req.method === 'GET' && req.url === '/health') {
|
|
||||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
||||||
res.end(JSON.stringify({ status: 'ok' }));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
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`);
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
|
|
||||||
const transport = new StdioServerTransport();
|
|
||||||
await server.connect(transport);
|
|
||||||
console.error("Apifox MCP 服务器已通过 stdio 启动"); // 日志输出到 stderr
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// 运行主函数,并捕获可能的错误
|
|
||||||
main().catch((error) => {
|
|
||||||
console.error("服务器运行出现致命错误:", error);
|
|
||||||
process.exit(1);
|
|
||||||
});
|
|
||||||
|
|
||||||
export { server };
|
export { server };
|
||||||
+29
-29
@@ -1,29 +1,29 @@
|
|||||||
{
|
{
|
||||||
"name": "apifox-mcp-server",
|
"name": "apifox-mcp-server",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "node index.js",
|
"start": "node index.js",
|
||||||
"dev": "npx @modelcontextprotocol/inspector node index.js"
|
"dev": "npx @modelcontextprotocol/inspector node index.js"
|
||||||
},
|
},
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"mcp",
|
"mcp",
|
||||||
"apifox"
|
"apifox"
|
||||||
],
|
],
|
||||||
"author": "Jasery",
|
"author": "Jasery",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"description": "A Model Context Protocol server for Apifox",
|
"description": "A Model Context Protocol server for Apifox",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@modelcontextprotocol/sdk": "^1.22.0",
|
"@modelcontextprotocol/sdk": "^1.22.0",
|
||||||
"axios": "^1.13.2",
|
"axios": "^1.13.2",
|
||||||
"node-cache": "^5.1.2",
|
"node-cache": "^5.1.2",
|
||||||
"zod": "^3.25.76"
|
"zod": "^3.25.76"
|
||||||
},
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
"apifox-mcp-server": "./index.js"
|
"apifox-mcp-server": "./index.js"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@modelcontextprotocol/inspector": "^0.17.2"
|
"@modelcontextprotocol/inspector": "^0.17.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Reference in new issue
Block a user