177 lines
4.6 KiB
JavaScript
177 lines
4.6 KiB
JavaScript
'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));
|
|
}
|
|
});
|