feat: 完成第一阶段核心功能实现\n\n- 添加 manage_components 工具\n- 添加 manage_script 工具(默认创建TS脚本)\n- 添加 batch_execute 工具\n- 添加 manage_asset 工具\n- 修复面板布局问题\n- 添加默认父目录创建功能\n- 更新 README 文档\n- 创建 DEVELOPMENT 开发文档

This commit is contained in:
火焰库拉
2026-01-31 16:48:21 +08:00
parent b5f745446c
commit 3b2e78eee7
6 changed files with 1370 additions and 34 deletions

420
DEVELOPMENT.md Normal file
View File

@@ -0,0 +1,420 @@
# MCP Bridge 插件开发流程文档
本文档记录了 MCP Bridge 插件的完整开发流程,包括核心架构设计、功能实现、测试与调试等各个阶段。
## 1. 项目初始化
### 1.1 目录结构搭建
```
mcp-bridge/
├── main.js # 插件主入口
├── scene-script.js # 场景脚本
├── mcp-proxy.js # MCP 代理
├── README.md # 项目说明
├── DEVELOPMENT.md # 开发流程文档
├── package.json # 插件配置
└── panel/ # 面板目录
├── index.html # 面板界面
└── index.js # 面板逻辑
```
### 1.2 插件配置
`package.json` 中配置插件信息:
```json
{
"name": "mcp-bridge",
"version": "1.0.0",
"description": "MCP Bridge for Cocos Creator",
"main": "main.js",
"panel": {
"main": "panel/index.html",
"type": "dockable",
"title": "MCP Bridge",
"width": 800,
"height": 600
},
"contributions": {
"menu": [
{
"path": "Packages/MCP Bridge",
"label": "Open Test Panel",
"message": "open-test-panel"
}
]
}
}
```
## 2. 核心架构设计
### 2.1 系统架构
```
┌────────────────────┐ HTTP ┌────────────────────┐ IPC ┌────────────────────┐
│ 外部 AI 工具 │ ──────────> │ main.js (HTTP服务) │ ─────────> │ scene-script.js │
│ (Cursor/VS Code) │ <──────── │ (MCP 协议处理) │ <──────── │ (场景操作执行) │
└────────────────────┘ JSON └────────────────────┘ JSON └────────────────────┘
```
### 2.2 核心模块
1. **HTTP 服务模块**:处理外部请求,解析 MCP 协议
2. **MCP 工具模块**:实现各种操作工具
3. **场景操作模块**:执行场景相关操作
4. **资源管理模块**:处理脚本和资源文件
5. **面板界面模块**:提供用户交互界面
## 3. 功能模块实现
### 3.1 HTTP 服务实现
`main.js` 中实现 HTTP 服务:
```javascript
startServer(port) {
try {
const http = require('http');
mcpServer = http.createServer((req, res) => {
// 处理请求...
});
mcpServer.listen(port, () => {
addLog("success", `MCP Server running at http://127.0.0.1:${port}`);
});
} catch (e) {
addLog("error", `Failed to start server: ${e.message}`);
}
}
```
### 3.2 MCP 工具注册
`/list-tools` 接口中注册工具:
```javascript
const tools = [
{
name: "get_selected_node",
description: "获取当前选中的节点",
parameters: []
},
// 其他工具...
];
```
### 3.3 场景操作实现
`scene-script.js` 中实现场景相关操作:
```javascript
const sceneScript = {
'create-node'(params, callback) {
// 创建节点逻辑...
},
'set-property'(params, callback) {
// 设置属性逻辑...
},
// 其他操作...
};
```
### 3.4 脚本管理实现
`main.js` 中实现脚本管理功能:
```javascript
manageScript(args, callback) {
const { action, path, content } = args;
switch (action) {
case "create":
// 确保父目录存在
const fs = require('fs');
const pathModule = require('path');
const absolutePath = Editor.assetdb.urlToFspath(path);
const dirPath = pathModule.dirname(absolutePath);
if (!fs.existsSync(dirPath)) {
fs.mkdirSync(dirPath, { recursive: true });
}
// 创建 TypeScript 脚本
Editor.assetdb.create(path, content || `const { ccclass, property } = cc._decorator;
@ccclass
export default class NewScript extends cc.Component {
// LIFE-CYCLE CALLBACKS:
onLoad () {}
start () {}
update (dt) {}
}`, (err) => {
callback(err, err ? null : `Script created at ${path}`);
});
break;
// 其他操作...
}
}
```
### 3.5 批处理执行实现
`main.js` 中实现批处理功能:
```javascript
batchExecute(args, callback) {
const { operations } = args;
const results = [];
let completed = 0;
if (!operations || operations.length === 0) {
return callback("No operations provided");
}
operations.forEach((operation, index) => {
this.handleMcpCall(operation.tool, operation.params, (err, result) => {
results[index] = { tool: operation.tool, error: err, result: result };
completed++;
if (completed === operations.length) {
callback(null, results);
}
});
});
}
```
### 3.6 资产管理实现
`main.js` 中实现资产管理功能:
```javascript
manageAsset(args, callback) {
const { action, path, targetPath, content } = args;
switch (action) {
case "create":
// 确保父目录存在
const fs = require('fs');
const pathModule = require('path');
const absolutePath = Editor.assetdb.urlToFspath(path);
const dirPath = pathModule.dirname(absolutePath);
if (!fs.existsSync(dirPath)) {
fs.mkdirSync(dirPath, { recursive: true });
}
Editor.assetdb.create(path, content || '', (err) => {
callback(err, err ? null : `Asset created at ${path}`);
});
break;
// 其他操作...
}
}
```
### 3.7 面板界面实现
`panel/index.html` 中实现标签页界面:
```html
<div class="mcp-container">
<!-- 标签页 -->
<div class="tabs">
<ui-button id="tabMain" class="tab-button active">Main</ui-button>
<ui-button id="tabTest" class="tab-button">Tool Test</ui-button>
</div>
<!-- 主面板内容 -->
<div id="panelMain" class="tab-content active">
<!-- 主面板内容... -->
</div>
<!-- 测试面板内容 -->
<div id="panelTest" class="tab-content">
<div class="test-container">
<div class="test-layout">
<!-- 左侧工具列表 -->
<div class="left-panel">
<!-- 工具列表... -->
</div>
<!-- 右侧输入输出 -->
<div class="right-panel">
<!-- 输入输出区域... -->
</div>
</div>
</div>
</div>
</div>
```
## 4. 测试与调试
### 4.1 本地测试
1. **启动服务**:在面板中点击 "Start" 按钮
2. **测试工具**:在 "Tool Test" 标签页中测试各个工具
3. **查看日志**:在主面板中查看操作日志
### 4.2 常见错误及修复
#### 4.2.1 面板加载错误
**错误信息**`Panel info not found for panel mcp-bridge`
**解决方案**
- 检查 `package.json` 中的面板配置
- 确保 `panel` 字段配置正确,移除冲突的 `panels` 字段
#### 4.2.2 资源创建错误
**错误信息**`Parent path ... is not exists`
**解决方案**
- 在创建资源前添加目录检查和创建逻辑
- 使用 `fs.mkdirSync(dirPath, { recursive: true })` 递归创建目录
#### 4.2.3 脚本语法错误
**错误信息**`SyntaxError: Invalid or unexpected token`
**解决方案**
- 使用模板字符串(反引号)处理多行字符串
- 避免变量名冲突
### 4.3 性能优化
1. **批处理执行**:使用 `batch_execute` 工具减少 HTTP 请求次数
2. **异步操作**:使用回调函数处理异步操作,避免阻塞主线程
3. **错误处理**:完善错误处理机制,提高插件稳定性
## 5. 文档编写
### 5.1 README.md
- 项目简介
- 功能特性
- 安装使用
- API 文档
- 技术实现
### 5.2 API 文档
为每个 MCP 工具编写详细的 API 文档,包括:
- 工具名称
- 功能描述
- 参数说明
- 返回值格式
- 使用示例
### 5.3 开发文档
- 项目架构
- 开发流程
- 代码规范
- 贡献指南
## 6. 部署与使用
### 6.1 部署方式
1. **本地部署**:将插件复制到 Cocos Creator 项目的 `packages` 目录
2. **远程部署**:通过版本控制系统管理插件代码
### 6.2 使用流程
1. **启动服务**
- 打开 Cocos Creator 编辑器
- 选择 `Packages/MCP Bridge/Open Test Panel`
- 点击 "Start" 按钮启动服务
2. **连接 AI 编辑器**
- 在 AI 编辑器中配置 MCP 代理
- 使用 `node [项目路径]/packages/mcp-bridge/mcp-proxy.js` 作为命令
3. **执行操作**
- 通过 AI 编辑器发送 MCP 请求
- 或在测试面板中直接测试工具
### 6.3 配置选项
- **端口设置**:默认 3456可自定义
- **自动启动**:支持编辑器启动时自动开启服务
## 7. 功能扩展
### 7.1 添加新工具
1. **在 `main.js` 中注册工具**
-`/list-tools` 响应中添加工具定义
-`handleMcpCall` 函数中添加处理逻辑
2. **在面板中添加示例**
-`panel/index.js` 中添加工具示例参数
- 更新工具列表
3. **更新文档**
-`README.md` 中添加工具文档
- 更新功能特性列表
### 7.2 集成新 API
1. **了解 Cocos Creator API**
- 查阅 Cocos Creator 编辑器 API 文档
- 了解场景脚本 API
2. **实现集成**
-`main.js``scene-script.js` 中添加对应功能
- 处理异步操作和错误情况
3. **测试验证**
- 编写测试用例
- 验证功能正确性
## 8. 版本管理
### 8.1 版本控制
- 使用 Git 进行版本控制
- 遵循语义化版本规范
### 8.2 发布流程
1. **代码审查**:检查代码质量和功能完整性
2. **测试验证**:确保所有功能正常工作
3. **文档更新**:更新 README 和相关文档
4. **版本发布**:标记版本号并发布
## 9. 技术栈
- **JavaScript**:主要开发语言
- **Node.js**HTTP 服务和文件操作
- **Cocos Creator API**:编辑器功能集成
- **HTML/CSS**:面板界面
- **MCP 协议**:与 AI 工具通信
## 10. 最佳实践
1. **代码组织**
- 模块化设计,职责分离
- 合理使用回调函数处理异步操作
2. **错误处理**
- 完善的错误捕获和处理
- 详细的错误日志记录
3. **用户体验**
- 直观的面板界面
- 实时的操作反馈
- 详细的日志信息
4. **安全性**
- 验证输入参数
- 防止路径遍历攻击
- 限制服务访问范围
## 11. 总结
MCP Bridge 插件通过 HTTP 服务和 MCP 协议,为外部 AI 工具提供了与 Cocos Creator 编辑器交互的能力。插件支持场景操作、资源管理、组件管理、脚本管理等多种功能,为 Cocos Creator 项目的开发和自动化提供了有力的支持。
通过本文档的开发流程,我们构建了一个功能完整、稳定可靠的 MCP Bridge 插件,为 Cocos Creator 生态系统增添了新的工具和能力。

View File

@@ -11,6 +11,10 @@
- **HTTP 服务接口**: 提供标准 HTTP 接口,外部工具可以通过 MCP 协议调用 Cocos Creator 编辑器功能
- **场景节点操作**: 获取、创建、修改场景中的节点
- **资源管理**: 创建场景、预制体,打开指定资源
- **组件管理**: 添加、删除、获取节点组件
- **脚本管理**: 创建、删除、读取、写入脚本文件
- **批处理执行**: 批量执行多个 MCP 工具操作,提高效率
- **资产管理**: 创建、删除、移动、获取资源信息
- **实时日志**: 提供详细的操作日志记录和展示
- **自动启动**: 支持编辑器启动时自动开启服务
@@ -126,6 +130,43 @@ Args: [你的项目所在盘符]:/[项目路径]/packages/mcp-bridge/mcp-proxy.j
- `parentId`: 父节点 UUID (可选,不传则挂在场景根部)
- `type`: 节点预设类型(`empty`, `sprite`, `label`, `canvas`
### 10. manage_components
- **描述**: 管理节点组件
- **参数**:
- `nodeId`: 节点 UUID
- `action`: 操作类型(`add`, `remove`, `get`
- `componentType`: 组件类型,如 `cc.Sprite`(用于 `add` 操作)
- `componentId`: 组件 ID用于 `remove` 操作)
- `properties`: 组件属性(用于 `add` 操作)
### 11. manage_script
- **描述**: 管理脚本文件,默认创建 TypeScript 脚本
- **参数**:
- `action`: 操作类型(`create`, `delete`, `read`, `write`
- `path`: 脚本路径,如 `db://assets/scripts/NewScript.ts`
- `content`: 脚本内容(用于 `create``write` 操作)
- `name`: 脚本名称(用于 `create` 操作)
- **默认模板**: 当未提供 content 时,会使用 TypeScript 格式的默认模板
### 12. batch_execute
- **描述**: 批处理执行多个操作
- **参数**:
- `operations`: 操作列表
- `tool`: 工具名称
- `params`: 工具参数
### 13. manage_asset
- **描述**: 管理资源
- **参数**:
- `action`: 操作类型(`create`, `delete`, `move`, `get_info`
- `path`: 资源路径,如 `db://assets/textures`
- `targetPath`: 目标路径(用于 `move` 操作)
- `content`: 资源内容(用于 `create` 操作)
## 技术实现
### 架构设计

252
main.js
View File

@@ -133,7 +133,7 @@ const getToolsList = () => {
},
{
name: "open_scene",
description: "在编辑器中打开指定的场景文件",
description: "打开场景文件。注意:这是一个异步且耗时的操作,打开后请等待几秒再进行节点创建或保存操作。",
inputSchema: {
type: "object",
properties: {
@@ -165,8 +165,74 @@ const getToolsList = () => {
required: ["name"],
},
},
{
name: "manage_components",
description: "管理节点组件",
inputSchema: {
type: "object",
properties: {
nodeId: { type: "string", description: "节点 UUID" },
action: { type: "string", enum: ["add", "remove", "get"], description: "操作类型" },
componentType: { type: "string", description: "组件类型,如 cc.Sprite" },
componentId: { type: "string", description: "组件 ID (用于 remove 操作)" },
properties: { type: "object", description: "组件属性 (用于 add 操作)" },
},
required: ["nodeId", "action"],
},
},
{
name: "manage_script",
description: "管理脚本文件",
inputSchema: {
type: "object",
properties: {
action: { type: "string", enum: ["create", "delete", "read", "write"], description: "操作类型" },
path: { type: "string", description: "脚本路径,如 db://assets/scripts/NewScript.js" },
content: { type: "string", description: "脚本内容 (用于 create 和 write 操作)" },
name: { type: "string", description: "脚本名称 (用于 create 操作)" },
},
required: ["action", "path"],
},
},
{
name: "batch_execute",
description: "批处理执行多个操作",
inputSchema: {
type: "object",
properties: {
operations: {
type: "array",
items: {
type: "object",
properties: {
tool: { type: "string", description: "工具名称" },
params: { type: "object", description: "工具参数" },
},
required: ["tool", "params"],
},
description: "操作列表",
},
},
required: ["operations"],
},
},
{
name: "manage_asset",
description: "管理资源",
inputSchema: {
type: "object",
properties: {
action: { type: "string", enum: ["create", "delete", "move", "get_info"], description: "操作类型" },
path: { type: "string", description: "资源路径,如 db://assets/textures" },
targetPath: { type: "string", description: "目标路径 (用于 move 操作)" },
content: { type: "string", description: "资源内容 (用于 create 操作)" },
},
required: ["action", "path"],
},
},
];
};
let isSceneBusy = false;
module.exports = {
"scene-script": "scene-script.js",
@@ -279,6 +345,9 @@ module.exports = {
// 统一处理逻辑,方便日志记录
handleMcpCall(name, args, callback) {
if (isSceneBusy && (name === "save_scene" || name === "create_node")) {
return callback("Editor is busy (Processing Scene), please wait a moment.");
}
switch (name) {
case "get_selected_node":
const ids = Editor.Selection.curSelection("node");
@@ -299,8 +368,18 @@ module.exports = {
break;
case "save_scene":
isSceneBusy = true;
addLog("info", "Preparing to save scene... Waiting for UI sync.");
// 强制延迟保存,防止死锁
setTimeout(() => {
Editor.Ipc.sendToMain("scene:save-scene");
callback(null, "Scene saved successfully");
addLog("info", "Executing Safe Save...");
setTimeout(() => {
isSceneBusy = false;
addLog("info", "Safe Save completed.");
callback(null, "Scene saved successfully.");
}, 1000);
}, 500);
break;
case "get_scene_hierarchy":
@@ -328,11 +407,16 @@ module.exports = {
break;
case "open_scene":
isSceneBusy = true; // 锁定
const openUuid = Editor.assetdb.urlToUuid(args.url);
if (openUuid) {
Editor.Ipc.sendToMain("scene:open-by-uuid", openUuid);
setTimeout(() => {
isSceneBusy = false;
callback(null, `Success: Opening scene ${args.url}`);
}, 2000);
} else {
isSceneBusy = false;
callback(`Could not find asset with URL ${args.url}`);
}
break;
@@ -341,11 +425,175 @@ module.exports = {
Editor.Scene.callSceneScript("mcp-bridge", "create-node", args, callback);
break;
case "manage_components":
Editor.Scene.callSceneScript("mcp-bridge", "manage-components", args, callback);
break;
case "manage_script":
this.manageScript(args, callback);
break;
case "batch_execute":
this.batchExecute(args, callback);
break;
case "manage_asset":
this.manageAsset(args, callback);
break;
default:
callback(`Unknown tool: ${name}`);
break;
}
},
// 管理脚本文件
manageScript(args, callback) {
const { action, path, content } = args;
switch (action) {
case "create":
if (Editor.assetdb.exists(path)) {
return callback(`Script already exists at ${path}`);
}
// 确保父目录存在
const fs = require('fs');
const pathModule = require('path');
const absolutePath = Editor.assetdb.urlToFspath(path);
const dirPath = pathModule.dirname(absolutePath);
if (!fs.existsSync(dirPath)) {
fs.mkdirSync(dirPath, { recursive: true });
}
Editor.assetdb.create(path, content || `const { ccclass, property } = cc._decorator;
@ccclass
export default class NewScript extends cc.Component {
@property(cc.Label)
label: cc.Label = null;
@property
text: string = 'hello';
// LIFE-CYCLE CALLBACKS:
onLoad () {}
start () {}
update (dt) {}
}`, (err) => {
callback(err, err ? null : `Script created at ${path}`);
});
break;
case "delete":
if (!Editor.assetdb.exists(path)) {
return callback(`Script not found at ${path}`);
}
Editor.assetdb.delete([path], (err) => {
callback(err, err ? null : `Script deleted at ${path}`);
});
break;
case "read":
Editor.assetdb.queryInfoByUuid(Editor.assetdb.urlToUuid(path), (err, info) => {
if (err) {
return callback(`Failed to get script info: ${err}`);
}
Editor.assetdb.loadAny(path, (err, content) => {
callback(err, err ? null : content);
});
});
break;
case "write":
Editor.assetdb.create(path, content, (err) => {
callback(err, err ? null : `Script updated at ${path}`);
});
break;
default:
callback(`Unknown script action: ${action}`);
break;
}
},
// 批处理执行
batchExecute(args, callback) {
const { operations } = args;
const results = [];
let completed = 0;
if (!operations || operations.length === 0) {
return callback("No operations provided");
}
operations.forEach((operation, index) => {
this.handleMcpCall(operation.tool, operation.params, (err, result) => {
results[index] = { tool: operation.tool, error: err, result: result };
completed++;
if (completed === operations.length) {
callback(null, results);
}
});
});
},
// 管理资源
manageAsset(args, callback) {
const { action, path, targetPath, content } = args;
switch (action) {
case "create":
if (Editor.assetdb.exists(path)) {
return callback(`Asset already exists at ${path}`);
}
// 确保父目录存在
const fs = require('fs');
const pathModule = require('path');
const absolutePath = Editor.assetdb.urlToFspath(path);
const dirPath = pathModule.dirname(absolutePath);
if (!fs.existsSync(dirPath)) {
fs.mkdirSync(dirPath, { recursive: true });
}
Editor.assetdb.create(path, content || '', (err) => {
callback(err, err ? null : `Asset created at ${path}`);
});
break;
case "delete":
if (!Editor.assetdb.exists(path)) {
return callback(`Asset not found at ${path}`);
}
Editor.assetdb.delete([path], (err) => {
callback(err, err ? null : `Asset deleted at ${path}`);
});
break;
case "move":
if (!Editor.assetdb.exists(path)) {
return callback(`Asset not found at ${path}`);
}
if (Editor.assetdb.exists(targetPath)) {
return callback(`Target asset already exists at ${targetPath}`);
}
Editor.assetdb.move(path, targetPath, (err) => {
callback(err, err ? null : `Asset moved from ${path} to ${targetPath}`);
});
break;
case "get_info":
Editor.assetdb.queryInfoByUuid(Editor.assetdb.urlToUuid(path), (err, info) => {
callback(err, err ? null : info);
});
break;
default:
callback(`Unknown asset action: ${action}`);
break;
}
},
// 暴露给 MCP 或面板的 API 封装
messages: {
"open-test-panel"() {

View File

@@ -1,4 +1,12 @@
<div class="mcp-container">
<!-- 标签页 -->
<div class="tabs">
<ui-button id="tabMain" class="tab-button active">Main</ui-button>
<ui-button id="tabTest" class="tab-button">Tool Test</ui-button>
</div>
<!-- 主面板内容 -->
<div id="panelMain" class="tab-content active">
<div class="toolbar">
<div class="ctrl-group">
<span>Port:</span>
@@ -18,6 +26,46 @@
<!-- 日志区域 -->
<div id="logConsole" class="log-view"></div>
</div>
<!-- 测试面板内容 -->
<div id="panelTest" class="tab-content">
<div class="test-container">
<div class="test-layout">
<!-- 左侧工具列表 -->
<div class="left-panel">
<div class="tool-name-section">
<label for="toolName">工具名称:</label>
<ui-input id="toolName" placeholder="例如: manage_components"></ui-input>
</div>
<div class="tools-list" id="toolsList"></div>
</div>
<!-- 右侧输入输出 -->
<div class="right-panel">
<div class="form-group">
<label for="toolParams">工具参数 (JSON格式):</label>
<textarea id="toolParams" placeholder="例如: {
\"nodeId\": \"节点UUID\",
\"action\": \"add\",
\"componentType\": \"cc.Button\"
}"></textarea>
</div>
<div class="button-group">
<ui-button id="testBtn" class="primary">测试工具</ui-button>
<ui-button id="listToolsBtn" class="secondary">获取工具列表</ui-button>
<ui-button id="clearBtn" class="secondary">清空结果</ui-button>
</div>
<div class="result">
<h2>测试结果:</h2>
<textarea id="resultContent" placeholder="点击\"测试工具\"按钮开始测试"></textarea>
</div>
</div>
</div>
</div>
</div>
</div>
<style>
@@ -34,6 +82,38 @@
padding: 5px;
box-sizing: border-box;
}
/* 标签页样式 */
.tabs {
display: flex;
border-bottom: 1px solid #444;
margin-bottom: 10px;
flex-shrink: 0;
}
.tab-button {
padding: 8px 16px;
margin-right: 2px;
border: none;
background-color: #333;
color: #ccc;
cursor: pointer;
border-bottom: 2px solid transparent;
}
.tab-button.active {
background-color: #2d2d2d;
color: #fff;
border-bottom-color: #4CAF50;
}
.tab-content {
display: none;
flex: 1;
flex-direction: column;
}
.tab-content.active {
display: flex;
}
/* 主面板样式 */
.toolbar {
display: flex;
align-items: center;
@@ -102,4 +182,178 @@
border-left-color: #c678dd;
color: #d19a66;
}
/* 测试面板样式 */
.test-container {
display: flex;
flex-direction: column;
height: 100%;
padding: 10px;
box-sizing: border-box;
}
.test-layout {
display: flex;
height: 100%;
gap: 15px;
}
.left-panel {
width: 300px;
flex-shrink: 0;
display: flex;
flex-direction: column;
height: 100%;
background-color: #333;
border-radius: 4px;
padding: 10px;
}
.right-panel {
flex: 1;
display: flex;
flex-direction: column;
gap: 15px;
height: 100%;
min-height: 0;
}
.left-panel .tool-name-section {
margin-bottom: 10px;
}
.left-panel .tools-list {
flex: 1;
border: 1px solid #444;
border-radius: 4px;
padding: 5px;
background-color: #222;
overflow-y: auto;
min-height: 0;
}
.left-panel label {
display: block;
margin-bottom: 5px;
color: #ccc;
font-size: 12px;
}
.left-panel ui-input {
width: 100%;
padding: 8px;
border: 1px solid #444;
border-radius: 4px;
font-size: 12px;
box-sizing: border-box;
background-color: #222;
color: #fff;
margin-bottom: 10px;
}
.right-panel .form-group {
margin-bottom: 0;
}
.right-panel .result {
flex: 1;
display: flex;
flex-direction: column;
min-height: 0;
margin-bottom: 0;
}
.right-panel .result textarea {
flex: 1;
min-height: 100px;
}
.right-panel .button-group {
flex-shrink: 0;
}
.form-group {
margin-bottom: 15px;
background-color: #333;
padding: 10px;
border-radius: 4px;
}
.form-group label {
display: block;
margin-bottom: 5px;
color: #ccc;
font-size: 12px;
}
.form-group ui-input,
.form-group textarea {
width: 100%;
padding: 8px;
border: 1px solid #444;
border-radius: 4px;
font-size: 12px;
box-sizing: border-box;
background-color: #222;
color: #fff;
}
.form-group textarea {
height: 150px;
resize: vertical;
font-family: 'Courier New', monospace;
}
.tool-item {
padding: 5px;
margin-bottom: 5px;
background-color: #333;
border-radius: 3px;
cursor: pointer;
font-size: 12px;
color: #ccc;
}
.tool-item:hover {
background-color: #444;
}
.button-group {
display: flex;
gap: 10px;
margin-bottom: 15px;
}
.button-group ui-button {
padding: 8px 16px;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 12px;
font-weight: bold;
}
.button-group ui-button.primary {
background-color: #4CAF50;
color: white;
}
.button-group ui-button.secondary {
background-color: #2196F3;
color: white;
}
.result {
flex: 1;
background-color: #333;
padding: 15px;
border-radius: 4px;
}
.result h2 {
color: #ccc;
font-size: 14px;
margin-bottom: 10px;
}
.result textarea {
width: 100%;
height: 100%;
background-color: #222;
color: #fff;
border: 1px solid #444;
border-radius: 4px;
padding: 10px;
font-family: 'Courier New', monospace;
font-size: 12px;
box-sizing: border-box;
resize: vertical;
}
</style>

View File

@@ -23,6 +23,24 @@ Editor.Panel.extend({
const btnCopy = this.shadowRoot.querySelector("#btnCopy");
const logView = this.shadowRoot.querySelector("#logConsole");
// 标签页元素
const tabMain = this.shadowRoot.querySelector("#tabMain");
const tabTest = this.shadowRoot.querySelector("#tabTest");
const panelMain = this.shadowRoot.querySelector("#panelMain");
const panelTest = this.shadowRoot.querySelector("#panelTest");
// 测试面板元素
const toolNameInput = this.shadowRoot.querySelector("#toolName");
const toolParamsTextarea = this.shadowRoot.querySelector("#toolParams");
const toolsList = this.shadowRoot.querySelector("#toolsList");
const testBtn = this.shadowRoot.querySelector("#testBtn");
const listToolsBtn = this.shadowRoot.querySelector("#listToolsBtn");
const clearBtn = this.shadowRoot.querySelector("#clearBtn");
const resultContent = this.shadowRoot.querySelector("#resultContent");
let tools = [];
const API_BASE = 'http://localhost:3456';
// 初始化
Editor.Ipc.sendToMain("mcp-bridge:get-server-state", (err, data) => {
if (data) {
@@ -32,6 +50,23 @@ Editor.Panel.extend({
}
});
// 标签页切换
tabMain.addEventListener("confirm", () => {
tabMain.classList.add("active");
tabTest.classList.remove("active");
panelMain.classList.add("active");
panelTest.classList.remove("active");
});
tabTest.addEventListener("confirm", () => {
tabTest.classList.add("active");
tabMain.classList.remove("active");
panelTest.classList.add("active");
panelMain.classList.remove("active");
// 自动获取工具列表
this.getToolsList();
});
btnToggle.addEventListener("confirm", () => {
Editor.Ipc.sendToMain("mcp-bridge:toggle-server", parseInt(portInput.value));
});
@@ -45,6 +80,7 @@ Editor.Panel.extend({
require("electron").clipboard.writeText(logView.innerText);
Editor.success("All logs copied!");
});
Editor.Ipc.sendToMain("mcp-bridge:get-server-state", (err, data) => {
if (data) {
portInput.value = data.config.port;
@@ -56,10 +92,218 @@ Editor.Panel.extend({
data.logs.forEach((log) => this.renderLog(log));
}
});
autoStartCheck.addEventListener("change", (event) => {
// event.target.value 在 ui-checkbox 中是布尔值
Editor.Ipc.sendToMain("mcp-bridge:set-auto-start", event.target.value);
});
// 测试面板事件
testBtn.addEventListener("confirm", () => this.testTool());
listToolsBtn.addEventListener("confirm", () => this.getToolsList());
clearBtn.addEventListener("confirm", () => this.clearResult());
// 获取工具列表
this.getToolsList = function() {
this.showResult('获取工具列表中...');
fetch(`${API_BASE}/list-tools`)
.then(response => {
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return response.json();
})
.then(data => {
if (data.tools) {
tools = data.tools;
this.displayToolsList(tools);
this.showResult(`成功获取 ${tools.length} 个工具`, 'success');
} else {
this.showResult('获取工具列表失败:未找到工具数据', 'error');
}
})
.catch(error => {
this.showResult(`获取工具列表失败:${error.message}`, 'error');
});
};
// 显示工具列表
this.displayToolsList = function(tools) {
toolsList.innerHTML = '';
tools.forEach(tool => {
const toolItem = document.createElement('div');
toolItem.className = 'tool-item';
toolItem.textContent = `${tool.name} - ${tool.description}`;
toolItem.addEventListener('click', () => {
toolNameInput.value = tool.name;
// 尝试填充示例参数
this.fillExampleParams(tool);
});
toolsList.appendChild(toolItem);
});
};
// 填充示例参数
this.fillExampleParams = function(tool) {
let exampleParams = {};
switch (tool.name) {
case 'get_selected_node':
case 'save_scene':
case 'get_scene_hierarchy':
exampleParams = {};
break;
case 'set_node_name':
exampleParams = {
"id": "节点UUID",
"newName": "新节点名称"
};
break;
case 'update_node_transform':
exampleParams = {
"id": "节点UUID",
"x": 100,
"y": 100,
"scaleX": 1,
"scaleY": 1
};
break;
case 'create_scene':
exampleParams = {
"sceneName": "NewScene"
};
break;
case 'create_prefab':
exampleParams = {
"nodeId": "节点UUID",
"prefabName": "NewPrefab"
};
break;
case 'open_scene':
exampleParams = {
"url": "db://assets/NewScene.fire"
};
break;
case 'create_node':
exampleParams = {
"name": "NewNode",
"parentId": "父节点UUID",
"type": "empty"
};
break;
case 'manage_components':
exampleParams = {
"nodeId": "节点UUID",
"action": "add",
"componentType": "cc.Button"
};
break;
case 'manage_script':
exampleParams = {
"action": "create",
"path": "db://assets/scripts/TestScript.ts",
"content": "const { ccclass, property } = cc._decorator;\n\n@ccclass\nexport default class TestScript extends cc.Component {\n // LIFE-CYCLE CALLBACKS:\n\n onLoad () {}\n\n start () {}\n\n update (dt) {}\n}"
};
break;
case 'batch_execute':
exampleParams = {
"operations": [
{
"tool": "get_selected_node",
"params": {}
}
]
};
break;
case 'manage_asset':
exampleParams = {
"action": "create",
"path": "db://assets/test.txt",
"content": "Hello, MCP!"
};
break;
}
toolParamsTextarea.value = JSON.stringify(exampleParams, null, 2);
};
// 测试工具
this.testTool = function() {
const toolName = toolNameInput.value.trim();
const toolParamsStr = toolParamsTextarea.value.trim();
if (!toolName) {
this.showResult('请输入工具名称', 'error');
return;
}
let toolParams;
try {
toolParams = toolParamsStr ? JSON.parse(toolParamsStr) : {};
} catch (error) {
this.showResult(`参数格式错误:${error.message}`, 'error');
return;
}
this.showResult('测试工具中...');
fetch(`${API_BASE}/call-tool`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
name: toolName,
arguments: toolParams
})
})
.then(response => {
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return response.json();
})
.then(data => {
if (data.error) {
this.showResult(`测试失败:${data.error}`, 'error');
} else {
this.showResult(JSON.stringify(data, null, 2), 'success');
}
})
.catch(error => {
this.showResult(`测试失败:${error.message}`, 'error');
});
};
// 显示结果
this.showResult = function(message, type = 'info') {
resultContent.value = message;
// 移除旧样式
resultContent.className = '';
// 添加新样式
if (type === 'error' || type === 'success') {
resultContent.className = type;
}
};
// 清空结果
this.clearResult = function() {
this.showResult('点击"测试工具"按钮开始测试');
};
},
renderLog(log) {

View File

@@ -91,6 +91,10 @@ module.exports = {
"create-node": function (event, args) {
const { name, parentId, type } = args;
const scene = cc.director.getScene();
if (!scene || !cc.director.getRunningScene()) {
if (event.reply) event.reply(new Error("Scene not ready or loading."));
return;
}
let newNode = null;
@@ -119,24 +123,149 @@ module.exports = {
// 设置层级
let parent = parentId ? cc.engine.getInstanceById(parentId) : scene;
if (newNode) {
if (parent) {
newNode.parent = parent;
// 坐标居中处理(如果是 Canvas 子节点)
if (parent.name === "Canvas") {
newNode.setPosition(0, 0);
} else {
newNode.setPosition(cc.v2(cc.winSize.width / 2, cc.winSize.height / 2));
}
// 通知编辑器刷新
// 【优化】通知主进程场景变脏
Editor.Ipc.sendToMain("scene:dirty");
// 【关键】使用 setTimeout 延迟通知 UI 刷新,让出主循环
setTimeout(() => {
Editor.Ipc.sendToAll("scene:node-created", {
uuid: newNode.uuid,
parentUuid: parent.uuid,
});
}, 10);
if (event.reply) event.reply(null, newNode.uuid);
}
},
"manage-components": function (event, args) {
const { nodeId, action, componentType, componentId, properties } = args;
let node = cc.engine.getInstanceById(nodeId);
if (!node) {
if (event.reply) event.reply(new Error("Node not found"));
return;
}
switch (action) {
case "add":
if (!componentType) {
if (event.reply) event.reply(new Error("Component type is required"));
return;
}
try {
// 解析组件类型
let compClass = null;
if (componentType.startsWith("cc.")) {
const className = componentType.replace("cc.", "");
compClass = cc[className];
} else {
// 尝试获取自定义组件
compClass = cc.js.getClassByName(componentType);
}
if (!compClass) {
if (event.reply) event.reply(new Error(`Component type not found: ${componentType}`));
return;
}
// 添加组件
const component = node.addComponent(compClass);
// 设置属性
if (properties) {
for (const [key, value] of Object.entries(properties)) {
if (component[key] !== undefined) {
component[key] = value;
}
}
}
Editor.Ipc.sendToMain("scene:dirty");
Editor.Ipc.sendToAll("scene:node-changed", { uuid: nodeId });
if (event.reply) event.reply(null, `Component ${componentType} added`);
} catch (err) {
if (event.reply) event.reply(new Error(`Failed to add component: ${err.message}`));
}
break;
case "remove":
if (!componentId) {
if (event.reply) event.reply(new Error("Component ID is required"));
return;
}
try {
// 查找并移除组件
const component = node.getComponentById(componentId);
if (component) {
node.removeComponent(component);
Editor.Ipc.sendToMain("scene:dirty");
Editor.Ipc.sendToAll("scene:node-changed", { uuid: nodeId });
if (event.reply) event.reply(null, "Component removed");
} else {
if (event.reply) event.reply(new Error("Component not found"));
}
} catch (err) {
if (event.reply) event.reply(new Error(`Failed to remove component: ${err.message}`));
}
break;
case "get":
try {
const components = node._components.map((c) => {
// 获取组件属性
const properties = {};
for (const key in c) {
if (typeof c[key] !== "function" &&
!key.startsWith("_") &&
c[key] !== undefined) {
try {
properties[key] = c[key];
} catch (e) {
// 忽略无法序列化的属性
}
}
}
return {
type: c.__typename,
uuid: c.uuid,
properties: properties
};
});
if (event.reply) event.reply(null, components);
} catch (err) {
if (event.reply) event.reply(new Error(`Failed to get components: ${err.message}`));
}
break;
default:
if (event.reply) event.reply(new Error(`Unknown component action: ${action}`));
break;
}
},
"get-component-properties": function (component) {
const properties = {};
// 遍历组件属性
for (const key in component) {
if (typeof component[key] !== "function" &&
!key.startsWith("_") &&
component[key] !== undefined) {
try {
properties[key] = component[key];
} catch (e) {
// 忽略无法序列化的属性
}
}
}
return properties;
},
};