Skip to main content
Glama
NorthSeacoder

Frontend Test Generation & Code Review MCP Server

generate-cursor-rule.ts6.64 kB
/** * GenerateCursorRuleTool - 生成项目特定的 Cursor 规则配置 * * 根据项目检测结果,生成适合项目的测试生成规则文件 */ import { z } from 'zod'; import { BaseTool } from '../core/base-tool.js'; import type { ToolMetadata } from '../core/base-tool.js'; import { getAppContext } from '../core/app-context.js'; import { logger } from '../utils/logger.js'; import { readFile, writeFile, mkdir } from 'fs/promises'; import { dirname, resolve } from 'path'; import { existsSync } from 'fs'; import { fileURLToPath } from 'url'; const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); const ArgsSchema = z.object({ workspaceId: z.string().describe('工作区 ID'), outputPath: z.string().optional().default('.cursor/rule/fe-mcp.md').describe('输出路径(相对于项目根目录)'), }); type GenerateCursorRuleArgs = z.infer<typeof ArgsSchema>; interface GenerateCursorRuleOutput { filePath: string; content: string; } export class GenerateCursorRuleTool extends BaseTool<GenerateCursorRuleArgs, GenerateCursorRuleOutput> { getMetadata(): ToolMetadata { return { name: 'generate-cursor-rule', description: `生成项目特定的 Cursor 规则配置文件。 此工具会根据项目检测结果自动生成适合该项目的测试生成规则,包括: - 项目信息(类型、测试框架、Monorepo 配置) - 测试生成配置(场景优先级、最大测试数) - 代码规范(React、Mock、断言、异步) - Monorepo 建议 - 排除规则 - 已有测试处理策略 - 自动修复建议 **参数**: - workspaceId: 工作区 ID(从 fetch-diff-from-repo 获取) - outputPath: 输出路径(默认 .cursor/rule/fe-mcp.md) **返回**: - filePath: 生成的文件路径 - content: 文件内容`, inputSchema: {}, }; } getZodSchema() { return ArgsSchema; } async executeImpl(args: GenerateCursorRuleArgs): Promise<GenerateCursorRuleOutput> { const { workspaceId, outputPath } = args; logger.info('[GenerateCursorRule] Generating cursor rule', { workspaceId, outputPath, }); const context = getAppContext(); const { workspaceManager, projectDetector } = context; if (!workspaceManager) { throw new Error('WorkspaceManager not initialized'); } if (!projectDetector) { throw new Error('ProjectDetector not initialized'); } // 获取工作区信息 const workspace = workspaceManager.getWorkspace(workspaceId); if (!workspace) { throw new Error(`Workspace not found: ${workspaceId}`); } const projectRoot = workspace.workDir; // 获取项目配置 const projectConfig = await projectDetector.detectProject( projectRoot, workspace.packageRoot, [] ); logger.info('[GenerateCursorRule] Project config retrieved', { isMonorepo: projectConfig.isMonorepo, testFramework: projectConfig.testFramework, }); // 读取模板文件 const templatePath = resolve(__dirname, '../../docs/cursor-rule-template.md'); let template: string; try { template = await readFile(templatePath, 'utf-8'); } catch (error) { logger.error('[GenerateCursorRule] Failed to read template', { error }); throw new Error(`Failed to read template file: ${templatePath}`); } // 替换模板变量 const content = this.populateTemplate(template, { workspace, projectConfig, }); // 写入文件 const absoluteOutputPath = resolve(projectRoot, outputPath); const outputDir = dirname(absoluteOutputPath); try { if (!existsSync(outputDir)) { await mkdir(outputDir, { recursive: true }); } await writeFile(absoluteOutputPath, content, 'utf-8'); logger.info('[GenerateCursorRule] Cursor rule generated', { filePath: absoluteOutputPath, }); return { filePath: outputPath, content, }; } catch (error) { logger.error('[GenerateCursorRule] Failed to write file', { error }); throw new Error(`Failed to write cursor rule file: ${absoluteOutputPath}`); } } /** * 填充模板变量 */ private populateTemplate( template: string, data: { workspace: any; projectConfig: any; } ): string { const { workspace, projectConfig } = data; const replacements: Record<string, string> = { '{{PROJECT_NAME}}': this.extractProjectName(workspace.repoUrl), '{{GENERATED_AT}}': new Date().toISOString(), '{{REPO_URL}}': workspace.repoUrl || 'N/A', '{{BRANCH}}': workspace.sourceBranch || workspace.branch || 'N/A', '{{BASELINE_BRANCH}}': workspace.baselineBranch || 'main', '{{PROJECT_TYPE}}': projectConfig.isMonorepo ? 'Monorepo' : '单仓库', '{{MONOREPO_TYPE}}': projectConfig.monorepoType || 'N/A', '{{PACKAGE_ROOT}}': projectConfig.packageRoot || workspace.packageRoot || projectConfig.projectRoot, '{{AFFECTED_SUBPROJECTS}}': this.formatList(projectConfig.affectedSubProjects || workspace.affectedSubProjects), '{{TESTABLE_SUBPROJECTS}}': this.formatList(projectConfig.testableSubProjects || workspace.testableSubProjects), '{{TEST_FRAMEWORK}}': projectConfig.testFramework || 'vitest', '{{HAS_EXISTING_TESTS}}': projectConfig.hasExistingTests ? '是' : '否', '{{TEST_PATTERN}}': projectConfig.testPattern || '**/*.{test,spec}.{ts,tsx,js,jsx}', '{{MAX_TESTS}}': process.env.MAX_TESTS || '10', '{{MAX_FIX_ATTEMPTS}}': process.env.FIX_MAX_ATTEMPTS || '3', '{{FIX_CONFIDENCE_THRESHOLD}}': process.env.FIX_CONFIDENCE_THRESHOLD || '0.5', }; let result = template; for (const [key, value] of Object.entries(replacements)) { result = result.replace(new RegExp(key, 'g'), value); } return result; } /** * 从仓库 URL 提取项目名称 */ private extractProjectName(repoUrl: string): string { if (!repoUrl) { return 'Unknown Project'; } // 如果是本地路径 if (!repoUrl.startsWith('http') && !repoUrl.startsWith('git@')) { const parts = repoUrl.split('/'); return parts[parts.length - 1] || 'Local Project'; } // 如果是 Git URL const match = repoUrl.match(/\/([^\/]+?)(\.git)?$/); if (match && match[1]) { return match[1]; } return 'Unknown Project'; } /** * 格式化列表 */ private formatList(items?: string[]): string { if (!items || items.length === 0) { return ' - 无'; } return items.map(item => ` - ${item}`).join('\n'); } }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/NorthSeacoder/fe-testgen-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server