Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8c46313ac6 | ||
|
|
be53f3c56c | ||
|
|
140d6295a8 |
@@ -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
|
||||
@@ -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
|
||||
@@ -4,6 +4,27 @@ All notable changes to Funplay MCP for Cocos will be documented in this file.
|
||||
|
||||
This project follows a simple changelog format inspired by [Keep a Changelog](https://keepachangelog.com/), and uses semantic versioning when releases are tagged.
|
||||
|
||||
## [0.1.1] - 2026-04-16
|
||||
|
||||
### Added
|
||||
|
||||
- Added automatic port fallback when the configured MCP port is already occupied.
|
||||
- Added actual-running-port reporting in MCP server status and panel state.
|
||||
- Added `.github/pull_request_template.md` for repository contribution guidance.
|
||||
- Added `.github/workflows/ci.yml` for lightweight GitHub validation.
|
||||
- Added a lightweight GitHub Star promotion log after successful MCP server startup.
|
||||
|
||||
### Changed
|
||||
|
||||
- Updated one-click MCP client configuration to write the actual running server port instead of the requested port when port fallback is active.
|
||||
- Updated the MCP panel status line to show configured-port to actual-port fallback information.
|
||||
- Updated the English and Chinese README files to document automatic port fallback behavior.
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed VS Code one-click configuration to use platform-specific config paths with macOS fallback behavior.
|
||||
- Fixed Windows one-click MCP configuration path resolution by using a more reliable home/appdata lookup strategy.
|
||||
|
||||
## [0.1.0] - 2026-04-15
|
||||
|
||||
### Added
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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`,再配合截图、诊断、资产、检查类工具。
|
||||
|
||||
+40
-4
@@ -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,
|
||||
|
||||
+41
-2
@@ -6,6 +6,45 @@ const path = require('path');
|
||||
|
||||
const SERVER_NAME = 'funplay_cocos';
|
||||
|
||||
function getUserHomePath() {
|
||||
const home = os.homedir();
|
||||
if (home) {
|
||||
return home;
|
||||
}
|
||||
|
||||
const homeDrive = process.env.HOMEDRIVE;
|
||||
const homePath = process.env.HOMEPATH;
|
||||
if (homeDrive && homePath) {
|
||||
return `${homeDrive}${homePath}`;
|
||||
}
|
||||
|
||||
return process.env.HOME || '';
|
||||
}
|
||||
|
||||
function getVSCodeConfigPath(homePath) {
|
||||
switch (process.platform) {
|
||||
case 'win32': {
|
||||
const appData = process.env.APPDATA || path.join(homePath, 'AppData', 'Roaming');
|
||||
return path.join(appData, 'Code', 'User', 'mcp.json');
|
||||
}
|
||||
|
||||
case 'darwin': {
|
||||
const primaryPath = path.join(homePath, 'Library', 'Application Support', 'Code', 'User', 'mcp.json');
|
||||
const primaryDirectory = path.dirname(primaryPath);
|
||||
if (fs.existsSync(primaryPath) || fs.existsSync(primaryDirectory)) {
|
||||
return primaryPath;
|
||||
}
|
||||
return path.join(homePath, '.vscode', 'mcp.json');
|
||||
}
|
||||
|
||||
case 'linux':
|
||||
return path.join(homePath, '.config', 'Code', 'User', 'mcp.json');
|
||||
|
||||
default:
|
||||
return path.join(homePath, '.vscode', 'mcp.json');
|
||||
}
|
||||
}
|
||||
|
||||
function ensureParent(filePath) {
|
||||
const dir = path.dirname(filePath);
|
||||
if (dir && !fs.existsSync(dir)) {
|
||||
@@ -67,7 +106,7 @@ function configureTomlTarget(target) {
|
||||
}
|
||||
|
||||
function buildTargets(config) {
|
||||
const home = os.homedir();
|
||||
const home = getUserHomePath();
|
||||
const url = `http://${config.host}:${config.port}/`;
|
||||
|
||||
return [
|
||||
@@ -88,7 +127,7 @@ function buildTargets(config) {
|
||||
{
|
||||
id: 'vscode',
|
||||
name: 'VS Code',
|
||||
configPath: path.join(home, '.vscode', 'mcp.json'),
|
||||
configPath: getVSCodeConfigPath(home),
|
||||
rootKey: 'servers',
|
||||
entry: { type: 'http', url },
|
||||
},
|
||||
|
||||
+95
-11
@@ -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 = [];
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "funplay-cocos-mcp",
|
||||
"package_version": 2,
|
||||
"version": "0.1.0",
|
||||
"version": "0.1.1",
|
||||
"description": "Embedded MCP server for Cocos Creator with scene script execution, project resources, prompts, and file/scene tools.",
|
||||
"author": "Funplay",
|
||||
"license": "MIT",
|
||||
|
||||
+5
-1
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user