2 Commits
Author SHA1 Message Date
winlifes e3dc45468b Release v0.3.2 2026-05-20 19:24:33 -07:00
winlifes 63c18b0b5f Release v0.3.1 2026-05-20 19:15:04 -07:00
12 changed files with 1694 additions and 5 deletions
+6
View File
@@ -75,3 +75,9 @@ jobs:
- name: Run tests - name: Run tests
run: npm test run: npm test
- name: Run release metadata checks
run: npm run release:check
- name: Run npm package dry-run
run: npm run pack:dry-run
+2
View File
@@ -6,3 +6,5 @@ Library/
library/ library/
dist/ dist/
build/ build/
.release-tmp/
releases/
+16
View File
@@ -6,6 +6,22 @@ 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
### Added
- Added a documented release workflow and release checklist for Cocos extension publishing.
- Added release packaging scripts that generate a Cocos extension zip, release manifest, checksum file, and per-release README.
- Added CI validation for release metadata.
## [0.3.0] - 2026-05-20 ## [0.3.0] - 2026-05-20
### Added ### Added
+45 -1
View File
@@ -51,6 +51,8 @@ git clone https://github.com/FunplayAI/funplay-cocos-mcp.git extensions/funplay-
Then restart Cocos Creator or reload extensions from the editor. Then restart Cocos Creator or reload extensions from the editor.
For a non-git install, download `Funplay.CocosMcp.v<version>.zip` from the GitHub Releases page, unzip it, and move the extracted `funplay-cocos-mcp` folder into your project `extensions/` directory.
You can also install it globally by copying the folder into your Cocos Creator user extensions directory. You can also install it globally by copying the folder into your Cocos Creator user extensions directory.
### 2. Start the MCP Server ### 2. Start the MCP Server
@@ -177,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:
@@ -362,10 +389,27 @@ The server speaks MCP-style HTTP JSON-RPC 2.0 and supports tools, resources, res
## Development ## Development
Run a syntax check before publishing changes: Run checks before publishing changes:
```bash ```bash
npm run check npm run check
npm test
npm run release:check
npm run pack:dry-run
```
To generate a GitHub Release-ready extension package:
```bash
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.
Validate MCP Registry metadata before publishing:
```bash
npm run registry:validate
``` ```
## License ## License
+45 -1
View File
@@ -51,6 +51,8 @@ git clone https://github.com/FunplayAI/funplay-cocos-mcp.git extensions/funplay-
然后重启 Cocos Creator,或在编辑器里重新加载扩展。 然后重启 Cocos Creator,或在编辑器里重新加载扩展。
如果不想用 git 安装,可以从 GitHub Releases 下载 `Funplay.CocosMcp.v<version>.zip`,解压后把 `funplay-cocos-mcp` 目录移动到项目的 `extensions/` 目录。
你也可以把目录复制到 Cocos Creator 的全局用户扩展目录中。 你也可以把目录复制到 Cocos Creator 的全局用户扩展目录中。
### 2. 启动 MCP Server ### 2. 启动 MCP Server
@@ -177,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 客户端里试几个安全请求:
@@ -362,10 +389,27 @@ Cocos Creator Extension
## 开发 ## 开发
发布改动前可以跑语法检查: 发布改动前可以跑检查:
```bash ```bash
npm run check npm run check
npm test
npm run release:check
npm run pack:dry-run
```
生成可上传到 GitHub Release 的扩展包:
```bash
npm run release:package
```
产物会写入 `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
``` ```
## 协议 ## 协议
+94
View File
@@ -0,0 +1,94 @@
# Release Checklist
Use this checklist before publishing a new release of Funplay MCP for Cocos.
## 1. Repository Hygiene
- [ ] `git status` contains only intended release changes
- [ ] No tracked local junk is present (`.DS_Store`, `.idea/`, `node_modules/`, `Library/`, `Temp/`, `dist/`, `build/`)
- [ ] `package.json` version matches the intended release
- [ ] `CHANGELOG.md` includes the release notes for the target version
- [ ] `README.md` and `README_CN.md` match the current product behavior
## 2. Automated Verification
- [ ] `npm run check` passes
- [ ] `npm test` 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>/`
- [ ] `shasum -a 256 -c releases/<version>/SHA256SUMS.txt` passes
## 3. Package Contents
- [ ] The zip is named `Funplay.CocosMcp.v<version>.zip`
- [ ] 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 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 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
- [ ] `SHA256SUMS.txt` includes the zip, manifest, and release README
## 4. Cocos Smoke Test
- [ ] Test in a clean Cocos Creator `3.8+` project
- [ ] Install from the generated zip into `<project>/extensions/funplay-cocos-mcp`
- [ ] Restart Cocos Creator or reload extensions
- [ ] Open `Funplay > MCP Server`
- [ ] Start the MCP server successfully
- [ ] If the configured port is already in use, verify automatic fallback is reported clearly
- [ ] Run a read-only tool such as `get_project_info`
- [ ] Run a scene inspection tool such as `get_scene_info`
- [ ] Run a screenshot tool when the editor has a visible scene or preview
- [ ] Verify interaction logs appear in the MCP Server panel
## 5. MCP Client Verification
- [ ] Verify at least one primary client can connect (`Claude Code`, `Cursor`, `Codex`, etc.)
- [ ] Confirm `tools/list` returns the expected `core` profile tools
- [ ] Confirm a tool call succeeds end-to-end from the external client
- [ ] 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. 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`
- [ ] Release commit message is `Release v<version>`
- [ ] Tag is `v<version>`
- [ ] GitHub Release title is `v<version>`
- [ ] GitHub Release includes the zip, manifest, checksum file, and release README
- [ ] Public GitHub Release page renders the release notes and assets correctly
## 8. Publish
- [ ] Commit the release changes
- [ ] Create and push the release tag
- [ ] Create or update the GitHub Release
- [ ] Upload generated release assets
- [ ] 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
## 9. Post-Release
- [ ] 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 README install instructions and download links
- [ ] Announce the release where appropriate
+329
View File
@@ -0,0 +1,329 @@
# Funplay Cocos MCP Release Workflow
This document records the release workflow for publishing Funplay MCP for Cocos to:
- Git tags
- GitHub Releases
- Downloadable Cocos Creator extension zip packages
- npm stdio wrapper package
- Official MCP Registry
## Published Identity
- GitHub repository: `https://github.com/FunplayAI/funplay-cocos-mcp`
- Git tag format: `v<version>`
- GitHub Release tag: `v<version>`
- Extension package asset: `Funplay.CocosMcp.v<version>.zip`
- 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/`
## Version Alignment Rule
Keep these versions aligned:
- `package.json` `version`
- `CHANGELOG.md` release section
- Git tag `v<version>`
- GitHub Release `v<version>`
- `releases/<version>/release-manifest.json` `version`
- Release zip filename `Funplay.CocosMcp.v<version>.zip`
- `server.json` top-level version
- `server.json` npm package version
Example:
- `package.json`: `0.3.1`
- Git tag: `v0.3.1`
- GitHub Release: `v0.3.1`
- 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
Update:
1. `package.json`
- `"version": "<version>"`
2. `CHANGELOG.md`
- add a dated release notes block
3. `server.json`
- top-level `"version"`
- npm package `"version"`
Optional but recommended:
4. `README.md`
5. `README_CN.md`
6. GitHub Release notes text
## Release Steps
### 1. Verify Working Tree
```bash
git status --short --branch
```
The tree should contain only intentional release changes.
### 2. Update Versions And Notes
Update `package.json` and `CHANGELOG.md`.
Use semantic versions such as `0.3.1`, and keep release headings in this format:
```markdown
## [0.3.1] - 2026-05-20
```
### 3. Run Release Verification
```bash
npm run release:verify
```
This runs:
- JavaScript syntax checks
- Node.js tests
- release metadata validation
- npm package dry-run validation
- release package generation
The generated local artifacts are written to:
```text
releases/<version>/
```
Expected contents:
- `Funplay.CocosMcp.v<version>.zip`
- `release-manifest.json`
- `SHA256SUMS.txt`
- `README.md`
### 4. Inspect The Package
The release script validates that every archive path stays under:
```text
funplay-cocos-mcp/
```
The package must not contain local/build content such as:
- `.git/`
- `.github/`
- `.DS_Store`
- `node_modules/`
- `Library/`
- `Temp/`
- `dist/`
- `build/`
- `test/`
- `scripts/`
Verify checksums:
```bash
cd releases/<version>
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
```bash
git add .
git commit -m "Release v<version>"
git tag v<version>
git push origin main
git push origin v<version>
```
### 6. Create GitHub Release
Regenerate the final release artifacts from the tagged clean commit:
```bash
npm run release:package -- --strict-tag
```
If creating a new release:
```bash
gh release create v<version> \
-R FunplayAI/funplay-cocos-mcp \
--title "v<version>" \
--notes-file /path/to/release-notes.md \
releases/<version>/Funplay.CocosMcp.v<version>.zip \
releases/<version>/release-manifest.json \
releases/<version>/SHA256SUMS.txt \
releases/<version>/README.md
```
If the release already exists and only assets need to be replaced:
```bash
gh release upload v<version> \
-R FunplayAI/funplay-cocos-mcp \
--clobber \
releases/<version>/Funplay.CocosMcp.v<version>.zip \
releases/<version>/release-manifest.json \
releases/<version>/SHA256SUMS.txt \
releases/<version>/README.md
```
### 7. Verify GitHub Release
```bash
gh release view v<version> \
-R FunplayAI/funplay-cocos-mcp \
--json url,assets,isDraft,isPrerelease,publishedAt
```
Confirm the release has all four assets.
### 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:
1. Download `Funplay.CocosMcp.v<version>.zip`.
2. Unzip it.
3. Move `funplay-cocos-mcp` into a Cocos project `extensions/` directory.
4. Restart Cocos Creator or reload extensions.
5. Open `Funplay > MCP Server`.
6. Start the MCP server.
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
```bash
npm run release:verify
npm run registry:validate
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
### Release validation says the changelog section is missing
Cause:
- `CHANGELOG.md` does not contain `## [<version>] - YYYY-MM-DD`.
Fix:
- Add a dated release section before packaging.
### `zip` command is missing
Cause:
- The local environment does not have the `zip` CLI installed.
Fix:
- Install `zip`, then rerun `npm run release:package`.
### GitHub Release upload replaces the wrong assets
Cause:
- The version directory or release tag does not match `package.json` version.
Fix:
- Rerun `npm run release:check`.
- 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;
});
}
+49 -3
View File
@@ -1,14 +1,60 @@
{ {
"name": "funplay-cocos-mcp", "name": "funplay-cocos-mcp",
"package_version": 2, "package_version": 2,
"version": "0.3.0", "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", "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:package": "node scripts/release.js 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": {
+539
View File
@@ -0,0 +1,539 @@
#!/usr/bin/env node
'use strict';
const childProcess = require('child_process');
const crypto = require('crypto');
const fs = require('fs');
const path = require('path');
const ROOT = path.resolve(__dirname, '..');
const PACKAGE_DIR_NAME = 'funplay-cocos-mcp';
const RELEASES_DIR = path.join(ROOT, 'releases');
const TEMP_DIR = path.join(ROOT, '.release-tmp');
const ZIP_PREFIX = 'Funplay.CocosMcp';
const REPOSITORY_URL = 'https://github.com/FunplayAI/funplay-cocos-mcp';
const REQUIRED_REPO_FILES = [
'package.json',
'README.md',
'README_CN.md',
'RELEASE_WORKFLOW.md',
'RELEASE_CHECKLIST.md',
'CHANGELOG.md',
'CONTRIBUTING.md',
'LICENSE',
'server.json',
'bin/funplay-cocos-mcp.js',
'browser.js',
'scene.js',
'panel/index.js',
'lib/server.js',
'lib/tool-registry.js'
];
const PACKAGE_INCLUDES = [
'package.json',
'README.md',
'README_CN.md',
'CHANGELOG.md',
'CONTRIBUTING.md',
'LICENSE',
'server.json',
'bin',
'browser.js',
'scene.js',
'panel',
'lib'
];
const FORBIDDEN_TRACKED_SEGMENTS = new Set([
'.idea',
'node_modules',
'Library',
'library',
'Temp',
'temp',
'dist',
'build',
'coverage',
'releases',
'.release-tmp'
]);
const FORBIDDEN_ARCHIVE_SEGMENTS = new Set([
'.git',
'.github',
'.idea',
'node_modules',
'Library',
'library',
'Temp',
'temp',
'dist',
'build',
'coverage',
'releases',
'.release-tmp',
'scripts',
'test'
]);
const FORBIDDEN_NAMES = new Set([
'.DS_Store'
]);
function main() {
const command = process.argv[2] || 'check';
const options = parseOptions(process.argv.slice(3));
if (command === 'check') {
const context = checkRelease(options);
console.log(`Release check passed for v${context.version}.`);
return;
}
if (command === 'package') {
const context = checkRelease(options);
const artifacts = packageRelease(context);
console.log(`Release package ready: ${path.relative(ROOT, artifacts.releaseDir)}`);
console.log(`- ${artifacts.zipName}`);
console.log('- release-manifest.json');
console.log('- SHA256SUMS.txt');
console.log('- README.md');
return;
}
printUsage();
process.exitCode = 2;
}
function parseOptions(args) {
const options = {
version: '',
strictTag: false
};
for (let i = 0; i < args.length; i += 1) {
const arg = args[i];
if (arg === '--version' && args[i + 1]) {
options.version = args[i + 1];
i += 1;
} else if (arg === '--strict-tag') {
options.strictTag = true;
} else {
throw new Error(`Unknown release option: ${arg}`);
}
}
return options;
}
function checkRelease(options = {}) {
const errors = [];
const packageJson = readJson(path.join(ROOT, 'package.json'), errors);
const version = options.version || (packageJson && packageJson.version) || '';
const tag = `v${version}`;
if (!packageJson) {
throwErrors(errors);
}
if (options.version && options.version !== packageJson.version) {
errors.push(`--version ${options.version} does not match package.json version ${packageJson.version}.`);
}
if (packageJson.name !== 'funplay-cocos-mcp') {
errors.push('package.json name must be funplay-cocos-mcp.');
}
if (!Number.isInteger(packageJson.package_version) || packageJson.package_version <= 0) {
errors.push('package.json package_version must be a positive integer.');
}
if (!packageJson.main || !fs.existsSync(path.join(ROOT, packageJson.main))) {
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)) {
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) {
if (!fs.existsSync(path.join(ROOT, relative))) {
errors.push(`Missing required repository file: ${relative}`);
}
}
const changelogPath = path.join(ROOT, 'CHANGELOG.md');
const changelog = fs.existsSync(changelogPath) ? fs.readFileSync(changelogPath, 'utf8') : '';
if (version && !new RegExp(`^## \\[${escapeRegExp(version)}\\] - \\d{4}-\\d{2}-\\d{2}`, 'm').test(changelog)) {
errors.push(`CHANGELOG.md is missing a dated ## [${version}] release section.`);
}
const trackedFiles = gitLines(['ls-files']);
const forbiddenTracked = trackedFiles.filter(isForbiddenTrackedPath);
if (forbiddenTracked.length > 0) {
errors.push(`Tracked local/build junk must not be committed:\n- ${forbiddenTracked.join('\n- ')}`);
}
for (const relative of PACKAGE_INCLUDES) {
const fullPath = path.join(ROOT, relative);
if (!fs.existsSync(fullPath)) {
errors.push(`Package include path is missing: ${relative}`);
}
}
if (options.strictTag && !gitTagExists(tag)) {
errors.push(`Git tag ${tag} does not exist. Create it before publishing.`);
}
throwErrors(errors);
return {
packageJson,
version,
tag,
changelogNotes: extractChangelogNotes(changelog, version),
gitCommit: gitText(['rev-parse', 'HEAD']).trim(),
gitDirty: gitText(['status', '--porcelain']).trim() !== ''
};
}
function packageRelease(context) {
ensureCommand('zip');
const releaseDir = path.join(RELEASES_DIR, context.version);
const stagingRoot = path.join(TEMP_DIR, PACKAGE_DIR_NAME);
const zipName = `${ZIP_PREFIX}.v${context.version}.zip`;
const zipPath = path.join(releaseDir, zipName);
fs.rmSync(releaseDir, { recursive: true, force: true });
fs.rmSync(TEMP_DIR, { recursive: true, force: true });
fs.mkdirSync(releaseDir, { recursive: true });
fs.mkdirSync(stagingRoot, { recursive: true });
for (const relative of PACKAGE_INCLUDES) {
copyIntoPackage(relative, stagingRoot);
}
const stagedFiles = collectFiles(stagingRoot)
.map((filePath) => path.relative(TEMP_DIR, filePath).split(path.sep).join('/'));
validateArchivePaths(stagedFiles);
run('zip', ['-qr', zipPath, PACKAGE_DIR_NAME], { cwd: TEMP_DIR });
validateZipListing(zipPath);
const zipSha256 = sha256File(zipPath);
const zipSize = fs.statSync(zipPath).size;
const manifest = buildManifest(context, {
zipName,
zipSha256,
zipSize,
fileCount: stagedFiles.length
});
const manifestPath = path.join(releaseDir, 'release-manifest.json');
fs.writeFileSync(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`);
const readmePath = path.join(releaseDir, 'README.md');
fs.writeFileSync(readmePath, buildReleaseReadme(context, manifest));
const checksums = [
checksumLine(zipPath, zipName),
checksumLine(manifestPath, 'release-manifest.json'),
checksumLine(readmePath, 'README.md')
].join('');
fs.writeFileSync(path.join(releaseDir, 'SHA256SUMS.txt'), checksums);
fs.rmSync(TEMP_DIR, { recursive: true, force: true });
return {
releaseDir,
zipName
};
}
function copyIntoPackage(relative, stagingRoot) {
const source = path.join(ROOT, relative);
const destination = path.join(stagingRoot, relative);
fs.cpSync(source, destination, {
recursive: true,
force: true,
filter(sourcePath) {
const name = path.basename(sourcePath);
if (FORBIDDEN_NAMES.has(name)) {
return false;
}
const relativeSource = path.relative(ROOT, sourcePath).split(path.sep);
return !relativeSource.some((part) => FORBIDDEN_ARCHIVE_SEGMENTS.has(part));
}
});
}
function buildManifest(context, artifact) {
return {
version: context.version,
generatedAt: new Date().toISOString(),
repository: {
url: REPOSITORY_URL,
source: 'github'
},
git: {
tag: context.tag,
commit: context.gitCommit,
dirty: context.gitDirty
},
package: {
name: context.packageJson.name,
version: context.packageJson.version,
main: context.packageJson.main,
packageVersion: context.packageJson.package_version
},
artifacts: {
extensionZip: {
file: artifact.zipName,
sha256: artifact.zipSha256,
sizeBytes: artifact.zipSize,
fileCount: artifact.fileCount,
installDirectory: 'extensions/funplay-cocos-mcp',
githubDownloadUrl: `${REPOSITORY_URL}/releases/download/${context.tag}/${artifact.zipName}`
}
},
notes: firstMeaningfulLine(context.changelogNotes)
};
}
function buildReleaseReadme(context, manifest) {
const zip = manifest.artifacts.extensionZip;
return `# Funplay MCP for Cocos ${context.tag}
This folder contains the generated release artifacts for Funplay MCP for Cocos ${context.tag}.
## Artifacts
- \`${zip.file}\` - Cocos Creator extension package.
- \`release-manifest.json\` - Machine-readable release metadata.
- \`SHA256SUMS.txt\` - SHA-256 checksums for release artifacts.
## Install
1. Unzip \`${zip.file}\`.
2. Move the extracted \`${PACKAGE_DIR_NAME}\` folder into your Cocos project \`extensions/\` directory.
3. Restart Cocos Creator or reload extensions.
4. Open \`Funplay > MCP Server\`.
## Verify
\`\`\`bash
shasum -a 256 -c SHA256SUMS.txt
\`\`\`
`;
}
function validateArchivePaths(paths) {
const bad = [];
const prefix = `${PACKAGE_DIR_NAME}/`;
for (const archivePath of paths) {
const normalized = archivePath.replace(/\\/g, '/');
const parts = normalized.split('/').filter(Boolean);
if (!normalized.startsWith(prefix)) {
bad.push(`${archivePath} (must stay under ${PACKAGE_DIR_NAME}/)`);
continue;
}
if (parts.some((part) => part === '..' || part === '.')) {
bad.push(`${archivePath} (contains unsafe relative path segments)`);
continue;
}
if (parts.some((part) => FORBIDDEN_ARCHIVE_SEGMENTS.has(part) || FORBIDDEN_NAMES.has(part))) {
bad.push(`${archivePath} (contains forbidden release content)`);
}
}
if (bad.length > 0) {
throw new Error(`Release archive contains invalid paths:\n- ${bad.join('\n- ')}`);
}
}
function validateZipListing(zipPath) {
const result = childProcess.spawnSync('unzip', ['-Z1', zipPath], {
cwd: ROOT,
encoding: 'utf8'
});
if (result.error && result.error.code === 'ENOENT') {
console.warn('Warning: unzip is not available; skipped zip listing validation.');
return;
}
if (result.status !== 0) {
throw new Error(`Failed to inspect ${zipPath}:\n${result.stderr || result.stdout}`);
}
const listing = result.stdout
.split(/\r?\n/)
.map((line) => line.trim())
.filter(Boolean);
validateArchivePaths(listing);
}
function isForbiddenTrackedPath(relative) {
const parts = relative.split('/');
return parts.some((part) => FORBIDDEN_TRACKED_SEGMENTS.has(part) || FORBIDDEN_NAMES.has(part));
}
function readJson(filePath, errors) {
try {
return JSON.parse(fs.readFileSync(filePath, 'utf8'));
} catch (error) {
errors.push(`${path.relative(ROOT, filePath)} is not valid JSON: ${error.message}`);
return null;
}
}
function collectFiles(directory) {
const entries = fs.readdirSync(directory, { withFileTypes: true });
const files = [];
for (const entry of entries) {
const fullPath = path.join(directory, entry.name);
if (entry.isDirectory()) {
files.push(...collectFiles(fullPath));
} else if (entry.isFile()) {
files.push(fullPath);
}
}
return files;
}
function extractChangelogNotes(changelog, version) {
const heading = new RegExp(`^## \\[${escapeRegExp(version)}\\] - \\d{4}-\\d{2}-\\d{2}\\s*$`, 'm');
const match = heading.exec(changelog);
if (!match) {
return '';
}
const start = match.index + match[0].length;
const rest = changelog.slice(start);
const next = rest.search(/^## /m);
return (next >= 0 ? rest.slice(0, next) : rest).trim();
}
function firstMeaningfulLine(text) {
const line = text
.split(/\r?\n/)
.map((value) => value.trim())
.find((value) => value && !value.startsWith('###') && !value.startsWith('-'));
if (line) {
return line;
}
const bullet = text
.split(/\r?\n/)
.map((value) => value.trim())
.find((value) => value.startsWith('- '));
return bullet ? bullet.slice(2) : '';
}
function checksumLine(filePath, displayName) {
return `${sha256File(filePath)} ${displayName}\n`;
}
function sha256File(filePath) {
return crypto.createHash('sha256').update(fs.readFileSync(filePath)).digest('hex');
}
function ensureCommand(name) {
const result = childProcess.spawnSync(name, ['-v'], { encoding: 'utf8' });
if (result.error && result.error.code === 'ENOENT') {
throw new Error(`Required command not found: ${name}`);
}
}
function run(command, args, options = {}) {
const result = childProcess.spawnSync(command, args, {
cwd: options.cwd || ROOT,
encoding: 'utf8'
});
if (result.error) {
throw result.error;
}
if (result.status !== 0) {
throw new Error(`${command} ${args.join(' ')} failed:\n${result.stderr || result.stdout}`);
}
return result.stdout;
}
function gitLines(args) {
const text = gitText(args);
return text ? text.split(/\r?\n/).filter(Boolean) : [];
}
function gitText(args) {
const result = childProcess.spawnSync('git', args, {
cwd: ROOT,
encoding: 'utf8'
});
if (result.error || result.status !== 0) {
return '';
}
return result.stdout;
}
function gitTagExists(tag) {
const result = childProcess.spawnSync('git', ['rev-parse', '-q', '--verify', `refs/tags/${tag}`], {
cwd: ROOT,
encoding: 'utf8'
});
return result.status === 0;
}
function escapeRegExp(value) {
return String(value).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}
function throwErrors(errors) {
if (errors.length > 0) {
throw new Error(`Release validation failed:\n- ${errors.join('\n- ')}`);
}
}
function printUsage() {
console.error(`Usage:
node scripts/release.js check [--version <version>] [--strict-tag]
node scripts/release.js package [--version <version>] [--strict-tag]`);
}
try {
main();
} catch (error) {
console.error(error.message);
process.exitCode = 1;
}
+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));
}
});