摘要
当前项目组在使用 YAPI 生成 Mock 数据的过程中,发现其生成的 Mock 数据粒度较粗,无法满足精细化的开发测试需求。为解决这一问题,计划开发 MCP(Mock Control Platform)平台来替代现有 YAPI 的 Mock 数据功能,旨在提供更贴合业务场景、更精准且可灵活配置的 Mock 数据服务,提升前端(涵盖 Vue、React、React Native、Uniapp 多技术栈)开发过程中接口联调、功能测试的效率和准确性。
总结
- 核心问题:YAPI 生成的 Mock 数据粗糙,无法满足精细化开发测试需求;
- 解决方案:开发 MCP 平台替代 YAPI 的 Mock 数据功能;
- 核心目标:提供精准、灵活、贴合业务的 Mock 数据,适配多技术栈开发场景,提升开发测试效率。
核心代码 index.cjs
/* eslint-env node */
/* global process, console, require, __dirname */
/* eslint-disable @typescript-eslint/no-require-imports */
const fs = require('node:fs');
const path = require('node:path');
// 确保从项目根目录解析模块
const projectRoot = path.resolve(__dirname, '../..');
const nodeModulesPath = path.join(projectRoot, 'node_modules');
// 设置 NODE_PATH 确保能找到模块
if (!process.env.NODE_PATH) {
process.env.NODE_PATH = nodeModulesPath;
} else {
process.env.NODE_PATH = nodeModulesPath + path.delimiter + process.env.NODE_PATH;
}
const Module = require('module');
Module._initPaths();
// 使用绝对路径 require 模块
const ts = require(path.join(nodeModulesPath, 'typescript'));
const { Server } = require(path.join(nodeModulesPath, '@modelcontextprotocol/sdk/dist/cjs/server/index.js'));
const { StdioServerTransport } = require(path.join(nodeModulesPath, '@modelcontextprotocol/sdk/dist/cjs/server/stdio.js'));
const { ListToolsRequestSchema, CallToolRequestSchema } = require(path.join(
nodeModulesPath,
'@modelcontextprotocol/sdk/dist/cjs/types.js'
));
const YAPI_DIR = process.env.YAPI_DIR ? path.resolve(process.env.YAPI_DIR) : path.join(projectRoot, 'apps/capacitor/src/yapi');
const state = {
program: null,
checker: null,
functionToType: new Map(),
typeNodes: new Map()
};
// For any image-like field, always return a real reachable resource.
const DEFAULT_MOCK_IMAGE_URL =
'https://xxxxxx.com/digital-food-test/product/897185091627843616/1745458813605.jpg';
async function startServer() {
const server = new Server({ name: 'yapi-mock-generator', version: '0.1.0' }, { capabilities: { tools: {} } });
server.setRequestHandler(ListToolsRequestSchema, () => ({
tools: [
{
name: 'generateMock',
description: '根据接口函数名生成 mock 数据并保存为 JSON 文件。文件命名格式:mock-接口名.json。如果提供了 contextPath(代码文件路径),则在与代码上下文同级的目录生成;否则保存在项目根目录的 mocks/ 文件夹中。例如:postMarketCouponMyCoupons',
inputSchema: {
type: 'object',
properties: {
functionName: {
type: 'string',
description: '接口函数名,例如:postMarketCouponMyCoupons。生成的文件名为:mock-postMarketCouponMyCoupons.json'
},
pageSize: {
type: 'number',
description: '列表大小(可选)'
},
total: {
type: 'number',
description: '总数(可选)'
},
contextPath: {
type: 'string',
description: '代码文件的路径(可选)。如果提供,mock 文件将生成在该文件同级的目录中。例如:apps/capacitor/src/pages/red-packet-merchant-activity/index.tsx'
}
},
required: ['functionName']
}
}
]
}));
server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { name, arguments: args } = request.params;
if (name === 'generateMock') {
const functionName = String(args?.functionName || '');
const mockConfig = {
pageSize: args?.pageSize,
total: args?.total,
listSize: args?.pageSize
};
const mock = generateMockFromFunction(functionName, mockConfig);
// 生成文件路径:mock-接口名.json
// 规则:如果提供了 contextPath,则在与代码上下文同级的目录生成;否则保存在项目根目录的 mocks/ 目录
const fileName = `mock-${functionName}.json`;
let targetDir;
let relativePath;
if (args?.contextPath) {
// 如果提供了 contextPath,在与代码文件同级的目录生成
const contextPath = String(args.contextPath);
const resolvedContextPath = path.isAbsolute(contextPath)
? contextPath
: path.join(projectRoot, contextPath);
const contextDir = path.dirname(resolvedContextPath);
targetDir = contextDir;
const filePath = path.join(contextDir, fileName);
relativePath = path.relative(projectRoot, filePath).replace(/\\/g, '/');
} else {
// 默认:在项目根目录的 mocks/ 目录生成
targetDir = path.join(projectRoot, 'mocks');
relativePath = `mocks/${fileName}`;
}
// 确保目标目录存在
if (!fs.existsSync(targetDir)) {
fs.mkdirSync(targetDir, { recursive: true });
}
const filePath = path.join(targetDir, fileName);
const jsonText = JSON.stringify(mock, null, 2);
// 写入文件
fs.writeFileSync(filePath, jsonText, 'utf8');
// 返回文件路径和内容
return {
content: [
{
type: 'text',
text: `✅ Mock 数据已生成!\n\n文件路径: ${relativePath}\n文件大小: ${jsonText.length} 字节\n\n文件内容预览:\n\`\`\`json\n${jsonText.substring(0, 500)}${jsonText.length > 500 ? '...' : ''}\n\`\`\``
}
]
};
}
throw new Error(`Unknown tool: ${name}`);
});
await server.connect(new StdioServerTransport());
}
function ensureProgram() {
if (state.program && state.checker) {
return;
}
const files = getYapiFiles();
const normalizePathKey = (value) => path.resolve(value).replace(/\\/g, '/').toLowerCase();
const fileSet = new Set(files.map(normalizePathKey));
const program = ts.createProgram(files, {
target: ts.ScriptTarget.ES2020,
module: ts.ModuleKind.ESNext,
strict: true,
skipLibCheck: true
});
const checker = program.getTypeChecker();
const functionToType = new Map();
const typeNodes = new Map();
for (const sourceFile of program.getSourceFiles()) {
if (!fileSet.has(normalizePathKey(sourceFile.fileName))) {
continue;
}
indexSourceFile(sourceFile, functionToType, typeNodes);
}
state.program = program;
state.checker = checker;
state.functionToType = functionToType;
state.typeNodes = typeNodes;
}
function getYapiFiles() {
if (!fs.existsSync(YAPI_DIR)) {
return [];
}
return fs
.readdirSync(YAPI_DIR)
.filter((file) => file.endsWith('.ts'))
.filter((file) => !file.includes('request.ts') && !file.includes('makeRequestHook.ts'))
.map((file) => path.join(YAPI_DIR, file));
}
function indexSourceFile(sourceFile, functionToType, typeNodes) {
const unwrap = (expr) => {
let current = expr;
while (current) {
if (ts.isParenthesizedExpression(current)) {
current = current.expression;
continue;
}
if (ts.isAsExpression(current)) {
current = current.expression;
continue;
}
if (ts.isNonNullExpression(current)) {
current = current.expression;
continue;
}
if (ts.isTypeAssertionExpression(current)) {
current = current.expression;
continue;
}
break;
}
return current;
};
const visit = (node) => {
if (ts.isInterfaceDeclaration(node) || ts.isTypeAliasDeclaration(node)) {
typeNodes.set(node.name.text, node);
}
if (ts.isVariableStatement(node) && node.modifiers?.some((mod) => mod.kind === ts.SyntaxKind.ExportKeyword)) {
for (const decl of node.declarationList.declarations) {
if (!ts.isIdentifier(decl.name)) {
continue;
}
const functionName = decl.name.text;
const initializer = decl.initializer ? unwrap(decl.initializer) : null;
if (!initializer || (!ts.isArrowFunction(initializer) && !ts.isFunctionExpression(initializer))) {
continue;
}
const responseType = findResponseTypeName(initializer);
if (responseType) {
functionToType.set(functionName, responseType);
}
}
}
ts.forEachChild(node, visit);
};
visit(sourceFile);
}
function findResponseTypeName(func) {
let result = null;
const visit = (node) => {
if (ts.isCallExpression(node) && ts.isIdentifier(node.expression) && node.expression.text === 'request') {
const typeArg = node.typeArguments?.[0];
if (typeArg && ts.isTypeReferenceNode(typeArg) && ts.isIdentifier(typeArg.typeName)) {
result = typeArg.typeName.text;
}
}
ts.forEachChild(node, visit);
};
if (func.body) {
ts.forEachChild(func.body, visit);
}
return result;
}
function generateMockFromFunction(functionName, mockConfig) {
ensureProgram();
const typeName = state.functionToType.get(functionName);
if (!typeName) {
return {
success: true,
error: { code: '0', message: '' },
data: {}
};
}
return generateMockFromType(typeName, mockConfig);
}
function generateMockFromType(typeName, mockConfig) {
ensureProgram();
const node = state.typeNodes.get(typeName);
if (!node || !state.checker) {
return {
success: true,
error: { code: '0', message: '' },
data: {}
};
}
const type = state.checker.getTypeAtLocation(node);
const mock = buildMockFromType(type, {
checker: state.checker,
depth: 0,
maxDepth: 12,
path: [],
mockConfig,
seen: new Set()
});
return mock;
}
function getArrayElementType(checker, arrayType) {
// Prefer official helper when it works
const byHelper = checker.getElementTypeOfArrayType(arrayType);
if (byHelper) {
return byHelper;
}
// Fallback: type arguments for Array<T>
const args = checker.getTypeArguments(arrayType);
if (args && args.length > 0) {
return args[0];
}
return null;
}
function getIndexFromPath(pathArr) {
if (!Array.isArray(pathArr)) {
return 0;
}
for (let i = pathArr.length - 1; i >= 0; i -= 1) {
const v = pathArr[i];
if (typeof v === 'string' && /^\d+$/.test(v)) {
return Number(v);
}
}
return 0;
}
function getParentKey(pathArr) {
if (!Array.isArray(pathArr)) {
return '';
}
// parent key = nearest non-numeric key before the last key
for (let i = pathArr.length - 2; i >= 0; i -= 1) {
const v = pathArr[i];
if (typeof v === 'string' && !/^\d+$/.test(v)) {
return v;
}
}
return '';
}
function buildMockFromType(type, context) {
const { checker, depth, maxDepth, path, mockConfig, seen } = context;
if (depth > maxDepth) {
return null;
}
const typeId = getTypeId(type);
if (typeId !== null) {
if (seen.has(typeId)) {
return null;
}
seen.add(typeId);
}
if (type.isUnion()) {
const next = pickUnionType(type.types);
return buildMockFromType(next, { ...context, depth: depth + 1 });
}
if (type.isIntersection()) {
const parts = type.types.map((part) => buildMockFromType(part, { ...context, depth: depth + 1 }));
return parts.reduce((acc, value) => mergeDeep(acc, value), {});
}
if (type.isStringLiteral()) {
return type.value;
}
if (type.isNumberLiteral()) {
return type.value;
}
if (type.flags & ts.TypeFlags.BooleanLiteral) {
return type.intrinsicName === 'true';
}
if (type.flags & ts.TypeFlags.String) {
return createStringSample(path.at(-1), path);
}
if (type.flags & ts.TypeFlags.Number) {
return createNumberSample(path.at(-1), path);
}
if (type.flags & ts.TypeFlags.Boolean) {
return createBooleanSample(path.at(-1), path);
}
if (checker.isArrayType(type)) {
const elementType = getArrayElementType(checker, type);
if (!elementType) {
return [];
}
const size = getArraySize(path.at(-1), mockConfig);
// IMPORTANT: do not share `seen` across siblings; arrays should allow repeated element shapes
return Array.from({ length: size }, (_, index) =>
buildMockFromType(elementType, {
...context,
depth: depth + 1,
path: [...path, String(index)],
seen: new Set(seen)
})
);
}
if (checker.isTupleType(type)) {
const tupleElements = checker.getTypeArguments(type);
return tupleElements.map((item, index) =>
buildMockFromType(item, { ...context, depth: depth + 1, path: [...path, String(index)], seen: new Set(seen) })
);
}
if (type.flags & ts.TypeFlags.Object) {
// Handle map-like objects and empty `{}` objects.
const props = type.getProperties();
const stringIndexType = typeof type.getStringIndexType === 'function' ? type.getStringIndexType() : null;
if (props.length === 0) {
const parentKey = getParentKey(path);
// i18n-ish objects: assetName/categoryName/promotionActivityTagName etc.
if (/name|title|label|desc|subtitle|tag/i.test(parentKey)) {
return {
'id-ID': createStringSample('id-ID', [...path, 'id-ID']),
'en-US': createStringSample('en-US', [...path, 'en-US']),
'zh-CN': createStringSample('zh-CN', [...path, 'zh-CN'])
};
}
// Generic string map
if (stringIndexType) {
return {
key: buildMockFromType(stringIndexType, {
...context,
depth: depth + 1,
path: [...path, 'key'],
seen: new Set(seen)
})
};
}
return {};
}
return buildMockFromObject(type, context);
}
return null;
}
function buildMockFromObject(type, context) {
const { checker, depth, maxDepth, path, mockConfig, seen } = context;
const result = {};
const props = type.getProperties();
for (const prop of props) {
const name = prop.getName();
const decl = prop.valueDeclaration ?? prop.declarations?.[0];
const propType = checker.getTypeOfSymbolAtLocation(prop, decl);
const doc = getSymbolDocText(prop, checker);
const value = buildMockFromType(propType, {
checker,
depth: depth + 1,
maxDepth,
path: [...path, name],
mockConfig,
seen: new Set(seen)
});
result[name] = applyFieldHeuristics(name, value, propType, mockConfig, doc, [...path, name]);
}
return result;
}
function applyFieldHeuristics(name, value, propType, mockConfig, doc, pathArr) {
if (name === 'success' && isBooleanLikeType(propType)) {
return true;
}
if (name === 'error' && isObjectLikeType(propType)) {
return { code: '0', message: '' };
}
// common pagination flags
if (name === 'hasMore' && isBooleanLikeType(propType)) {
const total = typeof mockConfig?.total === 'number' ? mockConfig.total : 0;
const pageSize = typeof mockConfig?.pageSize === 'number' ? mockConfig.pageSize : 0;
if (total > 0 && pageSize > 0) {
return total > pageSize;
}
return false;
}
// Use doc hint for status enum
if (name === 'status' && typeof value === 'string') {
if (typeof doc === 'string' && /valid=.*used=.*locked=.*expired=.*not_effective=/i.test(doc)) {
return 'valid';
}
return 'valid';
}
// common enum-like fields by business meaning
if (name === 'status' && typeof value === 'string') {
return 'valid';
}
if (name === 'category' && typeof value === 'string') {
return 'COUPON';
}
if (name === 'couponType' && typeof value === 'string') {
return 'discount';
}
if (name === 'redeemTimeType' && typeof value === 'string') {
return 'OPENING_TIME';
}
// fields that represent "used" info should be empty for valid coupons
if ((name === 'usedStoreId' || name === 'usedStoreName' || name === 'usedTime') && typeof value === 'string') {
return '';
}
// opening time mode does not need a fixed time range
if (name === 'redeemTimes' && typeof value === 'string') {
return '';
}
// number business meanings
if (name === 'couponAmount' && typeof value === 'number') {
// If doc says discount/deduct, default discount 10%
return 10;
}
if (name === 'useOrderAmountThreshold' && typeof value === 'number') {
return 50000;
}
if (name === 'effectiveDays' && typeof value === 'number') {
return 30;
}
if (name === 'remainingDays' && typeof value === 'number') {
return 7;
}
if (name === 'orderUseLimitNum' && typeof value === 'number') {
return 1;
}
// Time fields: try to be coherent
if (typeof value === 'string' && /time/i.test(name)) {
// for "receivedTime": now - minutes by index
const idx = getIndexFromPath(pathArr);
if (name === 'receivedTime') return new Date(Date.now() - idx * 2 * 60_000).toISOString();
if (name === 'effectiveStartTime') return new Date(Date.now()).toISOString();
if (name === 'effectiveEndTime') return new Date(Date.now() + 30 * 24 * 60 * 60_000).toISOString();
}
if (name === 'total' && typeof mockConfig?.total === 'number') {
return mockConfig.total;
}
if (name === 'pageSize' && typeof mockConfig?.pageSize === 'number') {
return mockConfig.pageSize;
}
// If doc hints "券id/模版id" etc, make them stable
if (typeof value === 'number') {
const idx = getIndexFromPath(pathArr);
if (typeof doc === 'string') {
if (/券id/i.test(doc) || name === 'id') return idx + 1;
if (/模版id/i.test(doc) || name === 'templateId') return 10000 + (idx + 1);
}
}
return value;
}
function isBooleanLikeType(type) {
if (type.flags & (ts.TypeFlags.Boolean | ts.TypeFlags.BooleanLiteral)) {
return true;
}
if (type.isUnion()) {
return type.types.some((t) => isBooleanLikeType(t));
}
return false;
}
function isObjectLikeType(type) {
if (type.flags & ts.TypeFlags.Object) {
return true;
}
if (type.isUnion()) {
return type.types.some((t) => isObjectLikeType(t));
}
return false;
}
function pickUnionType(types) {
const preferred = types.find((item) => !isNullableType(item));
return preferred || types[0];
}
function isNullableType(type) {
return Boolean(type.flags & (ts.TypeFlags.Null | ts.TypeFlags.Undefined));
}
function getArraySize(name, mockConfig) {
// Lists: should respect listSize/pageSize; other arrays should stay small
const fieldName = String(name || '');
const isListField = /list|records|items|couponList|orderList/i.test(fieldName);
const isIdsField = /Ids$/i.test(fieldName);
if (isListField) {
if (typeof mockConfig?.listSize === 'number') {
return Math.max(mockConfig.listSize, 0);
}
if (typeof mockConfig?.pageSize === 'number') {
return Math.max(mockConfig.pageSize, 0);
}
return 1;
}
// Id arrays (e.g. scopeStoreIds) are usually small
if (isIdsField) {
return 2;
}
// Non-list arrays: keep small and stable
if (typeof mockConfig?.listSize === 'number' && mockConfig.listSize > 0) {
return Math.min(2, mockConfig.listSize);
}
return 2;
}
function createStringSample(name, pathArr) {
if (!name) {
return '';
}
const index = getIndexFromPath(pathArr);
const field = String(name);
const parentKey = getParentKey(pathArr);
// Array element (name is "0"/"1"...): use parent field semantics
if (/^\d+$/.test(field)) {
if (/scopeStoreIds/i.test(parentKey)) {
return `store-${String(index + 1).padStart(3, '0')}`;
}
if (/storeIds/i.test(parentKey)) {
return `store-${String(index + 1).padStart(3, '0')}`;
}
if (/scope/i.test(parentKey) && /Ids$/i.test(parentKey)) {
return String(index + 1);
}
return `item-${index + 1}`;
}
// localized strings
if (field === 'id-ID') {
if (/couponLabel/i.test(parentKey)) return 'Diskon';
if (/couponSubTitle/i.test(parentKey)) return `Diskon 10% min. Rp 50.000`;
if (/description/i.test(parentKey)) return `Kupon diskon 10% untuk pembelian makanan`;
if (/name/i.test(parentKey)) return `Kupon ${index + 1}`;
return `Teks ${index + 1}`;
}
if (field === 'en-US') {
if (/couponLabel/i.test(parentKey)) return 'Discount';
if (/couponSubTitle/i.test(parentKey)) return `10% off, min Rp 50,000`;
if (/description/i.test(parentKey)) return `10% discount coupon for food purchases`;
if (/name/i.test(parentKey)) return `Coupon ${index + 1}`;
return `Text ${index + 1}`;
}
if (field === 'zh-CN') {
if (/couponLabel/i.test(parentKey)) return '折扣';
if (/couponSubTitle/i.test(parentKey)) return `满50000印尼盾享10%折扣`;
if (/description/i.test(parentKey)) return `食品购买10%折扣券`;
if (/name/i.test(parentKey)) return `优惠券${index + 1}`;
return `文本${index + 1}`;
}
// code-like
if (/couponCode/i.test(field)) {
return `CPN-${String(index + 1).padStart(4, '0')}`;
}
// url-like
// Always return a working image URL for image resources.
if (/url$/i.test(field) || /logoUrl/i.test(field) || /image/i.test(field)) {
return DEFAULT_MOCK_IMAGE_URL;
}
// geo / address / distance (often string in payload)
if (/latitude/i.test(field)) return '-6.200000';
if (/longitude/i.test(field)) return '106.816666';
if (/distance/i.test(field)) return `${(index + 1) * 0.3}km`;
if (/address/i.test(field)) return `Jl. Sudirman No. ${index + 1}`;
if (/province/i.test(field)) return 'DKI Jakarta';
if (/city/i.test(field)) return 'Jakarta';
if (/area/i.test(field) || /district/i.test(field)) return 'Central Jakarta';
// store ids / names
if (/storeId$/i.test(field)) {
return `store-${String(index + 1).padStart(3, '0')}`;
}
if (/storeName$/i.test(field) || /applicableStoreName/i.test(field)) {
return `Store ${index + 1}`;
}
// product/asset fields
if (/assetType/i.test(field)) return 'PRODUCT';
if (/remainingStock/i.test(field)) return String(100 - index);
if (/discountAmount/i.test(field)) return 'Rp 10.000';
if (/assetType/i.test(field)) return 'PRODUCT';
if (/id$/i.test(field)) {
// generic id should look like an id, not "mock-id"
return String(index + 1);
}
if (/code/i.test(name)) {
return `CODE-${String(index + 1).padStart(4, '0')}`;
}
if (/time|date/i.test(field)) {
return new Date(Date.now() - index * 60_000).toISOString();
}
if (/ruleDescription|useRuleDescription/i.test(field)) {
return 'Minimum purchase Rp 50,000';
}
if (/name/i.test(field)) {
return `${field} ${index + 1}`;
}
if (/description|title|subtitle/i.test(field)) {
return `${field} text ${index + 1}`;
}
// Generic non-empty fallback for unknown string fields
return `${field}-${index + 1}`;
}
function getSymbolDocText(symbol, checker) {
try {
const parts = symbol.getDocumentationComment(checker);
const text = ts.displayPartsToString(parts);
return (text || '').trim();
} catch {
return '';
}
}
function createNumberSample(name, pathArr) {
if (!name) {
return 1;
}
const index = getIndexFromPath(pathArr);
const field = String(name);
if (/id$/i.test(field)) {
return index + 1;
}
if (/percent/i.test(field)) return 10;
if (/ratio/i.test(field)) return 0.1;
if (/amount|price|fee/i.test(field)) {
return 1000;
}
if (/threshold/i.test(field)) {
return 50000;
}
return 0;
}
function createBooleanSample(name, pathArr) {
const field = String(name || '');
if (field === 'hasMore') return false;
if (/is|has|enable|visible|valid/i.test(field)) return true;
// stable alternating for list items
const index = getIndexFromPath(pathArr);
return index % 2 === 0;
}
function getTypeId(type) {
const candidate = type;
if (candidate && typeof candidate === 'object' && 'id' in candidate) {
const id = candidate.id;
return typeof id === 'number' ? id : null;
}
return null;
}
function mergeDeep(target, source) {
if (!source || typeof source !== 'object') {
return target ?? source;
}
if (Array.isArray(source)) {
return source.slice();
}
const output = isPlainObject(target) ? { ...target } : {};
for (const [key, value] of Object.entries(source)) {
if (isPlainObject(value)) {
output[key] = mergeDeep(output[key], value);
} else {
output[key] = value;
}
}
return output;
}
function isPlainObject(value) {
return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
}
function parseCliArgs(argv) {
const out = { functionName: null, pageSize: null, total: null, outFile: null };
for (let i = 0; i < argv.length; i += 1) {
const arg = argv[i];
const next = argv[i + 1];
if (arg === '--functionName' && next) {
out.functionName = next;
i += 1;
continue;
}
if (arg === '--pageSize' && next) {
out.pageSize = Number(next);
i += 1;
continue;
}
if (arg === '--total' && next) {
out.total = Number(next);
i += 1;
continue;
}
if (arg === '--out' && next) {
out.outFile = next;
i += 1;
continue;
}
}
return out;
}
function runCliIfNeeded() {
const cli = parseCliArgs(process.argv.slice(2));
if (!cli.functionName) {
return false;
}
const size = Number.isFinite(cli.pageSize) ? cli.pageSize : Number.isFinite(cli.total) ? cli.total : 1;
const mock = generateMockFromFunction(cli.functionName, {
pageSize: size,
total: Number.isFinite(cli.total) ? cli.total : undefined,
listSize: size
});
const text = JSON.stringify(mock, null, 2);
if (cli.outFile) {
const outPath = path.isAbsolute(cli.outFile) ? cli.outFile : path.join(projectRoot, cli.outFile);
fs.writeFileSync(outPath, text, 'utf8');
} else {
console.log(text);
}
return true;
}
if (!runCliIfNeeded()) {
startServer().catch((error) => {
console.error('[mcp-mock] failed to start:', error);
process.exit(1);
});
}
package.json配置命令
"mcp:mock": "node scripts/mcp-mock-server/index.cjs"
配置mcp
"yapi-mock-generator": {
"command": "node",
"args": ["D:\\app\\scripts\\mcp-mock-server\\index.cjs"],
"cwd": "D:\\app",
"env": {
"YAPI_DIR": "D:\\app\\apps\\capacitor\\src\\yapi",
"NODE_PATH": "D:\\app\\node_modules"
}
}