From be53f3c56c6396841e280243f6f866b0ce627a48 Mon Sep 17 00:00:00 2001 From: winlifes <110321103+Winlifes@users.noreply.github.com> Date: Thu, 16 Apr 2026 17:45:35 +0800 Subject: [PATCH] Improve MCP startup flow and repo automation --- .github/pull_request_template.md | 14 ++++ .github/workflows/ci.yml | 74 +++++++++++++++++++++ README.md | 3 + README_CN.md | 3 + browser.js | 44 +++++++++++-- lib/server.js | 106 +++++++++++++++++++++++++++---- panel/index.js | 6 +- 7 files changed, 234 insertions(+), 16 deletions(-) create mode 100644 .github/pull_request_template.md create mode 100644 .github/workflows/ci.yml diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 0000000..9756015 --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,14 @@ +## What changed + +- Describe the change briefly +- Explain the user impact or motivation + +## Checklist + +- [ ] I tested the extension in a clean Cocos Creator 3.8+ project +- [ ] I verified `Funplay > MCP Server` opens and the MCP server can start correctly +- [ ] If I changed setup, one-click config, or port/config behavior, I verified the affected flow end-to-end +- [ ] I ran `npm run check` +- [ ] I updated docs for any user-facing behavior changes +- [ ] I did not commit local junk such as `.DS_Store`, `temp/`, or `library/` +- [ ] I updated `CHANGELOG.md` when the change affects users diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..bc2fab5 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,74 @@ +name: CI + +on: + pull_request: + push: + branches: + - main + workflow_dispatch: + +jobs: + validate: + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + + - name: Validate repository metadata + run: | + python - <<'PY' + import json + import pathlib + import sys + + root = pathlib.Path('.') + errors = [] + + package_path = root / 'package.json' + try: + package = json.loads(package_path.read_text(encoding='utf-8')) + except Exception as exc: + errors.append(f"package.json is not valid JSON: {exc}") + else: + for key in ('name', 'version', 'main', 'package_version'): + if key not in package or package[key] in (None, ''): + errors.append(f"package.json is missing required key: {key}") + + required_docs = [ + 'README.md', + 'README_CN.md', + 'LICENSE', + 'CHANGELOG.md', + 'CONTRIBUTING.md', + ] + for relative in required_docs: + if not (root / relative).exists(): + errors.append(f"Missing required repository file: {relative}") + + forbidden_paths = [] + for path in root.rglob('*'): + if '.git' in path.parts: + continue + if path.name == '.DS_Store' or '.idea' in path.parts or 'library' in path.parts or 'Library' in path.parts: + forbidden_paths.append(str(path)) + + if forbidden_paths: + errors.append('Repository contains local junk files:\n- ' + '\n- '.join(sorted(forbidden_paths))) + + if errors: + print('Validation failed:\n') + for error in errors: + print(f'- {error}') + sys.exit(1) + + print('Repository validation passed.') + PY + + - name: Run syntax checks + run: npm run check diff --git a/README.md b/README.md index 282b786..7e69afe 100644 --- a/README.md +++ b/README.md @@ -63,6 +63,8 @@ Funplay > MCP Server The server runs on `http://127.0.0.1:8765/` by default. +If the configured port is already occupied, the extension automatically falls back to the next available local port and uses the actual running port for one-click MCP client configuration. + The panel is intentionally small: - Enable or disable the MCP server @@ -194,6 +196,7 @@ Try a higher-level prompt in your AI client: - This extension is **Editor-only**. It is meant to automate Cocos Creator, not to add runtime dependencies to your final game build. - The MCP server listens on `http://127.0.0.1:8765/` by default. +- If the configured port is busy, the server automatically falls back to the next available port and the panel/client config use the actual running port. - The default `core` profile exposes 19 high-signal tools. Switch to `full` in the panel if you want all 67 tools exposed. - All exposed MCP tools execute directly. There is no extra approval toggle inside the Cocos extension. - The recommended workflow is `execute_javascript` first, then focused helper tools for screenshots, diagnostics, assets, and inspection. diff --git a/README_CN.md b/README_CN.md index 2ea681a..fb97db4 100644 --- a/README_CN.md +++ b/README_CN.md @@ -63,6 +63,8 @@ Funplay > MCP Server 服务默认运行在 `http://127.0.0.1:8765/`。 +如果配置端口已被占用,扩展会自动回退到下一个可用本地端口,并在一键客户端配置时使用实际运行端口。 + 面板刻意保持精简: - 启用或停用 MCP Server @@ -194,6 +196,7 @@ url = "http://127.0.0.1:8765/" - 这是一个 **仅限 Editor** 的扩展,用于自动化 Cocos Creator,不会给最终游戏包添加运行时依赖。 - MCP Server 默认监听 `http://127.0.0.1:8765/`。 +- 如果配置端口被占用,服务会自动回退到下一个可用端口,面板与一键客户端配置会使用实际运行端口。 - 默认 `core` profile 暴露 19 个高频工具;如果需要完整工具集,可在面板切到 `full`,暴露全部 67 个工具。 - 所有已暴露的 MCP 工具都会直接执行,Cocos 扩展里没有额外 approval 开关。 - 推荐工作流是优先使用 `execute_javascript`,再配合截图、诊断、资产、检查类工具。 diff --git a/browser.js b/browser.js index 8fcdb15..fbf3dca 100644 --- a/browser.js +++ b/browser.js @@ -14,6 +14,7 @@ const { InteractionLog } = require('./lib/interaction-log'); const EXTENSION_NAME = manifest.name || 'funplay-cocos-mcp'; const LOG_PREFIX = '[Funplay Cocos MCP]'; +const REPOSITORY_URL = 'https://github.com/FunplayAI/funplay-cocos-mcp'; const AsyncFunction = Object.getPrototypeOf(async function () {}).constructor; class ExtensionService { @@ -114,6 +115,9 @@ class ExtensionService { await this.server.start(); console.log(`${LOG_PREFIX} MCP server started at ${this.getStatus().url}`); + console.log( + `${LOG_PREFIX} If this tool saves you time, please consider giving it a Star on GitHub: ${REPOSITORY_URL}` + ); return this.getStatus(); } @@ -137,17 +141,35 @@ class ExtensionService { return status; } + getEffectiveServerConnection() { + const port = this.server && this.server.isRunning() && typeof this.server.getPort === 'function' + ? this.server.getPort() + : this.config.port; + return { + host: this.config.host, + port, + url: `http://${this.config.host}:${port}/`, + }; + } + getStatus() { + const effective = this.getEffectiveServerConnection(); + const fallbackInfo = this.server && this.server.isRunning() && typeof this.server.getPortFallbackInfo === 'function' + ? this.server.getPortFallbackInfo() + : null; return { running: Boolean(this.server && this.server.isRunning()), host: this.config.host, - port: this.config.port, + port: effective.port, + requestedPort: this.config.port, + portFallbackActive: Boolean(fallbackInfo), + portFallbackInfo: fallbackInfo, toolProfile: this.config.toolProfile, autostart: this.config.autostart, projectPath: getProjectPath(), projectName: getProjectName(), cocosVersion: getCocosVersion(), - url: `http://${this.config.host}:${this.config.port}/`, + url: effective.url, }; } @@ -234,7 +256,7 @@ class ExtensionService { } getClientConfig() { - const url = `http://${this.config.host}:${this.config.port}/`; + const { url } = this.getEffectiveServerConnection(); return { url, codex: `[mcp_servers.funplay_cocos]\nurl = "${url}"\n`, @@ -251,7 +273,21 @@ class ExtensionService { configureClient(targetId) { this.ensureRuntime(); console.log(`${LOG_PREFIX} Configuring MCP client target: ${targetId}`); - const result = configureTarget(this.config, targetId); + const effective = this.getEffectiveServerConnection(); + if (effective.port !== this.config.port) { + console.log( + `${LOG_PREFIX} Using actual running port ${effective.port} for MCP client configuration ` + + `(requested: ${this.config.port}).` + ); + } + const result = configureTarget( + { + ...this.config, + host: effective.host, + port: effective.port, + }, + targetId + ); console.log(`${LOG_PREFIX} MCP client configured: ${result.name} -> ${result.configPath}`); return { ...result, diff --git a/lib/server.js b/lib/server.js index 8cb834f..c4127ce 100644 --- a/lib/server.js +++ b/lib/server.js @@ -3,6 +3,7 @@ const http = require('http'); const IMAGE_DATA_URI_PREFIX = 'data:image/png;base64,'; const LOG_PREFIX = '[Funplay Cocos MCP Server]'; +const MAX_PORT_FALLBACK_ATTEMPTS = 20; function json(response, statusCode, payload) { response.writeHead(statusCode, { 'Content-Type': 'application/json; charset=utf-8' }); @@ -42,20 +43,42 @@ class McpServer { this.serverName = options.serverName; this.serverVersion = options.serverVersion; this.server = null; + this.actualPort = null; + this.portFallbackInfo = null; } isRunning() { return Boolean(this.server && this.server.listening); } + getPort() { + if (this.server && typeof this.server.address === 'function') { + const address = this.server.address(); + if (address && typeof address.port === 'number') { + return address.port; + } + } + return this.actualPort || this.config.port; + } + + getRequestedPort() { + return this.config.port; + } + + getPortFallbackInfo() { + return this.portFallbackInfo; + } + async start() { if (this.isRunning()) { console.log(`${LOG_PREFIX} Start skipped: already running.`); return; } - console.log(`${LOG_PREFIX} Creating HTTP server on ${this.config.host}:${this.config.port}...`); - this.server = http.createServer(async (request, response) => { + this.actualPort = null; + this.portFallbackInfo = null; + + const requestHandler = async (request, response) => { try { if (request.method === 'GET' && request.url === '/health') { console.log(`${LOG_PREFIX} GET /health`); @@ -88,16 +111,59 @@ class McpServer { console.error(`${LOG_PREFIX} Request handling failed: ${error.message}`); return json(response, 500, this.createError(null, -32603, `Internal error: ${error.message}`)); } - }); + }; - await new Promise((resolve, reject) => { - this.server.once('error', reject); - this.server.listen(this.config.port, this.config.host, () => { - this.server.off('error', reject); - console.log(`${LOG_PREFIX} Listening on http://${this.config.host}:${this.config.port}/`); - resolve(); - }); - }); + let attempt = 0; + let port = this.config.port; + let lastError = null; + + while (attempt <= MAX_PORT_FALLBACK_ATTEMPTS) { + console.log(`${LOG_PREFIX} Creating HTTP server on ${this.config.host}:${port}...`); + const candidate = http.createServer(requestHandler); + + try { + await this.listen(candidate, port, this.config.host); + this.server = candidate; + this.actualPort = candidate.address() && typeof candidate.address().port === 'number' + ? candidate.address().port + : port; + + if (this.actualPort !== this.config.port) { + this.portFallbackInfo = { + requestedPort: this.config.port, + actualPort: this.actualPort, + attempts: attempt, + }; + console.warn( + `${LOG_PREFIX} Port ${this.config.port} was unavailable. ` + + `Fell back to ${this.actualPort}.` + ); + } + + console.log(`${LOG_PREFIX} Listening on http://${this.config.host}:${this.actualPort}/`); + return; + } catch (error) { + lastError = error; + if (error && error.code === 'EADDRINUSE' && port < 65535 && attempt < MAX_PORT_FALLBACK_ATTEMPTS) { + const nextPort = port + 1; + console.warn( + `${LOG_PREFIX} Port ${port} is already in use. ` + + `Trying fallback port ${nextPort}...` + ); + port = nextPort; + attempt += 1; + continue; + } + + candidate.removeAllListeners(); + break; + } + } + + this.server = null; + this.actualPort = null; + this.portFallbackInfo = null; + throw lastError || new Error('Failed to start MCP server.'); } async stop() { @@ -109,6 +175,8 @@ class McpServer { console.log(`${LOG_PREFIX} Closing HTTP server...`); const active = this.server; this.server = null; + this.actualPort = null; + this.portFallbackInfo = null; await new Promise((resolve, reject) => { active.close((error) => { if (error) { @@ -122,6 +190,22 @@ class McpServer { }); } + listen(server, port, host) { + return new Promise((resolve, reject) => { + const onError = (error) => { + server.off('listening', onListening); + reject(error); + }; + const onListening = () => { + server.off('error', onError); + resolve(); + }; + server.once('error', onError); + server.once('listening', onListening); + server.listen(port, host); + }); + } + readBody(request) { return new Promise((resolve, reject) => { const chunks = []; diff --git a/panel/index.js b/panel/index.js index 8cd449a..589f5b5 100644 --- a/panel/index.js +++ b/panel/index.js @@ -233,7 +233,11 @@ module.exports = Editor.Panel.define({ this.$.statusPill.textContent = isRunning ? 'Running' : 'Stopped'; this.$.statusPill.classList.toggle('running', isRunning); this.$.statusPill.classList.toggle('stopped', !isRunning); - this.$.statusText.textContent = `${status.url || ''} | Project: ${status.projectName || ''} | Cocos ${status.cocosVersion || ''}`; + const portText = status.portFallbackActive + ? ` | Port fallback: ${status.requestedPort} -> ${status.port}` + : ''; + this.$.statusText.textContent = + `${status.url || ''} | Project: ${status.projectName || ''} | Cocos ${status.cocosVersion || ''}${portText}`; this.$.enabledInput.value = Boolean(isRunning || config.autostart); this.$.portInput.value = Number(config.port || status.port || 8765);