diff --git a/src/api/builder/schema.ts b/src/api/builder/schema.ts index 6a663c57..82afae4a 100644 --- a/src/api/builder/schema.ts +++ b/src/api/builder/schema.ts @@ -54,6 +54,36 @@ export const SchemaWebMobilePackages = z.object({ embedWebDebugger: z.boolean().default(false).describe('Whether to embed Web debugger'), // 是否嵌入 Web 端调试工具 }).describe('Web Mobile Platform Configuration'); // Web Mobile 平台配置 +// iOS Configuration // iOS 配置 +const SchemaIOSPackageBase = z.object({ + packageName: z.string() + .min(1, 'iOS package name cannot be empty') // iOS包名不能为空 + .describe('iOS application package name (required)'), // iOS应用包名(必填) + + osTarget: z.object({ + iphoneos: z.boolean().optional(), + simulator: z.boolean().optional(), + }).optional(), + targetVersion: z.string().optional(), + developerTeam: z.string().optional(), +}).describe('iOS platform specific configuration'); // iOS平台特定配置 + +// Mac Packages Configuration // Mac Packages 配置 +export const SchemaMacPackage = z.object({ + packageName: z.string() + .min(1, 'Mac package name cannot be empty') // Mac包名不能为空 + .describe('Mac application package name (required)') // Mac应用包名(必填) +}).describe('Mac platform specific configuration'); // Mac平台特定配置 + +// Android Packages Configuration // Android Packages 配置 +export const SchemaAndroidPackage = z.object({ + packageName: z.string() + .min(1, 'Android package name cannot be empty') // Android包名不能为空 + .describe('Android application package name (required)'), // Android应用包名(必填) + keystorePath: z.string().optional().describe('Keystore file path'), // 签名文件路径 + keystorePassword: z.string().optional().describe('Keystore password'), // 签名文件密码 +}).describe('Android platform specific configuration'); // Android平台特定配置 + // ==================== Basic Build Configuration ==================== // 基础构建配置 // Core Build Field Definitions (excluding platform and packages, defined in platform-specific configurations) // 核心构建字段定义(不包含 platform 和 packages,这些在平台特定配置中定义) @@ -124,11 +154,14 @@ export const SchemaBuildRuntimeOptions = z.object({ // ==================== Platform Specific Complete Build Options ==================== // 平台特定的完整构建选项 +// Base Build Options (including runtime options) // 基础构建选项(包含运行时选项) +export const SchemaBuildBaseOption = SchemaBuildRuntimeOptions + .merge(SchemaBuildBaseConfig); + // Web Desktop Complete Build Options (Input, all fields optional) // Web Desktop 完整构建选项(入参,所有字段可选) -export const SchemaWebDesktopBuildOption = SchemaBuildRuntimeOptions - .merge(SchemaBuildBaseConfig) +export const SchemaWebDesktopBuildOption = SchemaBuildBaseOption .extend({ - platform: z.literal('web-desktop').describe('Build Platform').optional(), // 构建平台 + platform: z.literal('web-desktop').describe('Build Platform'), // 构建平台 packages: z.object({ 'web-desktop': SchemaWebDesktopPackages.partial() }).optional().describe('Web Desktop Platform Specific Configuration') // Web Desktop 平台特定配置 @@ -136,29 +169,99 @@ export const SchemaWebDesktopBuildOption = SchemaBuildRuntimeOptions .describe('Web Desktop Complete Build Options (all fields optional)'); // Web Desktop 完整构建选项(所有字段可选) // Web Mobile Complete Build Options (Input, all fields optional) // Web Mobile 完整构建选项(入参,所有字段可选) -export const SchemaWebMobileBuildOption = SchemaBuildRuntimeOptions - .merge(SchemaBuildBaseConfig) +export const SchemaWebMobileBuildOption = SchemaBuildBaseOption .extend({ - platform: z.literal('web-mobile').describe('Build Platform').optional(), // 构建平台 + platform: z.literal('web-mobile').describe('Build Platform'), // 构建平台 packages: z.object({ 'web-mobile': SchemaWebMobilePackages.partial() }).optional().describe('Web Mobile Platform Specific Configuration') // Web Mobile 平台特定配置 }) .describe('Web Mobile Complete Build Options (all fields optional)'); // Web Mobile 完整构建选项(所有字段可选) -// General Build Options (for API input) // 通用构建选项(用于 API 入参) -export const SchemaBuildOption = z.union([ - SchemaWebDesktopBuildOption, - SchemaWebMobileBuildOption, - SchemaBuildRuntimeOptions - .merge(SchemaBuildBaseConfig) +// Windows Build Options // Windows 构建选项 +export const SchemaWindowsBuildOption = SchemaBuildBaseOption + .extend({ + platform: z.literal('windows').describe('Build Platform') // 构建平台 + }) + .describe('Windows Platform Build Options'); // Windows平台构建选项 + +// iOS Build Options // iOS 构建选项 +const SchemaIOSPackageWithCatchall = SchemaIOSPackageBase.catchall(z.any()); +export const SchemaIOSPackage = SchemaIOSPackageWithCatchall + .refine((data) => { + // 当 osTarget.iphoneos 为 true 时,developerTeam 必须有值 + if (data.osTarget && data.osTarget.iphoneos === true) { + return data.developerTeam && data.developerTeam.trim().length > 0; + } + return true; + }, { + message: 'developerTeam is required when osTarget.iphoneos is true', + path: ['developerTeam'] + }) + .describe('iOS Platform Specific Configuration'); + +export const SchemaIOSBuildOption = SchemaBuildBaseOption + .extend({ + platform: z.literal('ios').describe('Build Platform'), // 构建平台 + packages: z.object({ + ios: SchemaIOSPackage + .optional() + }).describe('iOS Platform Configuration') // iOS平台配置 + }) + .describe('iOS Platform Build Options'); // iOS平台构建选项 + +// Android Build Options // Android 构建选项 +export const SchemaAndroidBuildOption = SchemaBuildBaseOption + .extend({ + platform: z.literal('android').describe('Build Platform'), // 构建平台 + packages: z.object({ + android: SchemaAndroidPackage + .catchall(z.any()) // 允许其他任意字段 + .optional() + }).describe('Android Platform Configuration') // Android平台配置 + }) + .describe('Android Platform Build Options'); // Android平台构建选项 + + +// Mac Build Options // Mac 构建选项 +export const SchemaMacBuildOption = SchemaBuildBaseOption + .extend({ + platform: z.literal('mac').describe('Build Platform'), // 构建平台 + packages: z.object({ + mac: SchemaMacPackage + .catchall(z.any()) // 允许其他任意字段 + .optional() + }).describe('Mac Platform Configuration') // Mac平台配置 + }) + .describe('Mac Platform Build Options'); // Mac平台构建选项 + + +// Other Platform Build Options (Generic) // 其他平台构建选项(通用) +export const SchemaOtherPlatformBuildOption = SchemaBuildBaseOption .extend({ platform: SchemaPlatform.optional().describe('Build Platform'), // 构建平台 packages: z.any().optional().describe('Platform Specific Configuration'), // 平台特定配置 }) -]).optional().describe('Build Options (for API input)'); // 构建选项(用于 API 入参) + .describe('Other Platform Build Options (Generic)'); // 其他平台构建选项(通用) + +export const SchemaKnownBuildOptions = [ + SchemaWebDesktopBuildOption, + SchemaWebMobileBuildOption, + SchemaWindowsBuildOption, + SchemaIOSBuildOption, + SchemaMacBuildOption, + SchemaAndroidBuildOption, +]; +// ==================== Create discriminatedUnion ==================== // +export const SchemaBuildOption = z.discriminatedUnion('platform', [ + ...SchemaKnownBuildOptions, + SchemaOtherPlatformBuildOption +] as any).default({}).describe('Build options (with platform preprocessing)'); // 构建选项(带平台预处理) + export type TBuildOption = z.infer; +// ==================== Result Type Definitions ==================== // 结果类型定义 + export const SchemaResultBase = z.object({ code: z.number().int().describe('Build exit code, 0 means success, others mean failure, 32 means parameter error, 34 means build failure, 37 means build busy, 38 means static compile error, 50 means unknown error'), // 构建的退出码, 0 表示成功, 其他表示失败, 32 表示参数错误, 34 表示构建失败, 37 表示构建繁忙, 38 表示静态编译错误, 50 表示未知错误 dest: z.string().optional().describe('Generated game folder after build, currently output as project protocol address'), // 构建后的游戏生成文件夹,目前输出为 project 协议地址 @@ -215,36 +318,17 @@ export type TPreviewSettingsResult = z.infer // ==================== Build Configuration Query Result ==================== // 构建配置查询结果 -// Web Desktop Build Configuration Query Result (All fields required, including packages, excluding runtime options) // Web Desktop 构建配置查询结果(所有字段必填,包含 packages,不包含运行时选项) -const SchemaWebDesktopBuildConfigResult = BuildConfigCoreFields.partial() - .extend({ - platform: z.literal('web-desktop').describe('Build Platform'), // 构建平台 - packages: z.object({ - 'web-desktop': SchemaWebDesktopPackages - }).describe('Web Desktop Platform Specific Configuration') // Web Desktop 平台特定配置 - }) - .describe('Web Desktop Build Configuration Query Result'); // Web Desktop 构建配置查询结果 - -// Web Mobile Build Configuration Query Result (All fields required, including packages, excluding runtime options) // Web Mobile 构建配置查询结果(所有字段必填,包含 packages,不包含运行时选项) -const SchemaWebMobileBuildConfigResult = BuildConfigCoreFields.partial() - .extend({ - platform: z.literal('web-mobile').describe('Build Platform'), // 构建平台 - packages: z.object({ - 'web-mobile': SchemaWebMobilePackages - }).describe('Web Mobile Platform Specific Configuration') // Web Mobile 平台特定配置 - }) - .describe('Web Mobile Build Configuration Query Result'); // Web Mobile 构建配置查询结果 - +// Build configuration query result: union type, all fields required, including packages, excluding runtime options // Build Configuration Query Result: Union type, all fields required, including packages, excluding runtime options // 构建配置查询结果:union 类型,所有字段必填,包含 packages,不包含运行时选项 export const SchemaBuildConfigResult = z.union([ - SchemaWebDesktopBuildConfigResult, - SchemaWebMobileBuildConfigResult, - BuildConfigCoreFields.partial() - .extend({ - platform: SchemaPlatform, - packages: z.any().optional().describe('Platform Specific Configuration'), // 平台特定配置 - }) -]).nullable().describe('Build Configuration Query Result (all fields required, including packages)'); // 构建配置查询结果(所有字段必填,包含 packages) + SchemaWebDesktopBuildOption.omit({ configPath: true, skipCheck: true, taskId: true, taskName: true }), + SchemaWebMobileBuildOption.omit({ configPath: true, skipCheck: true, taskId: true, taskName: true }), + SchemaWindowsBuildOption.omit({ configPath: true, skipCheck: true, taskId: true, taskName: true }), + SchemaIOSBuildOption.omit({ configPath: true, skipCheck: true, taskId: true, taskName: true }), + SchemaAndroidBuildOption.omit({ configPath: true, skipCheck: true, taskId: true, taskName: true }), + SchemaMacBuildOption.omit({ configPath: true, skipCheck: true, taskId: true, taskName: true }), + SchemaOtherPlatformBuildOption.omit({ configPath: true, skipCheck: true, taskId: true, taskName: true }), +]).nullable().describe('Build configuration query result (all fields required, including packages)'); // 构建配置查询结果(所有字段必填,包含 packages) export type TBuildConfigResult = z.infer; diff --git a/src/mcp/hooks/builder.hook.ts b/src/mcp/hooks/builder.hook.ts new file mode 100644 index 00000000..8dadde58 --- /dev/null +++ b/src/mcp/hooks/builder.hook.ts @@ -0,0 +1,140 @@ +import { z } from 'zod'; +import { join, resolve } from 'path'; +import { existsSync, readdirSync, readFileSync } from 'fs'; +import { SchemaBuildBaseOption, SchemaKnownBuildOptions, SchemaOtherPlatformBuildOption } from '../../api/builder/schema'; + +export class BuilderHook { + private dynamicPlatforms: string[] = []; + + constructor() { + this.scanPlatformPackages(); + } + + /** + * 扫描 packages/platforms 目录下的平台插件 + */ + private scanPlatformPackages() { + const platforms: string[] = []; + const platformsDir = resolve(__dirname, '../../../packages/platforms'); + + if (!existsSync(platformsDir)) { + this.dynamicPlatforms = platforms; + return; + } + + try { + const dirs = readdirSync(platformsDir); + for (const dir of dirs) { + const pkgJsonPath = join(platformsDir, dir, 'package.json'); + if (existsSync(pkgJsonPath)) { + try { + const pkgContent = JSON.parse(readFileSync(pkgJsonPath, 'utf-8')); + // 检查是否是平台插件 (contributes.builder.register === true) + if (pkgContent?.contributes?.builder?.register === true) { + // 优先使用 contributes.builder.platform,如果没有则使用 package.name + const platformName = pkgContent.contributes.builder.platform || pkgContent.name; + if (platformName) { + platforms.push(platformName); + } + } + } catch (e) { + console.warn(`Failed to parse package.json for ${dir}:`, e); + } + } + } + } catch (e) { + console.error('Failed to scan platform packages:', e); + } + + this.dynamicPlatforms = platforms; + } + + public onRegisterParam(toolName: string, param: any, inputSchemaFields: Record) { + if (toolName !== 'builder-build') return; + + const knownPlatforms = ['web-desktop', 'web-mobile', 'android', 'ios', 'windows', 'mac']; + // 合并去重 + const allPlatforms = Array.from(new Set([...knownPlatforms, ...this.dynamicPlatforms])); + const platformDesc = `Platform Identifier (e.g., ${allPlatforms.join(', ')})`; + + if (param.name === 'options') { + inputSchemaFields[param.name] = z.any(); + + // 动态构建 SchemaBuildOption + const dynamicSchemas = this.dynamicPlatforms.map(platform => { + return SchemaBuildBaseOption.extend({ + platform: z.literal(platform).describe('Build platform'), + packages: z.object({ + [platform]: z.any().optional().describe(`${platform} platform specific configuration`) + }).optional().describe(`${platform} platform specific configuration`) + }).describe(`${platform} complete build options`); + }); + + const newSchema = z.discriminatedUnion('platform', [ + ...SchemaKnownBuildOptions, + ...dynamicSchemas, + SchemaOtherPlatformBuildOption + ] as any).default({}).describe('Build options (with platform preprocessing)'); + + // 更新原始 meta 中的 schema,以便 list handler 使用 + param.schema = newSchema; + + } else if (param.name === 'platform') { + // 动态更新 platform 参数的描述,包含扫描到的平台 + const newPlatformSchema = param.schema.describe(platformDesc); + inputSchemaFields[param.name] = newPlatformSchema; + param.schema = newPlatformSchema; + } + } + + public onBeforeExecute(toolName: string, args: any) { + if (toolName !== 'builder-build') return; + + if (!args.options) { + args.options = {}; + } + + // 处理 configPath + let options = args.options; + if (options.configPath) { + const configPath = options.configPath; + if (existsSync(configPath)) { + try { + const fileContent = JSON.parse(readFileSync(configPath, 'utf-8')); + // 合并配置,args.options 优先级高于配置文件 + options = args.options = { + ...fileContent, + ...options + }; + + // 删除 configPath 字段 + delete options.configPath; + } catch (e) { + console.warn(`Failed to load config file: ${configPath}`, e); + } + } + } + + if (typeof options === 'object') { + if (!options.platform) { + // 注入 platform + options.platform = args.platform; + } + + // sourceMaps exported by CocosEditor is a string, so need to convert it to boolean + if (options.sourceMaps && typeof options.sourceMaps !== 'boolean') { + if (options.sourceMaps === 'true') { + options.sourceMaps = true; + } else if (options.sourceMaps === 'false') { + options.sourceMaps = false; + } + } + } + } + + public onValidationFailed(toolName: string, paramName: string, error: any) { + if (toolName === 'builder-build') { + throw new Error(`Parameter validation failed for ${paramName}: ${error instanceof Error ? error.message : String(error)}`); + } + } +} diff --git a/src/mcp/mcp.middleware.ts b/src/mcp/mcp.middleware.ts index 68bf4b84..a37798ec 100644 --- a/src/mcp/mcp.middleware.ts +++ b/src/mcp/mcp.middleware.ts @@ -8,6 +8,7 @@ import * as pkgJson from '../../package.json'; import { join } from 'path'; import { ResourceManager } from './resources'; import { HTTP_STATUS } from '../api/base/schema-base'; +import { BuilderHook } from './hooks/builder.hook'; import type { HttpStatusCode } from '../api/base/schema-base'; import stripAnsi from 'strip-ansi'; import { zodToJsonSchema } from 'zod-to-json-schema'; @@ -15,8 +16,10 @@ import { ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js'; export class McpMiddleware { private server: McpServer; private resourceManager: ResourceManager; + private builderHook: BuilderHook; constructor() { + this.builderHook = new BuilderHook(); // 创建 MCP server this.server = new McpServer({ name: 'cocos-cli-mcp-server', @@ -82,12 +85,8 @@ export class McpMiddleware { .sort((a, b) => a.index - b.index) .forEach(param => { if (param.name) { - // 特殊处理 builder-build 的 options 参数 - // 使用 z.any() 绕过 SDK 的初步验证,以便我们在 callback 中注入 platform - // 实际的严格验证会在 prepareMethodArguments 中使用 meta.paramSchemas 进行 - if (toolName === 'builder-build' && param.name === 'options') { - inputSchemaFields[param.name] = z.any(); - } else { + this.builderHook.onRegisterParam(toolName, param, inputSchemaFields); + if (!inputSchemaFields[param.name]) { inputSchemaFields[param.name] = param.schema; } } @@ -101,15 +100,7 @@ export class McpMiddleware { inputSchemaFields, async (args) => { // args 已经是验证过的参数对象 (对于 builder-build.options 是 any) - if (toolName === 'builder-build') { - if (!args.options) { - args.options = {}; - } - if (typeof args.options === 'object' && !args.options.platform) { - // 注入 platform - args.options.platform = args.platform; - } - } + this.builderHook.onBeforeExecute(toolName, args); try { // 这里的 prepareMethodArguments 主要是为了按顺序排列参数给 apply 使用 // 注意:args 是对象,prepareMethodArguments 需要处理对象 @@ -236,10 +227,7 @@ export class McpMiddleware { console.error(`Parameter validation failed for ${paramName}:`, error); - // 如果是 builder-build 接口,参数校验失败直接报错 - if (toolName === 'builder-build') { - throw new Error(`Parameter validation failed for ${paramName}: ${error instanceof Error ? error.message : String(error)}`); - } + this.builderHook.onValidationFailed(toolName, paramName, error); // 使用原始值 methodArgs[param.index] = value;