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
@@ -1,234 +1,196 @@
|
||||
#!/usr/bin/env node
|
||||
// 引入必要的模块
|
||||
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
||||
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
||||
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
||||
import http from "http";
|
||||
import { z } from "zod";
|
||||
import { ApifoxService } from './apifox.js'
|
||||
|
||||
// 创建 MCP 服务器实例
|
||||
// 为其设置一个名字和版本
|
||||
const server = new McpServer(
|
||||
{
|
||||
name: "apifox-mcp-server",
|
||||
version: "1.0.0",
|
||||
},
|
||||
{
|
||||
capabilities: {
|
||||
prompts: {},
|
||||
tools: {},
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
const apifoxService = new ApifoxService({
|
||||
projectId: Number(process.env.APIFOX_PROJECT_ID),
|
||||
token: process.env.APIFOX_TOKEN
|
||||
});
|
||||
|
||||
|
||||
server.registerTool(
|
||||
'get_api_by_path',
|
||||
{
|
||||
description: "根据接口path获取Apifox接口定义数据",
|
||||
inputSchema: z.object({
|
||||
path: z.string().describe("接口路径"),
|
||||
projectId: z.number().optional().describe("Apifox项目ID"),
|
||||
}),
|
||||
},
|
||||
|
||||
async ({ path, projectId }) => {
|
||||
try {
|
||||
|
||||
const apiData = await apifoxService.getApiDetailByPath(path, projectId);
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: JSON.stringify(apiData, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
} catch (error) {
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: `获取接口详情失败: ${error.message}`,
|
||||
},
|
||||
],
|
||||
isError: true,
|
||||
};
|
||||
}
|
||||
}
|
||||
)
|
||||
server.registerTool(
|
||||
'get_apis_by_folder',
|
||||
{
|
||||
description: "根据接口目录获取Apifox目录下所有接口定义数据",
|
||||
inputSchema: z.object({
|
||||
folderName: z.string().describe("接口路径"),
|
||||
projectId: z.number().optional().describe("Apifox项目ID"),
|
||||
}),
|
||||
},
|
||||
|
||||
async ({ folderName, projectId }) => {
|
||||
try {
|
||||
|
||||
const apiData = await apifoxService.getApiListByFolderName(folderName, projectId);
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: JSON.stringify(apiData, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
} catch (error) {
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: `获取接口数据失败: ${error.message}`,
|
||||
},
|
||||
],
|
||||
isError: true,
|
||||
};
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
|
||||
// 定义 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)与客户端通信
|
||||
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';
|
||||
|
||||
const portArgIndex = args.indexOf('--port') !== -1 ? args.indexOf('--port') : args.indexOf('-p');
|
||||
const port = portArgIndex !== -1 ? parseInt(args[portArgIndex + 1]) : 8889;
|
||||
|
||||
|
||||
|
||||
if (transportType === 'http') {
|
||||
const transport = new StreamableHTTPServerTransport({});
|
||||
await server.connect(transport);
|
||||
|
||||
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') {
|
||||
res.writeHead(204);
|
||||
res.end();
|
||||
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);
|
||||
});
|
||||
|
||||
#!/usr/bin/env node
|
||||
// 引入必要的模块
|
||||
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
||||
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
||||
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
||||
import http from "http";
|
||||
import { z } from "zod";
|
||||
import { AsyncLocalStorage } from 'async_hooks';
|
||||
import { ApifoxService } from './apifox.js'
|
||||
|
||||
const requestContext = new AsyncLocalStorage();
|
||||
|
||||
// 创建 MCP 服务器实例
|
||||
// 为其设置一个名字和版本
|
||||
const server = new McpServer(
|
||||
{
|
||||
name: "apifox-mcp-server",
|
||||
version: "1.0.0",
|
||||
},
|
||||
{
|
||||
capabilities: {
|
||||
prompts: {},
|
||||
tools: {},
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
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);
|
||||
|
||||
if (!token) {
|
||||
throw new Error("Apifox Token not configured (env: APIFOX_TOKEN or header: Authorization)");
|
||||
}
|
||||
|
||||
return new ApifoxService({
|
||||
projectId,
|
||||
token
|
||||
});
|
||||
}
|
||||
|
||||
server.registerTool(
|
||||
'get_api_by_path',
|
||||
{
|
||||
description: "根据接口path获取Apifox接口定义数据",
|
||||
inputSchema: z.object({
|
||||
path: z.string().describe("接口路径"),
|
||||
projectId: z.number().optional().describe("Apifox项目ID"),
|
||||
}),
|
||||
},
|
||||
|
||||
async ({ path, projectId }) => {
|
||||
try {
|
||||
|
||||
const apifoxService = getApifoxService();
|
||||
const apiData = await apifoxService.getApiDetailByPath(path, projectId);
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: JSON.stringify(apiData, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
} catch (error) {
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: `获取接口详情失败: ${error.message}`,
|
||||
},
|
||||
],
|
||||
isError: true,
|
||||
};
|
||||
}
|
||||
}
|
||||
)
|
||||
server.registerTool(
|
||||
'get_apis_by_folder',
|
||||
{
|
||||
description: "根据接口目录获取Apifox目录下所有接口定义数据",
|
||||
inputSchema: z.object({
|
||||
folderName: z.string().describe("接口路径"),
|
||||
projectId: z.number().optional().describe("Apifox项目ID"),
|
||||
}),
|
||||
},
|
||||
|
||||
async ({ folderName, projectId }) => {
|
||||
try {
|
||||
|
||||
const apifoxService = getApifoxService();
|
||||
const apiData = await apifoxService.getApiListByFolderName(folderName, projectId);
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: JSON.stringify(apiData, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
} catch (error) {
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: `获取接口数据失败: ${error.message}`,
|
||||
},
|
||||
],
|
||||
isError: true,
|
||||
};
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
// 启动服务器,使用标准输入/输出(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';
|
||||
|
||||
const portArgIndex = args.indexOf('--port') !== -1 ? args.indexOf('--port') : args.indexOf('-p');
|
||||
const port = portArgIndex !== -1 ? parseInt(args[portArgIndex + 1]) : 8889;
|
||||
|
||||
|
||||
|
||||
if (transportType === 'http') {
|
||||
const transport = new StreamableHTTPServerTransport({});
|
||||
await server.connect(transport);
|
||||
|
||||
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') {
|
||||
res.writeHead(204);
|
||||
res.end();
|
||||
return;
|
||||
}
|
||||
|
||||
if (req.method === 'GET' && req.url === '/health') {
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ status: 'ok' }));
|
||||
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 {
|
||||
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`);
|
||||
console.error(`MCP 消息 endpoint: http://localhost:${port}/messages`);
|
||||
});
|
||||
} 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 };
|
||||
Reference in new issue
Block a user