# Directory Structure
```
├── .env.example
├── .eslintrc
├── .gitignore
├── package-lock.json
├── package.json
├── pnpm-lock.yaml
├── README.md
├── src
│ ├── cli.ts
│ ├── config.ts
│ ├── index.ts
│ └── server.ts
└── tsconfig.json
```
# Files
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
```
1 | .cursor/
2 | .history/
3 | node_modules/
4 | .env
5 | build/
6 |
```
--------------------------------------------------------------------------------
/.env.example:
--------------------------------------------------------------------------------
```
1 | ###
2 | # @Descripttion:
3 | # @version:
4 | # @Author: wangmin
5 | # @Date: 2025-03-21 11:25:26
6 | # @LastEditors: wangmin
7 | # @LastEditTime: 2025-03-21 11:26:18
8 | ###
9 | # your apifox api key
10 | APIFOX_API_KEY=your_apifox_api_key
11 | # your apifox project id
12 | PROJECT_ID=your_apifox_project_id
13 | # your apifox server
14 | PORT=your_apifox_server_port
```
--------------------------------------------------------------------------------
/.eslintrc:
--------------------------------------------------------------------------------
```
1 | {
2 | "parser": "@typescript-eslint/parser",
3 | "extends": [
4 | "eslint:recommended",
5 | "plugin:@typescript-eslint/recommended",
6 | "prettier"
7 | ],
8 | "plugins": ["@typescript-eslint"],
9 | "parserOptions": {
10 | "ecmaVersion": 2022,
11 | "sourceType": "module"
12 | },
13 | "rules": {
14 | "@typescript-eslint/explicit-function-return-type": "warn",
15 | "@typescript-eslint/no-unused-vars": [
16 | "error",
17 | { "argsIgnorePattern": "^_" }
18 | ],
19 | "@typescript-eslint/no-explicit-any": "warn"
20 | }
21 | }
22 |
```
--------------------------------------------------------------------------------
/src/cli.ts:
--------------------------------------------------------------------------------
```typescript
1 | #!/usr/bin/env node
2 |
3 | import { resolve } from "path";
4 | import { config } from "dotenv";
5 | import { startServer } from "./index.js";
6 |
7 | // Load .env from the current working directory
8 | config({ path: resolve(process.cwd(), ".env") });
9 |
10 | startServer().catch((error: unknown) => {
11 | if (error instanceof Error) {
12 | console.error("Failed to start server:", error.message);
13 | } else {
14 | console.error("Failed to start server with unknown error:", error);
15 | }
16 | process.exit(1);
17 | });
18 |
```
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
```json
1 | {
2 | "compilerOptions": {
3 | "target": "ES2022",
4 | "module": "Node16",
5 | "moduleResolution": "Node16",
6 | "lib": ["ES2022", "DOM"],
7 | "outDir": "./build",
8 | "rootDir": "./src",
9 | "strict": true,
10 | "esModuleInterop": true,
11 | "skipLibCheck": true,
12 | "forceConsistentCasingInFileNames": true,
13 | "resolveJsonModule": true,
14 | "declaration": true,
15 | "sourceMap": true,
16 | "baseUrl": ".",
17 | "paths": {
18 | "~/*": ["src/*"]
19 | },
20 | "preserveSymlinks": true,
21 | "allowJs": true,
22 | "removeComments": true
23 | },
24 | "include": ["src/**/*"],
25 | "exclude": ["node_modules", "build"]
26 | }
27 |
```
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
```typescript
1 | /*
2 | * @Descripttion:
3 | * @version:
4 | * @Author: wangmin
5 | * @Date: 2025-03-20 14:39:11
6 | * @LastEditors: wangmin
7 | * @LastEditTime: 2025-04-03 09:33:34
8 | */
9 | import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
10 | import { getServerConfig } from "./config.js";
11 | import { ApiFoxServer } from "./server.js";
12 | // // 创建 MCP 服务器
13 |
14 | // 启动服务器
15 | export async function startServer(): Promise<void> {
16 | const isLocalMode =
17 | process.env.NODE_ENV === "cli" || process.argv.includes("--local");
18 | const config = getServerConfig();
19 |
20 | const server = new ApiFoxServer(config.apifoxApiKey, config.projectId);
21 |
22 | if (isLocalMode) {
23 | const transport = new StdioServerTransport();
24 | await server.connect(transport);
25 | } else {
26 | console.error(
27 | `初始化HTTP模式下的ApiFox MCP Server服务器在端口 ${config.port}`
28 | );
29 | await server.startHttpServer(config.port);
30 | }
31 | }
32 |
33 | startServer().catch((error) => {
34 | console.error("启动接口信息服务器失败:", error);
35 | process.exit(1);
36 | });
37 |
```
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
```json
1 | {
2 | "name": "@wangmhaha/apifox-mcp-server",
3 | "version": "1.6.8",
4 | "main": "build/index.js",
5 | "type": "module",
6 | "bin": {
7 | "@wangmhaha/apifox-mcp-server": "build/cli.js"
8 | },
9 | "scripts": {
10 | "build": "tsc && tsc-alias",
11 | "start": "node build/index.js",
12 | "start:cli": "cross-env NODE_ENV=cli node build/index.js",
13 | "start:http": "node build/index.js"
14 | },
15 | "files": [
16 | "build",
17 | "README.md"
18 | ],
19 | "repository": {
20 | "type": "git",
21 | "url": "git+https://github.com/wangmhaha/apifox-mcp-server"
22 | },
23 | "keywords": [],
24 | "author": "wangmhaha",
25 | "license": "ISC",
26 | "description": "",
27 | "devDependencies": {
28 | "@types/express": "^5.0.1",
29 | "@types/express-serve-static-core": "^5.0.6",
30 | "@types/node": "^22.10.0",
31 | "tsc-alias": "^1.8.11",
32 | "tsx": "^4.19.3",
33 | "typescript": "^5.7.2"
34 | },
35 | "dependencies": {
36 | "@modelcontextprotocol/sdk": "^1.6.1",
37 | "@types/yargs": "^17.0.33",
38 | "@typescript-eslint/eslint-plugin": "^8.27.0",
39 | "@typescript-eslint/parser": "^8.27.0",
40 | "cross-env": "^7.0.3",
41 | "dotenv": "^16.4.7",
42 | "eslint": "^9.22.0",
43 | "express": "^4.21.2",
44 | "yargs": "^17.7.2",
45 | "zod": "^3.24.2"
46 | }
47 | }
```
--------------------------------------------------------------------------------
/src/config.ts:
--------------------------------------------------------------------------------
```typescript
1 | /*
2 | * @Descripttion:
3 | * @version:
4 | * @Author: wangmin
5 | * @Date: 2025-03-20 17:26:55
6 | * @LastEditors: wangmin
7 | * @LastEditTime: 2025-03-21 09:16:43
8 | */
9 | import { config } from "dotenv";
10 | import yargs from "yargs";
11 | import { hideBin } from "yargs/helpers";
12 |
13 | config();
14 |
15 | interface ServerConfig {
16 | port: number;
17 | apifoxApiKey: string;
18 | projectId: string;
19 | }
20 |
21 | interface CliArgs {
22 | "apifox-api-key"?: string;
23 | port?: number;
24 | "project-id"?: string;
25 | }
26 |
27 | export function getServerConfig(): ServerConfig {
28 | const argv = yargs(hideBin(process.argv))
29 | .options({
30 | "apifox-api-key": {
31 | type: "string",
32 | describe: "apifox api key",
33 | },
34 | "project-id": {
35 | type: "string",
36 | describe: "apifox project id",
37 | },
38 | port: {
39 | type: "number",
40 | describe: "Prot to run the server on",
41 | },
42 | })
43 | .help()
44 | .parseSync() as CliArgs;
45 |
46 | const config: ServerConfig = {
47 | apifoxApiKey: "",
48 | projectId: "",
49 | port: 3000,
50 | };
51 |
52 | if (argv["apifox-api-key"]) {
53 | config.apifoxApiKey = argv["apifox-api-key"];
54 | } else if (process.env.APIFOX_API_KEY) {
55 | config.apifoxApiKey = process.env.APIFOX_API_KEY;
56 | }
57 |
58 | if (argv["project-id"]) {
59 | config.projectId = argv["project-id"];
60 | } else if (process.env.PROJECT_ID) {
61 | config.projectId = process.env.PROJECT_ID;
62 | }
63 |
64 | if (argv.port) {
65 | config.port = argv.port;
66 | } else if (process.env.PORT) {
67 | config.port = parseInt(process.env.PORT, 10);
68 | }
69 |
70 | if (!config.apifoxApiKey) {
71 | console.error("请提供 apifox api key");
72 | process.exit(1);
73 | }
74 |
75 | if (!config.projectId) {
76 | console.error("请提供 apifox project id");
77 | process.exit(1);
78 | }
79 |
80 | return config;
81 | }
82 |
```
--------------------------------------------------------------------------------
/src/server.ts:
--------------------------------------------------------------------------------
```typescript
1 | /*
2 | * @Descripttion:
3 | * @version:
4 | * @Author: wangmin
5 | * @Date: 2025-03-20 17:49:38
6 | * @LastEditors: wangmin
7 | * @LastEditTime: 2025-04-03 14:30:44
8 | */
9 | import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
10 | import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
11 | import { Transport } from "@modelcontextprotocol/sdk/shared/transport.js";
12 | import { IncomingMessage, ServerResponse } from "http";
13 | import express from "express";
14 | import { Response, Request } from "express-serve-static-core";
15 | import { z } from "zod";
16 |
17 | export class ApiFoxServer {
18 | private readonly server: McpServer;
19 | private sseTransport: SSEServerTransport | null = null;
20 |
21 | constructor(apifoxApiKey: string, projectId: string) {
22 | this.server = new McpServer({
23 | name: "ApiFox MCP Server",
24 | version: "1.0.0",
25 | capabilities: {
26 | notifications: true,
27 | },
28 | });
29 |
30 | this.registerTools(apifoxApiKey, projectId);
31 | }
32 |
33 | // 注册工具
34 | private registerTools(key: string, projectId: string): void {
35 | this.server.tool(
36 | "get-interface",
37 | "获取apiFox接口信息",
38 | {
39 | moduleId: z.string().describe("要查询模块id"),
40 | moduleName: z.string().describe("要查询模块名称"),
41 | },
42 | async (args: { moduleId: string; moduleName?: string }) => {
43 | try {
44 | const response = await fetch(
45 | `https://api.apifox.com/v1/projects/${projectId}/export-openapi`,
46 | {
47 | method: "POST",
48 | headers: {
49 | "Content-Type": "application/json",
50 | Authorization: `Bearer ${key}`,
51 | "X-Apifox-Api-Version": "2024-03-28",
52 | },
53 | body: JSON.stringify({
54 | scope: {
55 | type: "SELECTED_FOLDERS",
56 | selectedFolderIds: [args.moduleId],
57 | excludedByTags: ["pet"],
58 | },
59 | options: {
60 | includeApifoxExtensionProperties: false,
61 | addFoldersToTags: true,
62 | },
63 | oasVersion: "3.1",
64 | exportFormat: "JSON",
65 | }),
66 | }
67 | );
68 |
69 | // 检查响应状态
70 | if (!response.ok) {
71 | throw new Error(`HTTP error! status: ${response.status}`);
72 | }
73 |
74 | // 解析响应数据
75 | const data = await response.json();
76 |
77 | if (!data) {
78 | return {
79 | content: [
80 | {
81 | type: "text",
82 | text: `无法找到${args.moduleName}的接口信息`,
83 | },
84 | ],
85 | };
86 | }
87 |
88 | return {
89 | content: [
90 | {
91 | type: "text",
92 | text: `基于openapi3.1.0的规范,${
93 | args.moduleName
94 | }的接口信息如下: ${JSON.stringify(data)}`,
95 | },
96 | ],
97 | };
98 | } catch (error) {
99 | console.error("获取接口信息失败:", error);
100 | return {
101 | content: [
102 | {
103 | type: "text",
104 | text: `获取接口信息失败: ${error}`,
105 | },
106 | ],
107 | };
108 | }
109 | }
110 | );
111 | }
112 |
113 | async connect(transport: Transport): Promise<void> {
114 | await this.server.connect(transport);
115 | console.error("服务器已连接并准备处理请求");
116 | }
117 |
118 | async startHttpServer(port: number): Promise<void> {
119 | const app = express();
120 |
121 | app.get("/sse", async (req: Request, res: Response) => {
122 | console.error("SSE连接建立");
123 | this.sseTransport = new SSEServerTransport(
124 | "/messages",
125 | res as unknown as ServerResponse<IncomingMessage>
126 | );
127 | await this.connect(this.sseTransport);
128 | });
129 |
130 | app.post("/messages", async (req: Request, res: Response) => {
131 | if (!this.sseTransport) {
132 | res.status(400).send();
133 | return;
134 | }
135 | await this.sseTransport.handlePostMessage(
136 | req as unknown as IncomingMessage,
137 | res as unknown as ServerResponse<IncomingMessage>
138 | );
139 | });
140 |
141 | app.listen(port, () => {
142 | console.error(`HTTP服务器监听端口 ${port}`);
143 | console.error(`SSE 端点可用于 http://localhost:${port}/sse`);
144 | console.error(
145 | `消息端点可在以下位置访问: http://localhost:${port}/messages`
146 | );
147 | });
148 | }
149 | }
150 |
```