feat: refactor for multi-tenant HTTP support and global caching

This commit is contained in:
Gemini Agent committed 2026-01-14 18:56:03 +08:00
1 parent 30d4c8a9da
commit 1ea6e0e9f0
4 files changed
+249 -151

No files matched your search

+4
View File
@@ -1 +1,5 @@
node_modules node_modules
ecosystem.config.js
*.log
_data
+65
View File
@@ -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 消息。
+152 -85
View File
@@ -5,11 +5,19 @@ import path from 'path';
const APIFOX_API_BASE = "https://apifox.gzmiyuan.com/api/v1"; const APIFOX_API_BASE = "https://apifox.gzmiyuan.com/api/v1";
// 初始化node-cache,设置默认5分钟过期时间
// 全局共享缓存,避免每次实例化清空缓存
const globalCache = new NodeCache({
stdTTL: 5 * 60, // 5分钟,单位秒
checkperiod: 60, // 每分钟检查一次过期缓存
useClones: false // 创建缓存对象的克隆以避免引用问题
});
export class ApifoxService { export class ApifoxService {
constructor(config = {}) { constructor(config = {}) {
const { projectId, token, baseUrl, logFilePath } = config; const { projectId, token, baseUrl, logFilePath } = config;
this.projectId = projectId; this.defaultProjectId = projectId;
this.branchId = null; this.projectBranchIds = new Map(); // Cache branchIds: projectId -> branchId
this.apiTreeList = null; this.apiTreeList = null;
this.logFilePath = logFilePath || path.join(process.cwd(), 'apifox-service.log'); this.logFilePath = logFilePath || path.join(process.cwd(), 'apifox-service.log');
this.http = axios.create({ this.http = axios.create({
@@ -22,12 +30,7 @@ export class ApifoxService {
} }
}); });
// 初始化node-cache,设置默认5分钟过期时间 this.cache = globalCache;
this.cache = new NodeCache({
stdTTL: 5 * 60, // 5分钟,单位秒
checkperiod: 60, // 每分钟检查一次过期缓存
useClones: false // 创建缓存对象的克隆以避免引用问题
});
this.init(); this.init();
} }
@@ -89,35 +92,25 @@ export class ApifoxService {
} }
} }
/** async init() {
* 初始化服务,获取branchId // Pre-fetch branchId for default project if set
* @returns {Promise<void>} if (this.defaultProjectId) {
*/ await this.getBranchId(this.defaultProjectId);
async init(projectId) {
if (projectId) {
this.projectId = projectId;
this.branchId = await this.getBranchId();
} }
} }
/** /**
* 发送GET请求 * 发送GET请求
* @param {string} url - 请求的URL路径 * @param {string} url - 请求的URL路径
* @param {object} config - axios配置对象 * @param {object} config - axios配置对象 (contains headers with projectId/branchId)
* @returns {Promise} 返回响应数据中的data字段 * @returns {Promise} 返回响应数据中的data字段
*/ */
async get(url, config = {}) { async get(url, config = {}) {
try { try {
const response = await this.http.get(url, { const response = await this.http.get(url, config);
...config,
headers: {
...config.headers,
"X-Project-Id": this.projectId,
"X-Branch-Id": this.branchId,
},
});
return response.data; return response.data;
} catch (error) { } catch (error) {
// Enhance error logging if needed
throw error; throw error;
} }
} }
@@ -131,35 +124,78 @@ export class ApifoxService {
*/ */
async post(url, data = {}, config = {}) { async post(url, data = {}, config = {}) {
try { try {
const response = await this.http.post(url, data, { const response = await this.http.post(url, data, config);
...config,
headers: {
...config.headers,
"X-Project-Id": this.projectId,
"X-Branch-Id": this.branchId,
},
});
return response.data; return response.data;
} catch (error) { } catch (error) {
throw error; throw error;
} }
} }
async getRequestConfig(projectId) {
const pid = projectId || this.defaultProjectId;
if (!pid) throw new Error("No Project ID provided");
async getBranchId() { const branchId = await this.getBranchId(pid);
const branches = await this.get(`/projects/${this.projectId}/api-versions`); return {
return branches.data[0].id; headers: {
"X-Project-Id": pid,
"X-Branch-Id": branchId,
}
};
} }
async getApiTreeList() {
const apiTree = await this.get(`/projects/${this.projectId}/api-tree-list`); async getBranchId(projectId) {
this.apiTreeList = apiTree; if (this.projectBranchIds.has(projectId)) {
return this.projectBranchIds.get(projectId);
}
// We need to fetch branch ID.
// BUT fetching branch ID requires Project ID header usually?
// Let's check the original implementation:
// const branches = await this.get(`/projects/${this.projectId}/api-versions`);
// It used `this.http` which had `this.projectId` in headers?
// WAIT, original `get` implementation injected headers dynamically based on `this.projectId`.
// So here we must manually construct headers for this specific request to avoid circular dependency
// or just pass X-Project-Id.
try {
const response = await this.http.get(`/projects/${projectId}/api-versions`, {
headers: {
"X-Project-Id": projectId
}
});
const branchId = response.data.data[0].id;
this.projectBranchIds.set(projectId, branchId);
return branchId;
} catch (error) {
console.error(`Failed to get branch ID for project ${projectId}`, error);
throw error;
}
}
async getApiTreeList(projectId) {
const pid = projectId || this.defaultProjectId;
const cacheKey = `${pid}:apiTreeList`;
const cachedData = this.cache.get(cacheKey);
if (cachedData) {
return cachedData;
}
const config = await this.getRequestConfig(pid);
const apiTree = await this.get(`/projects/${pid}/api-tree-list`, config);
this.cache.set(cacheKey, apiTree);
return apiTree; return apiTree;
} }
async getFolderList() { async getFolderList(projectId) {
const pid = projectId || this.defaultProjectId;
// 定义缓存键 // 定义缓存键
const cacheKey = 'folderList'; const cacheKey = `${pid}:folderList`;
// 检查缓存是否有效 // 检查缓存是否有效
const cachedData = this.cache.get(cacheKey); const cachedData = this.cache.get(cacheKey);
@@ -167,24 +203,26 @@ export class ApifoxService {
return cachedData; return cachedData;
} }
const {data: folderList} = await this.get(`/projects/${this.projectId}/api-detail-folders`); const config = await this.getRequestConfig(pid);
const {data: folderList} = await this.get(`/projects/${pid}/api-detail-folders`, config);
// 设置缓存 // 设置缓存
this.cache.set(cacheKey, folderList); this.cache.set(cacheKey, folderList);
return folderList; return folderList;
} }
async getFolderByName(folderName) { async getFolderByName(folderName, projectId) {
const folderList = await this.getFolderList(); const folderList = await this.getFolderList(projectId);
const folder = folderList.find((item) => item.name === folderName); const folder = folderList.find((item) => item.name === folderName);
if (!folder) { if (!folder) {
throw new Error(`Folder with name ${folderName} not found`); throw new Error(`Folder with name ${folderName} not found`);
} }
return folder; return folder;
} }
async getDataSchema() { async getDataSchema(projectId) {
const pid = projectId || this.defaultProjectId;
// 定义缓存键 // 定义缓存键
const cacheKey = 'dataSchema'; const cacheKey = `${pid}:dataSchema`;
// 检查缓存是否有效 // 检查缓存是否有效
const cachedData = this.cache.get(cacheKey); const cachedData = this.cache.get(cacheKey);
@@ -192,16 +230,18 @@ export class ApifoxService {
return cachedData; return cachedData;
} }
const {data:schemasTreeList} = await this.get(`/projects/${this.projectId}/data-schemas?locale=zh-CN`); const config = await this.getRequestConfig(pid);
const {data:schemasTreeList} = await this.get(`/projects/${pid}/data-schemas?locale=zh-CN`, config);
// 设置缓存 // 设置缓存
this.cache.set(cacheKey, schemasTreeList); this.cache.set(cacheKey, schemasTreeList);
return schemasTreeList; return schemasTreeList;
} }
async getApiDetails() { async getApiDetails(projectId) {
const pid = projectId || this.defaultProjectId;
// 定义缓存键 // 定义缓存键
const cacheKey = 'apiDetails'; const cacheKey = `${pid}:apiDetails`;
// 检查缓存是否有效 // 检查缓存是否有效
const cachedData = this.cache.get(cacheKey); const cachedData = this.cache.get(cacheKey);
@@ -209,7 +249,8 @@ export class ApifoxService {
return cachedData; return cachedData;
} }
const {data:apiDetails} = await this.get(`/api-details?locale=zh-CN`, {}); const config = await this.getRequestConfig(pid);
const {data:apiDetails} = await this.get(`/api-details?locale=zh-CN`, config);
// 设置缓存 // 设置缓存
this.cache.set(cacheKey, apiDetails); this.cache.set(cacheKey, apiDetails);
@@ -227,17 +268,21 @@ export class ApifoxService {
/** /**
* 根据schema ID获取完整的schema定义,递归替换所有$ref引用 * 根据schema ID获取完整的schema定义,递归替换所有$ref引用
* @param {number|string} schemaId - schema的ID * @param {number|string} schemaId - schema的ID
* @param {Array} allSchemas - 所有schema数据数组(可选,用于递归) * @param {object} context - 上下文对象 { projectId, allSchemas, visitedIds }
* @param {Set} visitedIds - 已访问的ID集合,用于防止循环引用
* @returns {Promise<object>} 返回完整解析后的schema对象 * @returns {Promise<object>} 返回完整解析后的schema对象
*/ */
async getSchemaById(schemaId, allSchemas = null, visitedIds = new Set()) { async getSchemaById(schemaId, context = {}) {
if (!schemaId) { if (!schemaId) {
return null; return null;
} }
let { projectId, allSchemas, visitedIds } = context;
visitedIds = visitedIds || new Set();
// 如果没有传入allSchemas,则获取所有schema数据 // 如果没有传入allSchemas,则获取所有schema数据
if (!allSchemas) { if (!allSchemas) {
allSchemas = await this.getDataSchema(); allSchemas = await this.getDataSchema(projectId);
context.allSchemas = allSchemas; // Update context for recursion
} }
// 查找对应ID的schema // 查找对应ID的schema
@@ -252,18 +297,21 @@ export class ApifoxService {
} }
visitedIds.add(schemaId.toString()); 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)
// 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.
// However, existing code passed `new Set(visitedIds)` to resolveRefs.
// 深拷贝schema以避免修改原始数据 // 深拷贝schema以避免修改原始数据
const resolvedSchema = JSON.parse(JSON.stringify(schema)); const resolvedSchema = JSON.parse(JSON.stringify(schema));
// 递归替换jsonSchema中的所有$ref // 递归替换jsonSchema中的所有$ref
resolvedSchema.jsonSchema = await this.resolveRefs( resolvedSchema.jsonSchema = await this.resolveRefs(
resolvedSchema.jsonSchema, resolvedSchema.jsonSchema,
allSchemas, { ...context, visitedIds: new Set(visitedIds) }
new Set(visitedIds)
); );
// 移除不需要的字段 // 移除不需要的字段
const { description, folderId, projectId, moduleId, ordering, creatorId, editorId, createdAt, updatedAt, visibility, ...cleanSchema } = resolvedSchema; const { description, folderId, projectId: pid, moduleId, ordering, creatorId, editorId, createdAt, updatedAt, visibility, ...cleanSchema } = resolvedSchema;
return cleanSchema; return cleanSchema;
} }
@@ -271,11 +319,10 @@ export class ApifoxService {
/** /**
* 递归解析对象中的所有$ref引用 * 递归解析对象中的所有$ref引用
* @param {object} obj - 要解析的对象 * @param {object} obj - 要解析的对象
* @param {Array} allSchemas - 所有schema数据数组 * @param {object} context - 上下文对象 { projectId, allSchemas, visitedIds }
* @param {Set} visitedIds - 已访问的ID集合
* @returns {Promise<object>} 返回解析后的对象 * @returns {Promise<object>} 返回解析后的对象
*/ */
async resolveRefs(obj, allSchemas, visitedIds) { async resolveRefs(obj, context) {
if (!obj || typeof obj !== 'object') { if (!obj || typeof obj !== 'object') {
return obj; return obj;
} }
@@ -284,7 +331,7 @@ export class ApifoxService {
if (Array.isArray(obj)) { if (Array.isArray(obj)) {
const resolved = []; const resolved = [];
for (const item of obj) { for (const item of obj) {
resolved.push(await this.resolveRefs(item, allSchemas, visitedIds)); resolved.push(await this.resolveRefs(item, context));
} }
return resolved; return resolved;
} }
@@ -292,7 +339,8 @@ export class ApifoxService {
// 如果对象包含$ref,替换为实际的schema定义 // 如果对象包含$ref,替换为实际的schema定义
if (obj.$ref) { if (obj.$ref) {
const refId = this.getSchemaRefId(obj); const refId = this.getSchemaRefId(obj);
const referencedSchema = await this.getSchemaById(refId, allSchemas, visitedIds); // Calls getSchemaById with context
const referencedSchema = await this.getSchemaById(refId, context);
// 返回引用的jsonSchema内容 // 返回引用的jsonSchema内容
return referencedSchema.jsonSchema; return referencedSchema.jsonSchema;
} }
@@ -300,19 +348,40 @@ export class ApifoxService {
// 递归处理对象的所有属性 // 递归处理对象的所有属性
const resolved = {}; const resolved = {};
for (const [key, value] of Object.entries(obj)) { for (const [key, value] of Object.entries(obj)) {
resolved[key] = await this.resolveRefs(value, allSchemas, visitedIds); resolved[key] = await this.resolveRefs(value, context);
} }
return resolved; return resolved;
} }
async getApiDetailResp(apiDetail) { async getApiDetailResp(apiDetail, projectId) {
const dataSchema = await this.getDataSchema(); const pid = projectId || this.defaultProjectId;
const dataSchema = await this.getDataSchema(pid);
const context = { projectId: pid, allSchemas: dataSchema };
const requestSchemaId = this.getSchemaRefId(apiDetail.requestBody.jsonSchema); const requestSchemaId = this.getSchemaRefId(apiDetail.requestBody.jsonSchema);
const responseSchema = Array.isArray(apiDetail.responses) ? apiDetail.responses[0].jsonSchema : apiDetail.responses.jsonSchema; const responseSchema = Array.isArray(apiDetail.responses) ? apiDetail.responses[0].jsonSchema : apiDetail.responses.jsonSchema;
const responseSchemaId = this.getSchemaRefId(responseSchema); const responseSchemaId = this.getSchemaRefId(responseSchema);
const responseDataSchema = dataSchema.find((item) => item.id.toString() === responseSchemaId).jsonSchema.properties.data;
// Find the response data schema property
let responseDataSchema;
try {
const schema = dataSchema.find((item) => item.id.toString() === responseSchemaId);
if (schema && schema.jsonSchema && schema.jsonSchema.properties) {
responseDataSchema = schema.jsonSchema.properties.data;
}
} catch (e) {
// 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; let respDataSchemaId;
if (responseDataSchema) {
const isArray = responseDataSchema.type === 'array'; const isArray = responseDataSchema.type === 'array';
// 处理数组和对象两种情况 // 处理数组和对象两种情况
if (isArray && responseDataSchema.items && responseDataSchema.items.$ref) { if (isArray && responseDataSchema.items && responseDataSchema.items.$ref) {
@@ -326,51 +395,49 @@ export class ApifoxService {
this.logToFile('WARN', `无法从 responseDataSchema 中提取 schema ID`, responseDataSchema); this.logToFile('WARN', `无法从 responseDataSchema 中提取 schema ID`, responseDataSchema);
respDataSchemaId = null; respDataSchemaId = null;
} }
}
return { return {
path: apiDetail.path, path: apiDetail.path,
name: apiDetail.name, name: apiDetail.name,
method: apiDetail.method, method: apiDetail.method,
url: apiDetail.url, url: apiDetail.url,
requestSchema: await this.getSchemaById(requestSchemaId), requestSchema: await this.getSchemaById(requestSchemaId, context),
responseSchema: isArray ? { responseSchema: (responseDataSchema && responseDataSchema.type === 'array') ? {
type: 'array', type: 'array',
items: respDataSchemaId ? await this.getSchemaById(respDataSchemaId) : null, items: respDataSchemaId ? await this.getSchemaById(respDataSchemaId, context) : null,
} : await this.getSchemaById(respDataSchemaId), } : await this.getSchemaById(respDataSchemaId, context),
}; };
} }
async getApiDetailByPath(path, projectId) { async getApiDetailByPath(path, projectId) {
if (projectId) { const pid = projectId || this.defaultProjectId;
await this.init(projectId); const apiDetails = await this.getApiDetails(pid);
}
const apiDetails = await this.getApiDetails();
const apiDetail = apiDetails.find((item) => item.path === path); const apiDetail = apiDetails.find((item) => item.path === path);
if (!apiDetail) { if (!apiDetail) {
throw new Error(`API with path ${path} not found`); throw new Error(`API with path ${path} not found`);
} }
return this.getApiDetailResp(apiDetail); return this.getApiDetailResp(apiDetail, pid);
} }
async getApiDetailsById(apiId) { async getApiDetailsById(apiId, projectId) {
const apiDetails = await this.getApiDetails(); const pid = projectId || this.defaultProjectId;
const apiDetails = await this.getApiDetails(pid);
const apiDetail = apiDetails.find((item) => item.id === apiId); const apiDetail = apiDetails.find((item) => item.id === apiId);
if (!apiDetail) { if (!apiDetail) {
throw new Error(`API with id ${id} not found`); throw new Error(`API with id ${apiId} not found`);
} }
return this.getApiDetailResp(apiDetail); return this.getApiDetailResp(apiDetail, pid);
} }
// TODO: // TODO:
async getApiListByFolderName(folderName, projectId) { async getApiListByFolderName(folderName, projectId) {
if (projectId) { const pid = projectId || this.defaultProjectId;
await this.init(projectId); const folder = await this.getFolderByName(folderName, pid);
} const apiDetails = await this.getApiDetails(pid);
const folder = await this.getFolderByName(folderName);
const apiDetails = await this.getApiDetails();
const apiList = apiDetails.filter((item) => item.folderId === folder.id); const apiList = apiDetails.filter((item) => item.folderId === folder.id);
return Promise.all(apiList.map((api) => this.getApiDetailResp(api))); return Promise.all(apiList.map((api) => this.getApiDetailResp(api, pid)));
} }
} }
+28 -66
View File
@@ -5,8 +5,11 @@ 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 { AsyncLocalStorage } from 'async_hooks';
import { ApifoxService } from './apifox.js' import { ApifoxService } from './apifox.js'
const requestContext = new AsyncLocalStorage();
// 创建 MCP 服务器实例 // 创建 MCP 服务器实例
// 为其设置一个名字和版本 // 为其设置一个名字和版本
const server = new McpServer( const server = new McpServer(
@@ -22,12 +25,20 @@ const server = new McpServer(
} }
); );
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);
const apifoxService = new ApifoxService({ if (!token) {
projectId: Number(process.env.APIFOX_PROJECT_ID), throw new Error("Apifox Token not configured (env: APIFOX_TOKEN or header: Authorization)");
token: process.env.APIFOX_TOKEN }
return new ApifoxService({
projectId,
token
}); });
}
server.registerTool( server.registerTool(
'get_api_by_path', 'get_api_by_path',
@@ -42,6 +53,7 @@ server.registerTool(
async ({ path, projectId }) => { async ({ path, projectId }) => {
try { try {
const apifoxService = getApifoxService();
const apiData = await apifoxService.getApiDetailByPath(path, projectId); const apiData = await apifoxService.getApiDetailByPath(path, projectId);
return { return {
@@ -79,6 +91,7 @@ server.registerTool(
async ({ folderName, projectId }) => { async ({ folderName, projectId }) => {
try { try {
const apifoxService = getApifoxService();
const apiData = await apifoxService.getApiListByFolderName(folderName, projectId); const apiData = await apifoxService.getApiListByFolderName(folderName, projectId);
return { return {
@@ -105,68 +118,6 @@ server.registerTool(
) )
// 定义 prompts
server.registerPrompt(
'gen-api-by-path',
{
title: 'Generate API by Path',
description: '根据输入的接口路径获取Apifox接口文档定义,然后根据定义生成 api 接口代码定义',
},
() => ({
messages: [
{
role: "user",
content: {
type: "text",
text: `请根据以下步骤获取Apifox的接口定义并生成 api 定义代码。
步骤:
1. 使用 get_api_by_path 工具获取对应的接口定义schema
2. 根据返回的 path,method, requestSchema, responseSchema 和其他必要字段,生成 api 定义代码
3. 接口的参数和返回值的类型定义,可以分别参考 requestSchema 和 responseSchema 里面的jsonSchema定义,可以根据description要补充相关的注释
4. 所有字段先定义为可空类型
5. 请根据当前项目的项目接口和代码风格生成 api 接口代码
请开始执行这些步骤。`,
},
},
],
})
);
server.registerPrompt(
'gen-api-by-folder',
{
title: 'Generate API by Folder',
description: '根据输入的接口文件夹获取Apifox相关文件夹下所有的接口文档定义,然后根据定义生成 api 接口代码定义',
},
() => ({
messages: [
{
role: "user",
content: {
type: "text",
text: `请根据以下步骤获取Apifox的接口定义并生成 api 定义代码。
步骤:
1. 使用 get_api_by_folder 工具获取对应的接口定义schema,可能会返回多条接口的数据
2. 根据返回的 path,method, requestSchema, responseSchema 和其他必要字段,生成 api 定义代码
3. 接口的参数和返回值的类型定义,可以分别参考 requestSchema 和 responseSchema 里面的jsonSchema定义,可以根据description要补充相关的注释
4. 所有字段先定义为可空类型
5. 请根据当前项目的项目接口和代码风格生成 api 接口代码
请开始执行这些步骤。`,
},
},
],
})
);
// 启动服务器,使用标准输入/输出(stdio)与客户端通信
// 启动服务器,使用标准输入/输出(stdio)与客户端通信 // 启动服务器,使用标准输入/输出(stdio)与客户端通信
async function main() { async function main() {
const args = process.argv.slice(2); const args = process.argv.slice(2);
@@ -200,6 +151,15 @@ async function main() {
return; 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 { try {
await transport.handleRequest(req, res); await transport.handleRequest(req, res);
} catch (error) { } catch (error) {
@@ -210,10 +170,12 @@ async function main() {
} }
} }
}); });
});
httpServer.listen(port, () => { httpServer.listen(port, () => {
console.error(`Apifox MCP 服务器已通过 http 启动,监听端口 ${port}`); console.error(`Apifox MCP 服务器已通过 http 启动,监听端口 ${port}`);
console.error(`MCP 服务 endpoint: http://localhost:${port}/sse`); console.error(`MCP 服务 endpoint: http://localhost:${port}/sse`);
console.error(`MCP 消息 endpoint: http://localhost:${port}/messages`);
}); });
} else { } else {