From ad543045c7365bff36afe8463509dcdc6c2f6288 Mon Sep 17 00:00:00 2001 From: zhangguoyun Date: Sat, 29 Nov 2025 19:16:13 +0800 Subject: [PATCH] refactor: Restructure ApifoxService with dependency injection and MCP integration - Move APIFOX_TOKEN to index.js and pass via config object to ApifoxService - Convert ApifoxService to accept config object with projectId, token, and baseUrl parameters - Move axios instance creation from module level into ApifoxService constructor - Replace global http client references with instance-based this.http throughout service methods - Initialize ApifoxService in index.js with environment variables - Remove GitLab API client code and replace with Apifox-focused tool implementations - Simplify MCP server tool registration to use ApifoxService methods - Add init() call in ApifoxService constructor for future initialization logic - Improve code organization by centralizing API client configuration within service class - Enable better testability and configuration flexibility through dependency injection pattern --- apifox.js | 33 ++-- index.js | 482 +++++++++++++++--------------------------------------- 2 files changed, 150 insertions(+), 365 deletions(-) diff --git a/apifox.js b/apifox.js index f32e026..43123f7 100644 --- a/apifox.js +++ b/apifox.js @@ -2,33 +2,32 @@ import axios from "axios"; import NodeCache from 'node-cache'; import fs from 'fs' - const APIFOX_API_BASE = "https://apifox.gzmiyuan.com/api/v1"; -process.env.APIFOX_TOKEN = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MzQ1MTIwLCJ0cyI6IjUyOGZiZDMwNGM3MjU2NDMiLCJpYXQiOjE3NjQxNTQyODEzMzB9.IZvoCb3pN5AUl375c4Dhpur1GdYBqyHWVkJ94bDQ6BQ'; - -const http = axios.create({ - baseURL: APIFOX_API_BASE, - headers: { - "X-Client-Mode": "web", - "X-Client-Version": "2.7.19", - "X-Device-Id": "bd94aea0-7b65-48ba-9274-4b1177897954", - "Authorization": `Bearer ${process.env.APIFOX_TOKEN || ''}` - } -}); - - export class ApifoxService { - constructor(projectId) { + constructor(config = {}) { + const { projectId, token, baseUrl } = config; this.projectId = projectId; this.branchId = null; this.apiTreeList = null; + this.http = axios.create({ + baseURL: baseUrl || APIFOX_API_BASE, + headers: { + "X-Client-Mode": "web", + "X-Client-Version": "2.7.19", + "X-Device-Id": "bd94aea0-7b65-48ba-9274-4b1177897954", + "Authorization": `Bearer ${token}` + } + }); + // 初始化node-cache,设置默认5分钟过期时间 this.cache = new NodeCache({ stdTTL: 5 * 60, // 5分钟,单位秒 checkperiod: 60, // 每分钟检查一次过期缓存 useClones: false // 创建缓存对象的克隆以避免引用问题 }); + + this.init(); } /** @@ -66,7 +65,7 @@ export class ApifoxService { */ async get(url, config = {}) { try { - const response = await http.get(url, { + const response = await this.http.get(url, { ...config, headers: { ...config.headers, @@ -89,7 +88,7 @@ export class ApifoxService { */ async post(url, data = {}, config = {}) { try { - const response = await http.post(url, data, { + const response = await this.http.post(url, data, { ...config, headers: { ...config.headers, diff --git a/index.js b/index.js index 4014f1c..b7dd61a 100644 --- a/index.js +++ b/index.js @@ -2,6 +2,9 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import { z } from "zod"; +import { ApifoxService } from './apifox' + +process.env.APIFOX_TOKEN = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MzQ1MTIwLCJ0cyI6IjUyOGZiZDMwNGM3MjU2NDMiLCJpYXQiOjE3NjQxNTQyODEzMzB9.IZvoCb3pN5AUl375c4Dhpur1GdYBqyHWVkJ94bDQ6BQ'; // 创建 MCP 服务器实例 // 为其设置一个名字和版本 @@ -10,370 +13,77 @@ const server = new McpServer({ version: "1.0.0", }); -/** - * GitLab API 客户端配置 - * 用于连接到GitLab实例并执行操作 - */ -const gitlabApiClient = { - baseUrl: process.env.GITLAB_API_BASE_URL || "https://gitlab.com/api/v4", - token: process.env.GITLAB_ACCESS_TOKEN, - - async makeRequest(endpoint, options = {}) { - if (!this.token) { - throw new Error("GITLAB_ACCESS_TOKEN 环境变量未设置"); - } - - const url = `${this.baseUrl}${endpoint}`; - const defaultOptions = { - method: "GET", - headers: { - "PRIVATE-TOKEN": this.token, - "Content-Type": "application/json", - }, - }; - - const requestOptions = { ...defaultOptions, ...options }; - const response = await fetch(url, requestOptions); - - if (!response.ok) { - throw new Error(`GitLab API请求失败: ${response.status} ${response.statusText}`); - } - - return await response.json(); - }, - - /** - * 创建Merge Request - */ - async createMergeRequest(projectId, sourceBranch, targetBranch, title, description = "", assigneeId, reviewerIds) { - const body = { - id: projectId, - source_branch: sourceBranch, - target_branch: targetBranch, - title, - description, - ...(assigneeId !== undefined && { assignee_id: assigneeId }), - ...(reviewerIds !== undefined && { reviewer_ids: reviewerIds }), - }; - - return await this.makeRequest( - `/projects/${projectId}/merge_requests`, - { - method: "POST", - body: JSON.stringify(body), - headers: { - "PRIVATE-TOKEN": this.token, - "Content-Type": "application/json", - }, - } - ); - }, - - /** - * 合并Merge Request - */ - async mergeMergeRequest(projectId, mrIid, mergeCommitMessage = "", shouldRemoveSourceBranch = false, mergeWhenPipelineSucceeds = false) { - const body = { - id: projectId, - merge_request_iid: mrIid, - ...(mergeCommitMessage && { merge_commit_message: mergeCommitMessage }), - remove_source_branch: shouldRemoveSourceBranch, - merge_when_pipeline_succeeds: mergeWhenPipelineSucceeds, - }; - - return await this.makeRequest( - `/projects/${projectId}/merge_requests/${mrIid}/merge`, - { - method: "PUT", - body: JSON.stringify(body), - headers: { - "PRIVATE-TOKEN": this.token, - "Content-Type": "application/json", - }, - } - ); - }, - - /** - * 获取Merge Request详情 - */ - async getMergeRequest(projectId, mrIid) { - return await this.makeRequest(`/projects/${projectId}/merge_requests/${mrIid}`); - }, - - /** - * 关闭Merge Request - */ - async closeMergeRequest(projectId, mrIid) { - return await this.makeRequest( - `/projects/${projectId}/merge_requests/${mrIid}`, - { - method: "PUT", - body: JSON.stringify({ state_event: "close" }), - headers: { - "PRIVATE-TOKEN": this.token, - "Content-Type": "application/json", - }, - } - ); - } -}; - -/** - * 创建Merge Request工具 - */ -server.registerTool( - "create_merge_request", - { - description: "在指定的GitLab项目中创建一个新的Merge Request", - inputSchema: z.object({ - projectId: z.string().describe("GitLab项目的ID或URL编码的路径"), - sourceBranch: z.string().describe("源分支名称"), - targetBranch: z.string().describe("目标分支名称"), - title: z.string().describe("Merge Request的标题"), - description: z.string().optional().describe("Merge Request的描述"), - assigneeId: z.number().optional().describe("负责人ID"), - reviewerIds: z.array(z.number()).optional().describe("审阅者ID数组"), - }), - }, - async ({ projectId, sourceBranch, targetBranch, title, description = "", assigneeId, reviewerIds }) => { - try { - const result = await gitlabApiClient.createMergeRequest( - projectId, - sourceBranch, - targetBranch, - title, - description, - assigneeId, - reviewerIds - ); - - return { - content: [ - { - type: "text", - text: `成功创建Merge Request: ${result.web_url}\n标题: ${result.title}\n状态: ${result.state}`, - }, - ], - }; - } catch (error) { - return { - content: [ - { - type: "text", - text: `创建Merge Request失败: ${error.message}`, - }, - ], - isError: true, - }; - } - } -); - -/** - * 合并Merge Request工具 - */ -server.registerTool( - "merge_merge_request", - { - description: "合并指定的GitLab Merge Request", - inputSchema: z.object({ - projectId: z.string().describe("GitLab项目的ID或URL编码的路径"), - mrIid: z.number().describe("Merge Request的内部ID"), - mergeCommitMessage: z.string().optional().describe("合并提交的消息"), - shouldRemoveSourceBranch: z.boolean().optional().describe("是否在合并后删除源分支"), - mergeWhenPipelineSucceeds: z.boolean().optional().describe("是否在CI管道成功后合并"), - }), - }, - async ({ - projectId, - mrIid, - mergeCommitMessage = "", - shouldRemoveSourceBranch = false, - mergeWhenPipelineSucceeds = false - }) => { - try { - const result = await gitlabApiClient.mergeMergeRequest( - projectId, - mrIid, - mergeCommitMessage, - shouldRemoveSourceBranch, - mergeWhenPipelineSucceeds - ); - - return { - content: [ - { - type: "text", - text: `成功合并Merge Request #${mrIid}:\n状态: ${result.state}\n已合并时间: ${result.merged_at}`, - }, - ], - }; - } catch (error) { - return { - content: [ - { - type: "text", - text: `合并Merge Request失败: ${error.message}`, - }, - ], - isError: true, - }; - } - } -); - -/** - * 获取Merge Request详情工具 - */ -server.registerTool( - "get_merge_request", - { - description: "获取指定Merge Request的详细信息", - inputSchema: z.object({ - projectId: z.string().describe("GitLab项目的ID或URL编码的路径"), - mrIid: z.number().describe("Merge Request的内部ID"), - }), - }, - async ({ projectId, mrIid }) => { - try { - const result = await gitlabApiClient.getMergeRequest(projectId, mrIid); - - return { - content: [ - { - type: "text", - text: `Merge Request #${mrIid} 详情:\n标题: ${result.title}\n作者: ${result.author.name}\n状态: ${result.state}\n源分支: ${result.source_branch}\n目标分支: ${result.target_branch}\n创建时间: ${result.created_at}\n更新时间: ${result.updated_at}`, - }, - ], - }; - } catch (error) { - return { - content: [ - { - type: "text", - text: `获取Merge Request详情失败: ${error.message}`, - }, - ], - isError: true, - }; - } - } -); - -/** - * 关闭Merge Request工具 - */ -server.registerTool( - "close_merge_request", - { - description: "关闭指定的GitLab Merge Request而不合并", - inputSchema: z.object({ - projectId: z.string().describe("GitLab项目的ID或URL编码的路径"), - mrIid: z.number().describe("Merge Request的内部ID"), - }), - }, - async ({ projectId, mrIid }) => { - try { - const result = await gitlabApiClient.closeMergeRequest(projectId, mrIid); - - return { - content: [ - { - type: "text", - text: `成功关闭Merge Request #${mrIid}:\n标题: ${result.title}\n当前状态: ${result.state}`, - }, - ], - }; - } catch (error) { - return { - content: [ - { - type: "text", - text: `关闭Merge Request失败: ${error.message}`, - }, - ], - isError: true, - }; - } - } -); +const apifoxService = new ApifoxService({ + projectId: process.env.APIFOX_PROJECT_ID, + token: process.env.APIFOX_TOKEN +}); server.registerTool( - 'merge_to_test', + 'get_api_by_path', { - description: "把源分支名合并到测试分支,并部署到测试环境", + description: "根据接口path获取Apifox接口定义数据", inputSchema: z.object({ - projectId: z.string().describe("GitLab项目projectId"), - sourceBranch: z.string().describe("源分支名称,默认请使用git branch --show-current获取当前分支传递"), + path: z.string().describe("接口路径"), }), }, - async ({ projectId, sourceBranch, }) => { - if (!sourceBranch) { - // 获取当前分支,用 git branch --show-current 命令 - const { execSync } = await import('child_process'); - try { - sourceBranch = execSync('git branch --show-current', { encoding: 'utf-8' }).trim(); - } catch (error) { - console.error('执行 git branch --show-current 命令失败:', error); - return { - content: [ - { - type: "text", - text: `获取当前分支失败: ${error.message}`, - }, - ], - isError: true, - }; - } - } + async ({ path }) => { try { - const result = await gitlabApiClient.createMergeRequest( - projectId, - sourceBranch, - 'test', - `Merge ${sourceBranch} to test`, // 标题 - `Automated merge request from ${sourceBranch} to test branch` // 描述 - ); - const mrIid = result.iid; + const apiData = apifoxService.getApiDetailByPath(path); - // 等待3秒钟,让GitLab有时间处理新创建的MR - await new Promise(resolve => setTimeout(resolve, 3000)); + return { + content: [ + { + type: "text", + text: JSON.stringify(apiData, null, 2), + }, + ], + }; - // 尝试合并MR - try { - const mergeResult = await gitlabApiClient.mergeMergeRequest( - projectId, - mrIid - ); - return { - content: [ - { - type: "text", - text: `成功创建并合并Merge Request: ${mergeResult.web_url}\n标题: ${mergeResult.title}\n状态: ${mergeResult.state}\n合并时间: ${mergeResult.merged_at}`, - }, - ], - }; - } catch (mergeError) { - // 如果合并失败,返回MR创建成功的信息,但提示合并需要审核 - return { - content: [ - { - type: "text", - text: `成功创建Merge Request,但自动合并失败,请手动审核: ${result.web_url}\n标题: ${result.title}\n状态: ${result.state}\n错误信息: ${mergeError.message}`, - }, - ], - }; - } } catch (error) { return { content: [ { type: "text", - text: `合并到test分支失败: ${error.message}`, + text: `获取接口详情失败: ${error.message}`, + }, + ], + isError: true, + }; + } + } +) +server.registerTool( + 'get_apis_by_folder', + { + description: "根据接口目录获取Apifox目录下所有接口定义数据", + inputSchema: z.object({ + folderName: z.string().describe("接口路径"), + }), + }, + + async ({ folderName }) => { + try { + + const apiData = apifoxService.getApiListByFolderName(folderName); + + return { + content: [ + { + type: "text", + text: JSON.stringify(apiData, null, 2), + }, + ], + }; + + } catch (error) { + return { + content: [ + { + type: "text", + text: `获取接口数据失败: ${error.message}`, }, ], isError: true, @@ -383,11 +93,87 @@ server.registerTool( ) +// 定义 prompts 列表 +server.setRequestHandler(ListPromptsRequestSchema, async () => { + return { + prompts: [ + { + name: "gen-api-by-path", + description: "根据输入的接口路径获取Apifox接口文档定义,然后根据定义生成 api 接口代码定义", + arguments: [ + ], + }, + { + name: "gen-api-by-folder", + description: "根据输入的接口文件夹获取Apifox相关文件夹下所有的接口文档定义,然后根据定义生成 api 接口代码定义", + arguments: [ + ], + }, + ], + }; +}); + +// 处理获取具体 prompt 的请求 +server.setRequestHandler(GetPromptRequestSchema, async (request) => { + const { name, arguments: args } = request.params; + + switch (name) { + case "gen-api-by-path": + return { + 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定义,要补充相关的注释 +4. 所有字段先定义为可空类型 +5. 请根据当前项目的项目接口和代码风格生成 api 接口代码 + +请开始执行这些步骤。`, + }, + }, + ], + }; + + case "gen-api-by-folder": + return { + 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定义,要补充相关的注释 +4. 所有字段先定义为可空类型 +5. 请根据当前项目的项目接口和代码风格生成 api 接口代码 + +请开始执行这些步骤。`, + }, + }, + ], + }; + + default: + throw new Error(`未知的 prompt: ${name}`); + } +}); + + + // 启动服务器,使用标准输入/输出(stdio)与客户端通信 async function main() { const transport = new StdioServerTransport(); await server.connect(transport); - console.error("GitLab MCP 服务器已通过 stdio 启动"); // 日志输出到 stderr + console.error("Apifox MCP 服务器已通过 stdio 启动"); // 日志输出到 stderr } // 运行主函数,并捕获可能的错误