Release v0.2.0

This commit is contained in:
winlifes
2026-05-20 06:06:36 -07:00
parent e01f058e3e
commit 3ab48e2457
18 changed files with 1417 additions and 99 deletions
+23
View File
@@ -0,0 +1,23 @@
'use strict';
const assert = require('node:assert/strict');
const fs = require('node:fs');
const os = require('node:os');
const path = require('node:path');
const test = require('node:test');
const { getRecentProjectLogs, searchProjectLogs } = require('../lib/logs');
test('project log helpers read and search common project log files', () => {
const projectPath = fs.mkdtempSync(path.join(os.tmpdir(), 'funplay-cocos-logs-'));
const logDir = path.join(projectPath, 'temp', 'logs');
fs.mkdirSync(logDir, { recursive: true });
fs.writeFileSync(path.join(logDir, 'editor.log'), 'first line\nError: broken scene\nlast line\n', 'utf8');
const recent = getRecentProjectLogs(projectPath, { limit: 5, lines: 2 });
assert.equal(recent.length, 1);
assert.match(recent[0].text, /broken scene/);
const matches = searchProjectLogs(projectPath, { query: 'broken', limit: 5 });
assert.equal(matches.count, 1);
assert.equal(matches.matches[0].path, 'temp/logs/editor.log');
});
+115 -2
View File
@@ -1,6 +1,7 @@
'use strict';
const assert = require('node:assert/strict');
const http = require('node:http');
const test = require('node:test');
const {
MCP_PROTOCOL_VERSION,
@@ -8,9 +9,9 @@ const {
SUPPORTED_PROTOCOL_VERSIONS,
} = require('../lib/server');
function createServer(toolRegistry = {}) {
function createServer(toolRegistry = {}, config = {}) {
return new McpServer({
config: { host: '127.0.0.1', port: 8765 },
config: { host: '127.0.0.1', port: 8765, ...config },
toolRegistry: {
listTools: () => [],
callTool: async () => 'ok',
@@ -26,11 +27,45 @@ function createServer(toolRegistry = {}) {
getPrompt: () => ({ messages: [] }),
},
interactionLog: { add() {} },
runtimeLog: { add() {} },
serverName: 'test-server',
serverVersion: '0.0.0-test',
});
}
function httpJson(port, payload, headers = {}) {
const body = payload === undefined ? '' : JSON.stringify(payload);
return new Promise((resolve, reject) => {
const request = http.request(
{
host: '127.0.0.1',
port,
method: 'POST',
path: '/',
headers: {
Accept: 'application/json, text/event-stream',
'Content-Type': 'application/json',
'Content-Length': Buffer.byteLength(body),
...headers,
},
},
(response) => {
const chunks = [];
response.on('data', (chunk) => chunks.push(chunk));
response.on('end', () => {
resolve({
statusCode: response.statusCode,
headers: response.headers,
body: Buffer.concat(chunks).toString('utf8'),
});
});
}
);
request.on('error', reject);
request.end(body);
});
}
test('initialize negotiates the current MCP protocol version by default', async () => {
const server = createServer();
const response = await server.handleRpcRequest({
@@ -126,3 +161,81 @@ test('unsupported protocol version headers are rejected when present after initi
assert.equal(response.error.code, -32600);
assert.match(response.error.message, /Unsupported MCP protocol version/);
});
test('streamable HTTP Accept headers must allow json and event-stream', () => {
const server = createServer();
assert.equal(server.validateAcceptHeader({ headers: { accept: 'application/json, text/event-stream' } }), null);
assert.equal(server.validateAcceptHeader({ headers: { accept: '*/*' } }), null);
const response = server.validateAcceptHeader({ headers: { accept: 'application/json' } });
assert.equal(response.error.code, -32600);
assert.match(response.error.message, /Accept header/);
});
test('JSON-RPC responses and notifications are classified for 202 handling', () => {
const server = createServer();
assert.equal(server.classifyJsonRpcMessage({ jsonrpc: '2.0', id: 1, result: {} }), 'response');
assert.equal(server.classifyJsonRpcMessage({ jsonrpc: '2.0', method: 'notifications/initialized' }), 'notification');
assert.equal(server.classifyJsonRpcMessage({ jsonrpc: '2.0', id: 1, method: 'tools/list' }), 'request');
});
test('session validation requires a known session when enabled', () => {
const server = createServer({}, { enableSessions: true });
server.sessions.add('abc123');
assert.equal(
server.validateSession(
{ headers: { 'mcp-session-id': 'abc123' } },
{ jsonrpc: '2.0', id: 1, method: 'tools/list' }
),
null
);
const missing = server.validateSession(
{ headers: {} },
{ jsonrpc: '2.0', id: 1, method: 'tools/list' }
);
assert.equal(missing.statusCode, 400);
const unknown = server.validateSession(
{ headers: { 'mcp-session-id': 'nope' } },
{ jsonrpc: '2.0', id: 1, method: 'tools/list' }
);
assert.equal(unknown.statusCode, 404);
});
test('HTTP notifications return 202 Accepted with no body', async () => {
const server = createServer({}, { port: 0 });
await server.start();
try {
const response = await httpJson(server.getPort(), {
jsonrpc: '2.0',
method: 'notifications/initialized',
});
assert.equal(response.statusCode, 202);
assert.equal(response.body, '');
} finally {
await server.stop();
}
});
test('HTTP initialize can return an optional session id when sessions are enabled', async () => {
const server = createServer({}, { port: 0, enableSessions: true });
await server.start();
try {
const response = await httpJson(server.getPort(), {
jsonrpc: '2.0',
id: 1,
method: 'initialize',
params: { protocolVersion: MCP_PROTOCOL_VERSION },
});
assert.equal(response.statusCode, 200);
assert.match(response.headers['mcp-session-id'], /^[\x21-\x7e]+$/);
} finally {
await server.stop();
}
});
+28 -4
View File
@@ -5,13 +5,15 @@ const path = require('node:path');
const test = require('node:test');
const { createToolRegistry } = require('../lib/tool-registry');
function createRegistry(profile, projectPath = path.resolve('/tmp/funplay-cocos-test-project')) {
function createRegistry(profile, projectPath = path.resolve('/tmp/funplay-cocos-test-project'), configExtras = {}) {
return createToolRegistry({
getRuntimeContext: () => ({
config: { toolProfile: profile },
config: { toolProfile: profile, ...configExtras },
projectPath,
version: '0.0.0-test',
}),
interactionLog: { add() {} },
runtimeLog: { list: () => [], clear: () => 0 },
sceneBridge: { call: async () => ({ ok: true }) },
editorExecutor: async () => ({ ok: true }),
});
@@ -19,21 +21,43 @@ function createRegistry(profile, projectPath = path.resolve('/tmp/funplay-cocos-
test('core profile exposes the documented focused tool set', () => {
const tools = createRegistry('core').listTools();
assert.equal(tools.length, 22);
assert.equal(tools.length, 28);
assert.equal(tools.some((tool) => tool.name === 'execute_javascript'), true);
assert.equal(tools.some((tool) => tool.name === 'get_editor_state'), true);
assert.equal(tools.some((tool) => tool.name === 'get_tool_catalog'), true);
assert.equal(tools.some((tool) => tool.name === 'validate_scene'), true);
assert.equal(tools.some((tool) => tool.name === 'set_selection'), true);
assert.equal(tools.some((tool) => tool.name === 'write_file'), false);
});
test('full profile exposes all built-in tools', () => {
const tools = createRegistry('full').listTools();
assert.equal(tools.length, 70);
assert.equal(tools.length, 76);
assert.equal(tools.some((tool) => tool.name === 'write_file'), true);
assert.equal(tools.some((tool) => tool.name === 'get_editor_state'), true);
assert.equal(tools.some((tool) => tool.name === 'set_selection'), true);
});
test('custom profile can expose a category and disable a specific tool', () => {
const tools = createRegistry('custom', path.resolve('/tmp/funplay-cocos-test-project'), {
enabledToolCategories: ['files'],
disabledTools: ['write_file'],
}).listTools();
assert.equal(tools.some((tool) => tool.name === 'read_file'), true);
assert.equal(tools.some((tool) => tool.name === 'write_file'), false);
assert.equal(tools.some((tool) => tool.name === 'execute_javascript'), true);
});
test('tool catalog reports disabled tools under the current exposure settings', () => {
const catalog = createRegistry('core', path.resolve('/tmp/funplay-cocos-test-project'), {
disabledTools: ['execute_javascript'],
}).listToolCatalog();
const executeTool = catalog.find((tool) => tool.name === 'execute_javascript');
assert.equal(executeTool.enabled, false);
assert.equal(executeTool.category, 'execution');
});
test('file tools reject writes outside the project root', async () => {
const registry = createRegistry('full');
await assert.rejects(
+15
View File
@@ -0,0 +1,15 @@
'use strict';
const assert = require('node:assert/strict');
const test = require('node:test');
const { compareVersions, normalizeVersion } = require('../lib/update-checker');
test('normalizeVersion removes release tag prefixes and metadata', () => {
assert.equal(normalizeVersion('v1.2.3-beta+build'), '1.2.3');
});
test('compareVersions compares semantic version numbers', () => {
assert.equal(compareVersions('1.2.4', '1.2.3'), 1);
assert.equal(compareVersions('1.2.3', '1.2.4'), -1);
assert.equal(compareVersions('1.2.3', 'v1.2.3'), 0);
});