#!/usr/bin/env node
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import {
CallToolRequestSchema,
ListToolsRequestSchema,
Tool,
} from "@modelcontextprotocol/sdk/types.js";
// 项目配置接口
interface ProjectConfig {
id: number;
token: string;
}
// 从环境变量获取配置
const YAPI_BASE_URL = process.env.YAPI_BASE_URL || "";
// 解析项目配置
// 格式: YAPI_PROJECTS='projectId1:token1,projectId2:token2'
const projects: ProjectConfig[] = [];
const projectsStr = process.env.YAPI_PROJECTS || "";
if (projectsStr) {
const projectPairs = projectsStr.split(",");
for (const pair of projectPairs) {
const trimmedPair = pair.trim();
if (!trimmedPair) continue;
const colonIndex = trimmedPair.indexOf(":");
if (colonIndex === -1) {
console.error(`项目配置格式错误: ${trimmedPair},应为 projectId:token`);
continue;
}
const idStr = trimmedPair.substring(0, colonIndex).trim();
const token = trimmedPair.substring(colonIndex + 1).trim();
const id = Number(idStr);
if (isNaN(id)) {
console.error(`项目 ID 无效: ${idStr}`);
continue;
}
if (!token) {
console.error(`项目 ${id} 的 token 为空`);
continue;
}
projects.push({ id, token });
}
}
// 根据项目 ID 获取项目配置
function getProjectConfig(projectId?: unknown): ProjectConfig {
if (projects.length === 0) {
throw new Error(
"未配置任何项目,请设置 YAPI_PROJECTS 环境变量,格式: projectId1:token1,projectId2:token2"
);
}
// 优先使用传入的参数
let id = projectId;
// 如果没有传入参数,使用第一个项目
if (id === undefined || id === null || id === "") {
return projects[0];
}
// 按 ID 查找
const idNum = Number(id);
if (isNaN(idNum)) {
throw new Error(`项目 ID 无效: ${id}`);
}
const project = projects.find((p) => p.id === idNum);
if (project) {
return project;
}
throw new Error(
`未找到项目: ${id},可用项目 ID: ${projects.map((p) => p.id).join(", ")}`
);
}
// YAPI API 请求封装
async function yapiRequest(
endpoint: string,
method: "GET" | "POST" = "GET",
params?: Record<string, unknown>,
token?: string
): Promise<unknown> {
if (!YAPI_BASE_URL) {
throw new Error("YAPI_BASE_URL 环境变量未配置");
}
if (!token) {
throw new Error("token 未提供");
}
const url = new URL(endpoint, YAPI_BASE_URL);
const options: RequestInit = {
method,
headers: {
"Content-Type": "application/json",
},
};
if (method === "GET") {
// GET 请求时将参数添加到 URL
if (params) {
Object.entries(params).forEach(([key, value]) => {
if (value !== undefined && value !== null) {
url.searchParams.append(key, String(value));
}
});
}
url.searchParams.append("token", token);
} else {
// POST 请求时将参数放在 body 中
url.searchParams.append("token", token);
if (params) {
options.body = JSON.stringify(params);
}
}
const response = await fetch(url.toString(), options);
if (!response.ok) {
throw new Error(`YAPI 请求失败: ${response.status} ${response.statusText}`);
}
return response.json();
}
// 解析 YAPI URL,提取项目 ID、接口 ID、分类 ID
function parseYapiUrl(url: string): {
projectId?: number;
interfaceId?: number;
catId?: number;
} {
const result: { projectId?: number; interfaceId?: number; catId?: number } =
{};
// 匹配项目 ID: /project/{projectId}
const projectMatch = url.match(/\/project\/(\d+)/);
if (projectMatch) {
result.projectId = Number(projectMatch[1]);
}
// 匹配分类 ID: /interface/api/cat_{catId}
const catMatch = url.match(/\/interface\/api\/cat_(\d+)/);
if (catMatch) {
result.catId = Number(catMatch[1]);
} else {
// 匹配接口 ID: /interface/api/{interfaceId}
const interfaceMatch = url.match(/\/interface\/api\/(\d+)/);
if (interfaceMatch) {
result.interfaceId = Number(interfaceMatch[1]);
}
}
return result;
}
// 判断 YAPI 返回结果是否为 token 错误
function isTokenError(result: unknown): boolean {
if (result && typeof result === "object" && "errcode" in result) {
const r = result as { errcode: number; errmsg?: string };
return r.errcode === 400 && r.errmsg === "token有误";
}
return false;
}
// 带自动重试的请求:未指定项目时,依次尝试所有项目的 token
async function yapiRequestWithFallback(
endpoint: string,
method: "GET" | "POST",
buildParams: (project: ProjectConfig) => Record<string, unknown> | undefined,
specifiedProject?: unknown
): Promise<unknown> {
// 如果明确指定了项目,直接使用该项目
if (specifiedProject !== undefined && specifiedProject !== null && specifiedProject !== "") {
const project = getProjectConfig(specifiedProject);
return yapiRequest(endpoint, method, buildParams(project), project.token);
}
if (projects.length === 0) {
throw new Error(
"未配置任何项目,请设置 YAPI_PROJECTS 环境变量,格式: projectId1:token1,projectId2:token2"
);
}
// 依次尝试所有项目的 token
for (const project of projects) {
try {
const result = await yapiRequest(
endpoint,
method,
buildParams(project),
project.token
);
if (!isTokenError(result)) {
return result;
}
} catch {
// 请求失败,尝试下一个项目
continue;
}
}
throw new Error(
`所有已配置项目的 token 均无法访问此接口,已尝试项目: ${projects.map((p) => p.id).join(", ")}`
);
}
// 通用的 project 参数描述
const projectParamDesc =
"项目 ID(可选,不传则使用默认项目)";
// 通用的 url 参数定义
const urlParamDef = {
type: "string" as const,
description:
"YAPI 接口页面 URL(可选),如 https://yapi.xxx.com/project/1009/interface/api/108375,会自动解析出项目 ID、接口 ID 等参数",
};
// 定义所有工具
const tools: Tool[] = [
{
name: "yapi_list_projects",
description: "列出所有已配置的 YAPI 项目",
inputSchema: {
type: "object",
properties: {},
required: [],
},
},
{
name: "yapi_run_auto_test",
description: "运行 YAPI 自动化测试",
inputSchema: {
type: "object",
properties: {
url: urlParamDef,
project: {
type: ["number", "string"],
description: projectParamDesc,
},
env_name: {
type: "string",
description: "环境名称(可选)",
},
},
required: [],
},
},
{
name: "yapi_import_data",
description: "导入接口数据到 YAPI",
inputSchema: {
type: "object",
properties: {
url: urlParamDef,
project: {
type: ["number", "string"],
description: projectParamDesc,
},
type: {
type: "string",
description: "导入类型,如:swagger、postman、har、json",
enum: ["swagger", "postman", "har", "json"],
},
json: {
type: "string",
description: "导入的 JSON 数据",
},
merge: {
type: "string",
description: "合并模式:normal(普通)、good(智能合并)、merge(完全覆盖)",
enum: ["normal", "good", "merge"],
},
},
required: ["type", "json"],
},
},
{
name: "yapi_interface_add",
description: "新增接口",
inputSchema: {
type: "object",
properties: {
url: urlParamDef,
project: {
type: ["number", "string"],
description: projectParamDesc,
},
catid: {
type: "number",
description: "接口分类 ID",
},
title: {
type: "string",
description: "接口名称",
},
path: {
type: "string",
description: "接口路径,如:/api/user",
},
method: {
type: "string",
description: "请求方法,如:GET、POST、PUT、DELETE",
},
desc: {
type: "string",
description: "接口描述(可选)",
},
status: {
type: "string",
description: "接口状态:done(已完成)、undone(未完成)",
enum: ["done", "undone"],
},
req_params: {
type: "array",
description: "路径参数",
items: {
type: "object",
properties: {
name: { type: "string" },
desc: { type: "string" },
},
},
},
req_query: {
type: "array",
description: "查询参数",
items: {
type: "object",
properties: {
name: { type: "string" },
desc: { type: "string" },
required: { type: "string" },
},
},
},
req_headers: {
type: "array",
description: "请求头",
items: {
type: "object",
properties: {
name: { type: "string" },
value: { type: "string" },
},
},
},
req_body_type: {
type: "string",
description: "请求体类型:form、json、file、raw",
enum: ["form", "json", "file", "raw"],
},
req_body_form: {
type: "array",
description: "表单类型的请求体",
items: {
type: "object",
properties: {
name: { type: "string" },
type: { type: "string" },
desc: { type: "string" },
required: { type: "string" },
},
},
},
req_body_other: {
type: "string",
description: "其他类型的请求体(JSON 字符串)",
},
res_body_type: {
type: "string",
description: "返回数据类型:json、raw",
enum: ["json", "raw"],
},
res_body: {
type: "string",
description: "返回数据(JSON 字符串)",
},
},
required: ["catid", "title", "path", "method"],
},
},
{
name: "yapi_interface_save",
description: "保存接口(新增或更新)",
inputSchema: {
type: "object",
properties: {
url: urlParamDef,
project: {
type: ["number", "string"],
description: projectParamDesc,
},
catid: {
type: "number",
description: "接口分类 ID",
},
id: {
type: "number",
description: "接口 ID(更新时必填)",
},
title: {
type: "string",
description: "接口名称",
},
path: {
type: "string",
description: "接口路径",
},
method: {
type: "string",
description: "请求方法",
},
desc: {
type: "string",
description: "接口描述",
},
status: {
type: "string",
description: "接口状态",
enum: ["done", "undone"],
},
req_params: {
type: "array",
description: "路径参数",
},
req_query: {
type: "array",
description: "查询参数",
},
req_headers: {
type: "array",
description: "请求头",
},
req_body_type: {
type: "string",
description: "请求体类型",
},
req_body_form: {
type: "array",
description: "表单请求体",
},
req_body_other: {
type: "string",
description: "其他请求体",
},
res_body_type: {
type: "string",
description: "返回数据类型",
},
res_body: {
type: "string",
description: "返回数据",
},
},
required: ["catid", "title", "path", "method"],
},
},
{
name: "yapi_interface_up",
description: "更新接口",
inputSchema: {
type: "object",
properties: {
url: urlParamDef,
project: {
type: ["number", "string"],
description: projectParamDesc,
},
id: {
type: "number",
description: "接口 ID",
},
catid: {
type: "number",
description: "接口分类 ID",
},
title: {
type: "string",
description: "接口名称",
},
path: {
type: "string",
description: "接口路径",
},
method: {
type: "string",
description: "请求方法",
},
desc: {
type: "string",
description: "接口描述",
},
status: {
type: "string",
description: "接口状态",
enum: ["done", "undone"],
},
req_params: {
type: "array",
description: "路径参数",
},
req_query: {
type: "array",
description: "查询参数",
},
req_headers: {
type: "array",
description: "请求头",
},
req_body_type: {
type: "string",
description: "请求体类型",
},
req_body_form: {
type: "array",
description: "表单请求体",
},
req_body_other: {
type: "string",
description: "其他请求体",
},
res_body_type: {
type: "string",
description: "返回数据类型",
},
res_body: {
type: "string",
description: "返回数据",
},
},
required: ["id"],
},
},
{
name: "yapi_interface_get",
description:
"获取接口详情,可直接传入 YAPI 接口页面 URL 自动解析参数",
inputSchema: {
type: "object",
properties: {
url: urlParamDef,
project: {
type: ["number", "string"],
description: projectParamDesc,
},
id: {
type: "number",
description: "接口 ID(如果传了 url 则可省略)",
},
},
required: [],
},
},
{
name: "yapi_interface_list",
description: "获取接口列表",
inputSchema: {
type: "object",
properties: {
url: urlParamDef,
project: {
type: ["number", "string"],
description: projectParamDesc,
},
catid: {
type: "number",
description: "分类 ID(可选,不传则获取所有分类下的接口)",
},
page: {
type: "number",
description: "页码,默认 1",
},
limit: {
type: "number",
description: "每页条数,默认 10",
},
},
required: [],
},
},
{
name: "yapi_interface_list_menu",
description: "获取接口菜单(包含分类和接口列表)",
inputSchema: {
type: "object",
properties: {
url: urlParamDef,
project: {
type: ["number", "string"],
description: projectParamDesc,
},
},
required: [],
},
},
{
name: "yapi_interface_add_cat",
description: "新增接口分类",
inputSchema: {
type: "object",
properties: {
url: urlParamDef,
project: {
type: ["number", "string"],
description: projectParamDesc,
},
name: {
type: "string",
description: "分类名称",
},
desc: {
type: "string",
description: "分类描述(可选)",
},
},
required: ["name"],
},
},
{
name: "yapi_interface_get_cat_menu",
description: "获取所有接口分类",
inputSchema: {
type: "object",
properties: {
url: urlParamDef,
project: {
type: ["number", "string"],
description: projectParamDesc,
},
},
required: [],
},
},
];
// 工具处理函数
async function handleToolCall(
name: string,
args: Record<string, unknown>
): Promise<string> {
try {
// 如果提供了 URL,自动解析出项目 ID、接口 ID、分类 ID
if (args.url && typeof args.url === "string") {
const parsed = parseYapiUrl(args.url);
if (parsed.projectId !== undefined && !args.project) {
args.project = parsed.projectId;
}
if (parsed.interfaceId !== undefined && !args.id) {
args.id = parsed.interfaceId;
}
if (parsed.catId !== undefined && !args.catid) {
args.catid = parsed.catId;
}
}
let result: unknown;
// 获取项目配置(除了 yapi_list_projects 外都需要)
const getProject = () => getProjectConfig(args.project);
switch (name) {
case "yapi_list_projects": {
const projectList = projects.map((p) => ({
id: p.id,
isDefault: projects[0]?.id === p.id,
}));
result = {
projects: projectList,
defaultProject: projects[0]?.id ?? "无",
};
break;
}
case "yapi_run_auto_test": {
result = await yapiRequestWithFallback(
"/api/open/run_auto_test",
"GET",
(project) => ({
id: project.id,
env_name: args.env_name,
}),
args.project
);
break;
}
case "yapi_import_data": {
result = await yapiRequestWithFallback(
"/api/open/import_data",
"POST",
() => ({
type: args.type,
json: args.json,
merge: args.merge || "normal",
}),
args.project
);
break;
}
case "yapi_interface_add": {
const { project: _, ...restArgs } = args;
result = await yapiRequestWithFallback(
"/api/interface/add",
"POST",
() => restArgs,
args.project
);
break;
}
case "yapi_interface_save": {
const { project: _, ...restArgs } = args;
result = await yapiRequestWithFallback(
"/api/interface/save",
"POST",
() => restArgs,
args.project
);
break;
}
case "yapi_interface_up": {
const { project: _, ...restArgs } = args;
result = await yapiRequestWithFallback(
"/api/interface/up",
"POST",
() => restArgs,
args.project
);
break;
}
case "yapi_interface_get": {
result = await yapiRequestWithFallback(
"/api/interface/get",
"GET",
() => ({
id: args.id,
}),
args.project
);
break;
}
case "yapi_interface_list": {
result = await yapiRequestWithFallback(
"/api/interface/list",
"GET",
(project) => ({
project_id: project.id,
catid: args.catid,
page: args.page || 1,
limit: args.limit || 10,
}),
args.project
);
break;
}
case "yapi_interface_list_menu": {
result = await yapiRequestWithFallback(
"/api/interface/list_menu",
"GET",
(project) => ({
project_id: project.id,
}),
args.project
);
break;
}
case "yapi_interface_add_cat": {
result = await yapiRequestWithFallback(
"/api/interface/add_cat",
"POST",
(project) => ({
project_id: project.id,
name: args.name,
desc: args.desc,
}),
args.project
);
break;
}
case "yapi_interface_get_cat_menu": {
result = await yapiRequestWithFallback(
"/api/interface/getCatMenu",
"GET",
(project) => ({
project_id: project.id,
}),
args.project
);
break;
}
default:
throw new Error(`未知工具: ${name}`);
}
return JSON.stringify(result, null, 2);
} catch (error) {
const errorMessage =
error instanceof Error ? error.message : String(error);
return JSON.stringify({ error: errorMessage });
}
}
// 创建 MCP 服务器
const server = new Server(
{
name: "yapi-mcp",
version: "1.0.0",
},
{
capabilities: {
tools: {},
},
}
);
// 注册工具列表处理器
server.setRequestHandler(ListToolsRequestSchema, async () => {
return { tools };
});
// 注册工具调用处理器
server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { name, arguments: args } = request.params;
const result = await handleToolCall(name, args as Record<string, unknown>);
return {
content: [
{
type: "text",
text: result,
},
],
};
});
// 启动服务器
async function main() {
const transport = new StdioServerTransport();
await server.connect(transport);
console.error("YAPI MCP Server 已启动");
}
main().catch((error) => {
console.error("服务器启动失败:", error);
process.exit(1);
});