Release v0.2.0
This commit is contained in:
@@ -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
@@ -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();
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
Reference in New Issue
Block a user