Release v0.3.2

This commit is contained in:
winlifes
2026-05-20 19:24:33 -07:00
parent 63c18b0b5f
commit e3dc45468b
11 changed files with 853 additions and 10 deletions
+3
View File
@@ -78,3 +78,6 @@ jobs:
- name: Run release metadata checks - name: Run release metadata checks
run: npm run release:check run: npm run release:check
- name: Run npm package dry-run
run: npm run pack:dry-run
+8
View File
@@ -6,6 +6,14 @@ This project follows a simple changelog format inspired by [Keep a Changelog](ht
## [Unreleased] ## [Unreleased]
## [0.3.2] - 2026-05-20
### Added
- Added an npm-installable `funplay-cocos-mcp` stdio wrapper that bridges MCP clients to the local Cocos HTTP endpoint.
- Added MCP Registry metadata in `server.json`, including npm package ownership metadata via `mcpName`.
- Added wrapper tests, npm pack dry-run verification, and registry validation scripts.
## [0.3.1] - 2026-05-20 ## [0.3.1] - 2026-05-20
### Added ### Added
+32
View File
@@ -179,6 +179,31 @@ url = "http://127.0.0.1:8765/"
</details> </details>
### Optional: npm stdio Wrapper
If your MCP client prefers a local `stdio` command, install the npm wrapper after starting the Cocos editor server:
```bash
npm install -g funplay-cocos-mcp
```
Example MCP client entry:
```json
{
"mcpServers": {
"funplay_cocos": {
"command": "funplay-cocos-mcp",
"env": {
"FUNPLAY_COCOS_MCP_URL": "http://127.0.0.1:8765/"
}
}
}
}
```
The wrapper bridges stdio MCP traffic to the embedded Cocos HTTP endpoint. You can also run it with `npx funplay-cocos-mcp --url http://127.0.0.1:8765/`.
### 4. Verify the Connection ### 4. Verify the Connection
Open your AI client and try a few safe requests first: Open your AI client and try a few safe requests first:
@@ -370,6 +395,7 @@ Run checks before publishing changes:
npm run check npm run check
npm test npm test
npm run release:check npm run release:check
npm run pack:dry-run
``` ```
To generate a GitHub Release-ready extension package: To generate a GitHub Release-ready extension package:
@@ -380,6 +406,12 @@ npm run release:package
The package is written to `releases/<version>/` with a zip, manifest, checksum file, and release README. See [RELEASE_WORKFLOW.md](./RELEASE_WORKFLOW.md) and [RELEASE_CHECKLIST.md](./RELEASE_CHECKLIST.md) for the full process. The package is written to `releases/<version>/` with a zip, manifest, checksum file, and release README. See [RELEASE_WORKFLOW.md](./RELEASE_WORKFLOW.md) and [RELEASE_CHECKLIST.md](./RELEASE_CHECKLIST.md) for the full process.
Validate MCP Registry metadata before publishing:
```bash
npm run registry:validate
```
## License ## License
MIT License. See [LICENSE](./LICENSE). MIT License. See [LICENSE](./LICENSE).
+32
View File
@@ -179,6 +179,31 @@ url = "http://127.0.0.1:8765/"
</details> </details>
### 可选:npm stdio Wrapper
如果 MCP 客户端更适合使用本地 `stdio` 命令,可以在启动 Cocos 编辑器内置服务后安装 npm wrapper
```bash
npm install -g funplay-cocos-mcp
```
示例 MCP 客户端配置:
```json
{
"mcpServers": {
"funplay_cocos": {
"command": "funplay-cocos-mcp",
"env": {
"FUNPLAY_COCOS_MCP_URL": "http://127.0.0.1:8765/"
}
}
}
}
```
这个 wrapper 会把 stdio MCP 流量桥接到 Cocos 内置 HTTP endpoint。也可以直接运行 `npx funplay-cocos-mcp --url http://127.0.0.1:8765/`
### 4. 验证连接 ### 4. 验证连接
先在 AI 客户端里试几个安全请求: 先在 AI 客户端里试几个安全请求:
@@ -370,6 +395,7 @@ Cocos Creator Extension
npm run check npm run check
npm test npm test
npm run release:check npm run release:check
npm run pack:dry-run
``` ```
生成可上传到 GitHub Release 的扩展包: 生成可上传到 GitHub Release 的扩展包:
@@ -380,6 +406,12 @@ npm run release:package
产物会写入 `releases/<version>/`,包含 zip、manifest、checksum 和 release README。完整流程见 [RELEASE_WORKFLOW.md](./RELEASE_WORKFLOW.md) 和 [RELEASE_CHECKLIST.md](./RELEASE_CHECKLIST.md)。 产物会写入 `releases/<version>/`,包含 zip、manifest、checksum 和 release README。完整流程见 [RELEASE_WORKFLOW.md](./RELEASE_WORKFLOW.md) 和 [RELEASE_CHECKLIST.md](./RELEASE_CHECKLIST.md)。
发布 MCP Registry 前可以验证元数据:
```bash
npm run registry:validate
```
## 协议 ## 协议
MIT License。详见 [LICENSE](./LICENSE)。 MIT License。详见 [LICENSE](./LICENSE)。
+24 -3
View File
@@ -15,6 +15,8 @@ Use this checklist before publishing a new release of Funplay MCP for Cocos.
- [ ] `npm run check` passes - [ ] `npm run check` passes
- [ ] `npm test` passes - [ ] `npm test` passes
- [ ] `npm run release:check` passes - [ ] `npm run release:check` passes
- [ ] `npm run pack:dry-run` passes
- [ ] `npm run registry:validate` passes when `mcp-publisher` is available
- [ ] `npm run release:package` creates `releases/<version>/` - [ ] `npm run release:package` creates `releases/<version>/`
- [ ] `shasum -a 256 -c releases/<version>/SHA256SUMS.txt` passes - [ ] `shasum -a 256 -c releases/<version>/SHA256SUMS.txt` passes
@@ -23,6 +25,7 @@ Use this checklist before publishing a new release of Funplay MCP for Cocos.
- [ ] The zip is named `Funplay.CocosMcp.v<version>.zip` - [ ] The zip is named `Funplay.CocosMcp.v<version>.zip`
- [ ] The zip contains a single top-level `funplay-cocos-mcp/` folder - [ ] The zip contains a single top-level `funplay-cocos-mcp/` folder
- [ ] The zip contains runtime files: `package.json`, `browser.js`, `scene.js`, `panel/`, and `lib/` - [ ] The zip contains runtime files: `package.json`, `browser.js`, `scene.js`, `panel/`, and `lib/`
- [ ] The zip contains stdio wrapper metadata: `bin/funplay-cocos-mcp.js` and `server.json`
- [ ] The zip includes docs: `README.md`, `README_CN.md`, `CHANGELOG.md`, `CONTRIBUTING.md`, and `LICENSE` - [ ] The zip includes docs: `README.md`, `README_CN.md`, `CHANGELOG.md`, `CONTRIBUTING.md`, and `LICENSE`
- [ ] The zip does not contain `.git/`, `.github/`, `.DS_Store`, `node_modules/`, `Library/`, `Temp/`, `dist/`, `build/`, `test/`, or `scripts/` - [ ] The zip does not contain `.git/`, `.github/`, `.DS_Store`, `node_modules/`, `Library/`, `Temp/`, `dist/`, `build/`, `test/`, or `scripts/`
- [ ] `release-manifest.json` references the correct GitHub download URL - [ ] `release-manifest.json` references the correct GitHub download URL
@@ -47,8 +50,21 @@ Use this checklist before publishing a new release of Funplay MCP for Cocos.
- [ ] Confirm `tools/list` returns the expected `core` profile tools - [ ] Confirm `tools/list` returns the expected `core` profile tools
- [ ] Confirm a tool call succeeds end-to-end from the external client - [ ] Confirm a tool call succeeds end-to-end from the external client
- [ ] Verify one-click config output still matches the documented config snippets - [ ] Verify one-click config output still matches the documented config snippets
- [ ] Verify the stdio wrapper can connect with `funplay-cocos-mcp --url http://127.0.0.1:8765/`
## 6. GitHub Release Readiness ## 6. npm And MCP Registry Readiness
- [ ] `package.json` `name` is `funplay-cocos-mcp`
- [ ] `package.json` `version` matches `server.json` version
- [ ] `package.json` `mcpName` matches `server.json` name
- [ ] `package.json` `bin.funplay-cocos-mcp` points to `bin/funplay-cocos-mcp.js`
- [ ] `server.json` npm package identifier and version match `package.json`
- [ ] `server.json` npm transport type is `stdio`
- [ ] npm package dry-run includes `bin/`, `lib/`, `panel/`, `browser.js`, `scene.js`, and `server.json`
- [ ] npm credentials are available for `npm publish`
- [ ] MCP Registry credentials are available for `mcp-publisher publish`
## 7. GitHub Release Readiness
- [ ] CI passes on `main` - [ ] CI passes on `main`
- [ ] Release commit message is `Release v<version>` - [ ] Release commit message is `Release v<version>`
@@ -57,17 +73,22 @@ Use this checklist before publishing a new release of Funplay MCP for Cocos.
- [ ] GitHub Release includes the zip, manifest, checksum file, and release README - [ ] GitHub Release includes the zip, manifest, checksum file, and release README
- [ ] Public GitHub Release page renders the release notes and assets correctly - [ ] Public GitHub Release page renders the release notes and assets correctly
## 7. Publish ## 8. Publish
- [ ] Commit the release changes - [ ] Commit the release changes
- [ ] Create and push the release tag - [ ] Create and push the release tag
- [ ] Create or update the GitHub Release - [ ] Create or update the GitHub Release
- [ ] Upload generated release assets - [ ] Upload generated release assets
- [ ] Verify the GitHub Release asset list - [ ] Verify the GitHub Release asset list
- [ ] Publish npm package with `npm publish`
- [ ] Verify npm package with `npm view funplay-cocos-mcp@<version>`
- [ ] Publish MCP Registry metadata with `mcp-publisher publish server.json`
- [ ] Verify MCP Registry latest and specific-version endpoints
## 8. Post-Release ## 9. Post-Release
- [ ] Re-test installation from the public GitHub Release zip - [ ] Re-test installation from the public GitHub Release zip
- [ ] Re-test stdio wrapper installation from npm
- [ ] Check the update checker reports the new latest version - [ ] Check the update checker reports the new latest version
- [ ] Check README install instructions and download links - [ ] Check README install instructions and download links
- [ ] Announce the release where appropriate - [ ] Announce the release where appropriate
+105 -4
View File
@@ -5,6 +5,8 @@ This document records the release workflow for publishing Funplay MCP for Cocos
- Git tags - Git tags
- GitHub Releases - GitHub Releases
- Downloadable Cocos Creator extension zip packages - Downloadable Cocos Creator extension zip packages
- npm stdio wrapper package
- Official MCP Registry
## Published Identity ## Published Identity
@@ -13,6 +15,9 @@ This document records the release workflow for publishing Funplay MCP for Cocos
- GitHub Release tag: `v<version>` - GitHub Release tag: `v<version>`
- Extension package asset: `Funplay.CocosMcp.v<version>.zip` - Extension package asset: `Funplay.CocosMcp.v<version>.zip`
- Cocos extension folder name inside the zip: `funplay-cocos-mcp` - Cocos extension folder name inside the zip: `funplay-cocos-mcp`
- npm package id: `funplay-cocos-mcp`
- npm command: `funplay-cocos-mcp`
- MCP Registry server name: `io.github.FunplayAI/funplay-cocos-mcp`
- Default local MCP endpoint: `http://127.0.0.1:8765/` - Default local MCP endpoint: `http://127.0.0.1:8765/`
## Version Alignment Rule ## Version Alignment Rule
@@ -25,6 +30,8 @@ Keep these versions aligned:
- GitHub Release `v<version>` - GitHub Release `v<version>`
- `releases/<version>/release-manifest.json` `version` - `releases/<version>/release-manifest.json` `version`
- Release zip filename `Funplay.CocosMcp.v<version>.zip` - Release zip filename `Funplay.CocosMcp.v<version>.zip`
- `server.json` top-level version
- `server.json` npm package version
Example: Example:
@@ -32,6 +39,8 @@ Example:
- Git tag: `v0.3.1` - Git tag: `v0.3.1`
- GitHub Release: `v0.3.1` - GitHub Release: `v0.3.1`
- Release asset: `Funplay.CocosMcp.v0.3.1.zip` - Release asset: `Funplay.CocosMcp.v0.3.1.zip`
- npm package: `funplay-cocos-mcp@0.3.1`
- MCP Registry: `0.3.1`
## Files To Update For A New Release ## Files To Update For A New Release
@@ -41,12 +50,15 @@ Update:
- `"version": "<version>"` - `"version": "<version>"`
2. `CHANGELOG.md` 2. `CHANGELOG.md`
- add a dated release notes block - add a dated release notes block
3. `server.json`
- top-level `"version"`
- npm package `"version"`
Optional but recommended: Optional but recommended:
3. `README.md` 4. `README.md`
4. `README_CN.md` 5. `README_CN.md`
5. GitHub Release notes text 6. GitHub Release notes text
## Release Steps ## Release Steps
@@ -79,6 +91,7 @@ This runs:
- JavaScript syntax checks - JavaScript syntax checks
- Node.js tests - Node.js tests
- release metadata validation - release metadata validation
- npm package dry-run validation
- release package generation - release package generation
The generated local artifacts are written to: The generated local artifacts are written to:
@@ -122,6 +135,19 @@ cd releases/<version>
shasum -a 256 -c SHA256SUMS.txt shasum -a 256 -c SHA256SUMS.txt
``` ```
### 4.5 Validate npm And MCP Registry Metadata
```bash
npm run pack:dry-run
npm run registry:validate
```
The npm package must include the stdio wrapper command:
```bash
npx --yes ./funplay-cocos-mcp-<version>.tgz --version
```
### 5. Commit, Tag, And Push ### 5. Commit, Tag, And Push
```bash ```bash
@@ -175,7 +201,51 @@ gh release view v<version> \
Confirm the release has all four assets. Confirm the release has all four assets.
### 8. Post-Release Smoke Test ### 8. Publish To npm
```bash
npm publish
```
Verify the published package:
```bash
npm view funplay-cocos-mcp@<version> version bin mcpName
```
Notes:
- `package.json` `mcpName` must match `server.json` `name`.
- If `npm publish` returns `ENEEDAUTH`, run `npm adduser` with a publishing account and retry.
- If the package name already exists under another owner, choose a scoped package name and update both `package.json` and `server.json`.
### 9. Publish To MCP Registry
Log in if needed:
```bash
mcp-publisher login github
```
Publish:
```bash
mcp-publisher publish server.json
```
Verify latest:
```bash
curl "https://registry.modelcontextprotocol.io/v0.1/servers?search=io.github.FunplayAI/funplay-cocos-mcp&version=latest"
```
Check a specific version:
```bash
curl "https://registry.modelcontextprotocol.io/v0.1/servers/io.github.FunplayAI%2Ffunplay-cocos-mcp/versions/<version>"
```
### 10. Post-Release Smoke Test
Test the package from the public GitHub Release: Test the package from the public GitHub Release:
@@ -186,12 +256,16 @@ Test the package from the public GitHub Release:
5. Open `Funplay > MCP Server`. 5. Open `Funplay > MCP Server`.
6. Start the MCP server. 6. Start the MCP server.
7. Connect an MCP client and call `get_project_info`. 7. Connect an MCP client and call `get_project_info`.
8. Install the npm wrapper with `npm install -g funplay-cocos-mcp`.
9. Connect an MCP client through the `funplay-cocos-mcp` command and call `tools/list`.
## Current Verification Commands ## Current Verification Commands
```bash ```bash
npm run release:verify npm run release:verify
npm run registry:validate
gh release view v<version> -R FunplayAI/funplay-cocos-mcp --json url,assets gh release view v<version> -R FunplayAI/funplay-cocos-mcp --json url,assets
npm view funplay-cocos-mcp@<version> version bin mcpName
``` ```
## Common Failure Cases ## Common Failure Cases
@@ -226,3 +300,30 @@ Fix:
- Rerun `npm run release:check`. - Rerun `npm run release:check`.
- Confirm the command uses `releases/<version>/` and `v<version>`. - Confirm the command uses `releases/<version>/` and `v<version>`.
### npm `ENEEDAUTH`
Cause:
- The local machine is not logged in to npm.
Fix:
```bash
npm adduser
npm publish
```
### MCP Registry `Package validation failed`
Cause:
- npm package has not been published yet.
- `package.json` `mcpName` does not match `server.json` `name`.
- `server.json` package version does not match the npm package version.
Fix:
- Publish the npm package first.
- Rerun `npm run release:check`.
- Rerun `npm run registry:validate`.
+356
View File
@@ -0,0 +1,356 @@
#!/usr/bin/env node
'use strict';
const fs = require('fs');
const http = require('http');
const https = require('https');
const path = require('path');
const DEFAULT_URL = 'http://127.0.0.1:8765/';
const DEFAULT_TIMEOUT_SECONDS = 120;
const ACCEPT_HEADER = 'application/json, text/event-stream';
async function main() {
const args = process.argv.slice(2);
if (hasFlag(args, '--help') || hasFlag(args, '-h')) {
printHelp();
return 0;
}
if (hasFlag(args, '--version')) {
console.error(`funplay-cocos-mcp ${readPackageVersion()}`);
return 0;
}
const urlText = getOption(args, '--url')
|| process.env.FUNPLAY_COCOS_MCP_URL
|| DEFAULT_URL;
const timeoutText = getOption(args, '--timeout-seconds')
|| process.env.FUNPLAY_COCOS_MCP_TIMEOUT_SECONDS
|| String(DEFAULT_TIMEOUT_SECONDS);
const endpoint = parseEndpoint(urlText);
if (!endpoint) {
console.error(`Invalid --url value: ${urlText}`);
return 2;
}
const timeoutSeconds = Number.parseInt(timeoutText, 10);
if (!Number.isFinite(timeoutSeconds) || timeoutSeconds <= 0) {
console.error(`Invalid timeout value: ${timeoutText}`);
return 2;
}
console.error(`[Funplay Cocos MCP] Bridging stdio to ${endpoint.href}`);
await bridgeStdioToHttp({
input: process.stdin,
output: process.stdout,
endpoint,
timeoutMs: timeoutSeconds * 1000
});
return 0;
}
async function bridgeStdioToHttp({ input, output, endpoint, timeoutMs }) {
let sessionId = '';
while (true) {
const message = await readMessage(input);
if (message === null) {
return;
}
let parsed = null;
let requestId = null;
try {
parsed = JSON.parse(message);
requestId = getRequestId(parsed);
} catch (error) {
await writeJsonRpcError(output, null, -32700, 'Parse error');
continue;
}
try {
const response = await postJsonRpc(endpoint, message, {
timeoutMs,
sessionId
});
const nextSessionId = response.headers['mcp-session-id'];
if (typeof nextSessionId === 'string' && nextSessionId) {
sessionId = nextSessionId;
}
if (response.statusCode >= 200 && response.statusCode < 300 && response.body.trim()) {
await writeMessage(output, response.body);
continue;
}
if (response.statusCode >= 200 && response.statusCode < 300 && isNotification(parsed)) {
continue;
}
if (requestId !== null) {
const messageText = response.body.trim()
? `Cocos MCP server returned HTTP ${response.statusCode}: ${response.body}`
: `Cocos MCP server returned HTTP ${response.statusCode}.`;
await writeJsonRpcError(output, requestId, -32000, messageText);
}
} catch (error) {
console.error(`[Funplay Cocos MCP] ${error.message}`);
if (requestId !== null) {
await writeJsonRpcError(output, requestId, -32000, `Proxy transport error: ${error.message}`);
}
}
}
}
function postJsonRpc(endpoint, body, { timeoutMs, sessionId }) {
return new Promise((resolve, reject) => {
const client = endpoint.protocol === 'https:' ? https : http;
const request = client.request(
endpoint,
{
method: 'POST',
headers: {
Accept: ACCEPT_HEADER,
'Content-Type': 'application/json',
'Content-Length': Buffer.byteLength(body),
...(sessionId ? { 'Mcp-Session-Id': sessionId } : {})
},
timeout: timeoutMs
},
(response) => {
const chunks = [];
response.on('data', (chunk) => chunks.push(chunk));
response.on('end', () => {
resolve({
statusCode: response.statusCode || 0,
headers: response.headers,
body: Buffer.concat(chunks).toString('utf8')
});
});
}
);
request.on('timeout', () => {
request.destroy(new Error(`HTTP request timed out after ${timeoutMs / 1000} seconds.`));
});
request.on('error', reject);
request.end(body);
});
}
function parseEndpoint(value) {
try {
const endpoint = new URL(value);
if (endpoint.protocol !== 'http:' && endpoint.protocol !== 'https:') {
return null;
}
return endpoint;
} catch (error) {
return null;
}
}
function getRequestId(value) {
if (!value || typeof value !== 'object' || Array.isArray(value)) {
return null;
}
return Object.prototype.hasOwnProperty.call(value, 'id') ? value.id : null;
}
function isNotification(value) {
return Boolean(value && typeof value === 'object' && !Array.isArray(value) && value.method && !Object.prototype.hasOwnProperty.call(value, 'id'));
}
async function writeJsonRpcError(output, id, code, message) {
await writeMessage(output, JSON.stringify({
jsonrpc: '2.0',
id,
error: {
code,
message
}
}));
}
async function readMessage(input) {
const headers = new Map();
while (true) {
const line = await readHeaderLine(input);
if (line === null) {
return headers.size === 0 ? null : Promise.reject(new Error('Unexpected EOF while reading MCP headers.'));
}
if (line === '') {
break;
}
const separator = line.indexOf(':');
if (separator <= 0) {
continue;
}
headers.set(line.slice(0, separator).trim().toLowerCase(), line.slice(separator + 1).trim());
}
const contentLength = Number.parseInt(headers.get('content-length') || '', 10);
if (!Number.isFinite(contentLength) || contentLength < 0) {
throw new Error('Missing Content-Length header.');
}
const payload = await readExact(input, contentLength);
return payload.toString('utf8');
}
function readHeaderLine(input) {
return new Promise((resolve, reject) => {
const chunks = [];
function cleanup() {
input.off('readable', onReadable);
input.off('end', onEnd);
input.off('error', onError);
}
function onReadable() {
let byte;
while ((byte = input.read(1)) !== null) {
if (byte[0] === 0x0a) {
cleanup();
if (chunks.length > 0 && chunks[chunks.length - 1][0] === 0x0d) {
chunks.pop();
}
resolve(Buffer.concat(chunks).toString('ascii'));
return;
}
chunks.push(byte);
}
}
function onEnd() {
cleanup();
resolve(chunks.length === 0 ? null : Buffer.concat(chunks).toString('ascii'));
}
function onError(error) {
cleanup();
reject(error);
}
input.on('readable', onReadable);
input.once('end', onEnd);
input.once('error', onError);
onReadable();
});
}
function readExact(input, length) {
return new Promise((resolve, reject) => {
const chunks = [];
let remaining = length;
function cleanup() {
input.off('readable', onReadable);
input.off('end', onEnd);
input.off('error', onError);
}
function onReadable() {
while (remaining > 0) {
const chunk = input.read(remaining);
if (chunk === null) {
return;
}
chunks.push(chunk);
remaining -= chunk.length;
}
cleanup();
resolve(Buffer.concat(chunks, length));
}
function onEnd() {
cleanup();
reject(new Error('Unexpected EOF while reading MCP payload.'));
}
function onError(error) {
cleanup();
reject(error);
}
input.on('readable', onReadable);
input.once('end', onEnd);
input.once('error', onError);
onReadable();
});
}
function writeMessage(output, json) {
const payload = Buffer.from(json, 'utf8');
return new Promise((resolve, reject) => {
output.write(`Content-Length: ${payload.length}\r\n\r\n`, 'ascii', (headerError) => {
if (headerError) {
reject(headerError);
return;
}
output.write(payload, (payloadError) => {
if (payloadError) {
reject(payloadError);
return;
}
resolve();
});
});
});
}
function hasFlag(args, name) {
return args.some((arg) => arg.toLowerCase() === name.toLowerCase());
}
function getOption(args, name) {
for (let i = 0; i < args.length - 1; i += 1) {
if (args[i].toLowerCase() === name.toLowerCase()) {
return args[i + 1];
}
}
return '';
}
function readPackageVersion() {
try {
const packagePath = path.resolve(__dirname, '..', 'package.json');
const packageJson = JSON.parse(fs.readFileSync(packagePath, 'utf8'));
return packageJson.version || '0.0.0';
} catch (error) {
return '0.0.0';
}
}
function printHelp() {
console.error('funplay-cocos-mcp');
console.error('Bridges stdio MCP traffic to a local Cocos Creator HTTP MCP server.');
console.error();
console.error('Options:');
console.error(' --url <http://127.0.0.1:8765/> Cocos MCP HTTP endpoint.');
console.error(' --timeout-seconds <120> HTTP timeout per request.');
console.error(' --version Print the proxy version.');
console.error(' --help Show this help.');
console.error();
console.error('Environment:');
console.error(' FUNPLAY_COCOS_MCP_URL');
console.error(' FUNPLAY_COCOS_MCP_TIMEOUT_SECONDS');
}
if (require.main === module) {
main().then((code) => {
process.exitCode = code;
}).catch((error) => {
console.error(`[Funplay Cocos MCP] ${error.message}`);
process.exitCode = 1;
});
}
+46 -3
View File
@@ -1,17 +1,60 @@
{ {
"name": "funplay-cocos-mcp", "name": "funplay-cocos-mcp",
"package_version": 2, "package_version": 2,
"version": "0.3.1", "version": "0.3.2",
"description": "Embedded MCP server for Cocos Creator with scene script execution, project resources, prompts, and file/scene tools.", "description": "Embedded MCP server for Cocos Creator with scene script execution, project resources, prompts, and file/scene tools.",
"author": "Funplay", "author": "Funplay",
"license": "MIT", "license": "MIT",
"main": "browser.js", "main": "browser.js",
"bin": {
"funplay-cocos-mcp": "bin/funplay-cocos-mcp.js"
},
"mcpName": "io.github.FunplayAI/funplay-cocos-mcp",
"repository": {
"type": "git",
"url": "git+https://github.com/FunplayAI/funplay-cocos-mcp.git"
},
"homepage": "https://github.com/FunplayAI/funplay-cocos-mcp#readme",
"bugs": {
"url": "https://github.com/FunplayAI/funplay-cocos-mcp/issues"
},
"keywords": [
"mcp",
"cocos",
"cocos-creator",
"funplay",
"model-context-protocol",
"ai"
],
"engines": {
"node": ">=18"
},
"files": [
"bin/",
"lib/",
"panel/",
"browser.js",
"scene.js",
"server.json",
"README.md",
"README_CN.md",
"CHANGELOG.md",
"CONTRIBUTING.md",
"LICENSE",
"RELEASE_WORKFLOW.md",
"RELEASE_CHECKLIST.md"
],
"publishConfig": {
"access": "public"
},
"scripts": { "scripts": {
"check": "node --check browser.js && node --check scene.js && node --check panel/index.js && node --check lib/assets.js && node --check lib/client-config.js && node --check lib/config.js && node --check lib/diagnostics.js && node --check lib/electron-tools.js && node --check lib/input.js && node --check lib/interaction-log.js && node --check lib/logs.js && node --check lib/path-safety.js && node --check lib/prefabs.js && node --check lib/project-instructions.js && node --check lib/prompts.js && node --check lib/resources.js && node --check lib/runtime-log.js && node --check lib/screenshots.js && node --check lib/server.js && node --check lib/tool-registry.js && node --check lib/update-checker.js && node --check lib/utils.js && node --check scripts/release.js", "check": "node --check browser.js && node --check scene.js && node --check panel/index.js && node --check bin/funplay-cocos-mcp.js && node --check lib/assets.js && node --check lib/client-config.js && node --check lib/config.js && node --check lib/diagnostics.js && node --check lib/electron-tools.js && node --check lib/input.js && node --check lib/interaction-log.js && node --check lib/logs.js && node --check lib/path-safety.js && node --check lib/prefabs.js && node --check lib/project-instructions.js && node --check lib/prompts.js && node --check lib/resources.js && node --check lib/runtime-log.js && node --check lib/screenshots.js && node --check lib/server.js && node --check lib/tool-registry.js && node --check lib/update-checker.js && node --check lib/utils.js && node --check scripts/release.js",
"test": "node --test", "test": "node --test",
"pack:dry-run": "npm pack --dry-run",
"registry:validate": "mcp-publisher validate server.json",
"release:check": "node scripts/release.js check", "release:check": "node scripts/release.js check",
"release:package": "node scripts/release.js package", "release:package": "node scripts/release.js package",
"release:verify": "npm run check && npm test && npm run release:check && npm run release:package" "release:verify": "npm run check && npm test && npm run release:check && npm run pack:dry-run && npm run release:package"
}, },
"panels": { "panels": {
"default": { "default": {
+34
View File
@@ -22,6 +22,8 @@ const REQUIRED_REPO_FILES = [
'CHANGELOG.md', 'CHANGELOG.md',
'CONTRIBUTING.md', 'CONTRIBUTING.md',
'LICENSE', 'LICENSE',
'server.json',
'bin/funplay-cocos-mcp.js',
'browser.js', 'browser.js',
'scene.js', 'scene.js',
'panel/index.js', 'panel/index.js',
@@ -36,6 +38,8 @@ const PACKAGE_INCLUDES = [
'CHANGELOG.md', 'CHANGELOG.md',
'CONTRIBUTING.md', 'CONTRIBUTING.md',
'LICENSE', 'LICENSE',
'server.json',
'bin',
'browser.js', 'browser.js',
'scene.js', 'scene.js',
'panel', 'panel',
@@ -150,10 +154,40 @@ function checkRelease(options = {}) {
errors.push('package.json main must point to an existing file.'); errors.push('package.json main must point to an existing file.');
} }
if (!packageJson.bin || packageJson.bin['funplay-cocos-mcp'] !== 'bin/funplay-cocos-mcp.js') {
errors.push('package.json bin.funplay-cocos-mcp must point to bin/funplay-cocos-mcp.js.');
}
if (!/^\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?$/.test(version)) { if (!/^\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?$/.test(version)) {
errors.push(`package.json version must be semver-like, got: ${version}`); errors.push(`package.json version must be semver-like, got: ${version}`);
} }
const serverJson = readJson(path.join(ROOT, 'server.json'), errors);
if (serverJson) {
if (serverJson.name !== packageJson.mcpName) {
errors.push('server.json name must match package.json mcpName.');
}
if (serverJson.version !== version) {
errors.push(`server.json version ${serverJson.version} must match package.json version ${version}.`);
}
const npmPackage = Array.isArray(serverJson.packages)
? serverJson.packages.find((entry) => entry && entry.registryType === 'npm')
: null;
if (!npmPackage) {
errors.push('server.json must include an npm package entry.');
} else {
if (npmPackage.identifier !== packageJson.name) {
errors.push(`server.json npm identifier ${npmPackage.identifier} must match package.json name ${packageJson.name}.`);
}
if (npmPackage.version !== version) {
errors.push(`server.json npm package version ${npmPackage.version} must match package.json version ${version}.`);
}
if (!npmPackage.transport || npmPackage.transport.type !== 'stdio') {
errors.push('server.json npm package transport must be stdio.');
}
}
}
for (const relative of REQUIRED_REPO_FILES) { for (const relative of REQUIRED_REPO_FILES) {
if (!fs.existsSync(path.join(ROOT, relative))) { if (!fs.existsSync(path.join(ROOT, relative))) {
errors.push(`Missing required repository file: ${relative}`); errors.push(`Missing required repository file: ${relative}`);
+37
View File
@@ -0,0 +1,37 @@
{
"$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json",
"name": "io.github.FunplayAI/funplay-cocos-mcp",
"title": "Funplay Cocos MCP",
"description": "stdio bridge for the local Cocos Creator Editor MCP server from FunplayAI/funplay-cocos-mcp.",
"repository": {
"url": "https://github.com/FunplayAI/funplay-cocos-mcp",
"source": "github"
},
"version": "0.3.2",
"packages": [
{
"registryType": "npm",
"identifier": "funplay-cocos-mcp",
"version": "0.3.2",
"transport": {
"type": "stdio"
},
"environmentVariables": [
{
"name": "FUNPLAY_COCOS_MCP_URL",
"description": "Optional local Cocos MCP HTTP endpoint. Defaults to http://127.0.0.1:8765/.",
"format": "string",
"isRequired": false,
"isSecret": false
},
{
"name": "FUNPLAY_COCOS_MCP_TIMEOUT_SECONDS",
"description": "Optional HTTP timeout per request in seconds. Defaults to 120.",
"format": "string",
"isRequired": false,
"isSecret": false
}
]
}
]
}
+176
View File
@@ -0,0 +1,176 @@
'use strict';
const assert = require('node:assert/strict');
const childProcess = require('node:child_process');
const http = require('node:http');
const path = require('node:path');
const test = require('node:test');
const ROOT = path.resolve(__dirname, '..');
const WRAPPER = path.join(ROOT, 'bin', 'funplay-cocos-mcp.js');
const PACKAGE = require('../package.json');
function writeFramed(stream, value) {
const payload = Buffer.from(JSON.stringify(value), 'utf8');
stream.write(`Content-Length: ${payload.length}\r\n\r\n`);
stream.write(payload);
}
function createFramedReader(stream) {
let buffer = Buffer.alloc(0);
const waiters = [];
function readFromBuffer() {
const marker = buffer.indexOf('\r\n\r\n');
if (marker < 0) {
return null;
}
const header = buffer.slice(0, marker).toString('ascii');
const match = /^content-length:\s*(\d+)$/im.exec(header);
if (!match) {
throw new Error(`Missing Content-Length header: ${header}`);
}
const length = Number.parseInt(match[1], 10);
const start = marker + 4;
const end = start + length;
if (buffer.length < end) {
return null;
}
const payload = buffer.slice(start, end).toString('utf8');
buffer = buffer.slice(end);
return JSON.parse(payload);
}
function pump() {
while (waiters.length > 0) {
const value = readFromBuffer();
if (value === null) {
return;
}
waiters.shift().resolve(value);
}
}
stream.on('data', (chunk) => {
buffer = Buffer.concat([buffer, chunk]);
pump();
});
stream.on('error', (error) => {
while (waiters.length > 0) {
waiters.shift().reject(error);
}
});
return function readNext() {
const value = readFromBuffer();
if (value !== null) {
return Promise.resolve(value);
}
return new Promise((resolve, reject) => {
waiters.push({ resolve, reject });
});
};
}
function createHttpProxyTarget(handler) {
const server = http.createServer(handler);
return new Promise((resolve, reject) => {
server.once('error', reject);
server.listen(0, '127.0.0.1', () => {
resolve({
server,
url: `http://127.0.0.1:${server.address().port}/`
});
});
});
}
function readBody(request) {
return new Promise((resolve, reject) => {
const chunks = [];
request.on('data', (chunk) => chunks.push(chunk));
request.on('end', () => resolve(Buffer.concat(chunks).toString('utf8')));
request.on('error', reject);
});
}
function waitForExit(child) {
return new Promise((resolve, reject) => {
child.once('error', reject);
child.once('exit', (code) => resolve(code));
});
}
test('stdio wrapper prints package version', () => {
const result = childProcess.spawnSync(process.execPath, [WRAPPER, '--version'], {
cwd: ROOT,
encoding: 'utf8'
});
assert.equal(result.status, 0);
assert.match(result.stderr, new RegExp(`funplay-cocos-mcp ${PACKAGE.version}`));
});
test('stdio wrapper proxies framed JSON-RPC to the Cocos HTTP endpoint', async () => {
const requests = [];
const target = await createHttpProxyTarget(async (request, response) => {
const body = await readBody(request);
requests.push({
headers: request.headers,
body: JSON.parse(body)
});
response.setHeader('Content-Type', 'application/json');
if (requests.length === 1) {
response.setHeader('Mcp-Session-Id', 'session-1');
}
response.end(JSON.stringify({
jsonrpc: '2.0',
id: requests.length,
result: { ok: true, index: requests.length }
}));
});
const child = childProcess.spawn(process.execPath, [WRAPPER, '--url', target.url], {
cwd: ROOT,
stdio: ['pipe', 'pipe', 'pipe']
});
const readNext = createFramedReader(child.stdout);
try {
writeFramed(child.stdin, {
jsonrpc: '2.0',
id: 1,
method: 'initialize',
params: {}
});
assert.deepEqual(await readNext(), {
jsonrpc: '2.0',
id: 1,
result: { ok: true, index: 1 }
});
writeFramed(child.stdin, {
jsonrpc: '2.0',
id: 2,
method: 'tools/list'
});
assert.deepEqual(await readNext(), {
jsonrpc: '2.0',
id: 2,
result: { ok: true, index: 2 }
});
assert.equal(requests.length, 2);
assert.equal(requests[0].headers.accept, 'application/json, text/event-stream');
assert.equal(requests[1].headers['mcp-session-id'], 'session-1');
assert.equal(requests[1].body.method, 'tools/list');
} finally {
child.stdin.end();
await waitForExit(child);
await new Promise((resolve) => target.server.close(resolve));
}
});