Release v0.1.2
This commit is contained in:
@@ -0,0 +1,38 @@
|
||||
'use strict';
|
||||
|
||||
const assert = require('node:assert/strict');
|
||||
const path = require('node:path');
|
||||
const test = require('node:test');
|
||||
const { isPathInside, resolveProjectPath } = require('../lib/path-safety');
|
||||
|
||||
test('resolveProjectPath resolves project-relative paths inside the project root', () => {
|
||||
const root = path.resolve('/tmp/funplay-cocos-test-project');
|
||||
assert.equal(resolveProjectPath(root, 'assets/player.ts'), path.join(root, 'assets', 'player.ts'));
|
||||
});
|
||||
|
||||
test('resolveProjectPath allows absolute paths only when they stay inside the project root', () => {
|
||||
const root = path.resolve('/tmp/funplay-cocos-test-project');
|
||||
const inside = path.join(root, 'assets', 'scene.scene');
|
||||
assert.equal(resolveProjectPath(root, inside), inside);
|
||||
});
|
||||
|
||||
test('resolveProjectPath rejects path traversal outside the project root', () => {
|
||||
const root = path.resolve('/tmp/funplay-cocos-test-project');
|
||||
assert.throws(
|
||||
() => resolveProjectPath(root, '../secret.txt'),
|
||||
/outside the Cocos project/
|
||||
);
|
||||
});
|
||||
|
||||
test('resolveProjectPath rejects absolute paths outside the project root', () => {
|
||||
const root = path.resolve('/tmp/funplay-cocos-test-project');
|
||||
assert.throws(
|
||||
() => resolveProjectPath(root, '/tmp/secret.txt'),
|
||||
/outside the Cocos project/
|
||||
);
|
||||
});
|
||||
|
||||
test('isPathInside treats the project root as inside itself', () => {
|
||||
const root = path.resolve('/tmp/funplay-cocos-test-project');
|
||||
assert.equal(isPathInside(root, root), true);
|
||||
});
|
||||
@@ -0,0 +1,128 @@
|
||||
'use strict';
|
||||
|
||||
const assert = require('node:assert/strict');
|
||||
const test = require('node:test');
|
||||
const {
|
||||
MCP_PROTOCOL_VERSION,
|
||||
McpServer,
|
||||
SUPPORTED_PROTOCOL_VERSIONS,
|
||||
} = require('../lib/server');
|
||||
|
||||
function createServer(toolRegistry = {}) {
|
||||
return new McpServer({
|
||||
config: { host: '127.0.0.1', port: 8765 },
|
||||
toolRegistry: {
|
||||
listTools: () => [],
|
||||
callTool: async () => 'ok',
|
||||
...toolRegistry,
|
||||
},
|
||||
resourceProvider: {
|
||||
listResources: () => [],
|
||||
listResourceTemplates: () => [],
|
||||
readResource: async () => ({ contents: [] }),
|
||||
},
|
||||
promptProvider: {
|
||||
listPrompts: () => [],
|
||||
getPrompt: () => ({ messages: [] }),
|
||||
},
|
||||
interactionLog: { add() {} },
|
||||
serverName: 'test-server',
|
||||
serverVersion: '0.0.0-test',
|
||||
});
|
||||
}
|
||||
|
||||
test('initialize negotiates the current MCP protocol version by default', async () => {
|
||||
const server = createServer();
|
||||
const response = await server.handleRpcRequest({
|
||||
jsonrpc: '2.0',
|
||||
id: 1,
|
||||
method: 'initialize',
|
||||
params: {},
|
||||
});
|
||||
|
||||
assert.equal(response.result.protocolVersion, MCP_PROTOCOL_VERSION);
|
||||
assert.equal(response.result.serverInfo.name, 'test-server');
|
||||
});
|
||||
|
||||
test('initialize can negotiate an older supported MCP protocol version', async () => {
|
||||
const server = createServer();
|
||||
const olderVersion = SUPPORTED_PROTOCOL_VERSIONS[SUPPORTED_PROTOCOL_VERSIONS.length - 1];
|
||||
const response = await server.handleRpcRequest({
|
||||
jsonrpc: '2.0',
|
||||
id: 1,
|
||||
method: 'initialize',
|
||||
params: { protocolVersion: olderVersion },
|
||||
});
|
||||
|
||||
assert.equal(response.result.protocolVersion, olderVersion);
|
||||
});
|
||||
|
||||
test('tool execution failures are returned as MCP tool errors', async () => {
|
||||
const server = createServer({
|
||||
callTool: async () => {
|
||||
throw new Error('bad arguments');
|
||||
},
|
||||
});
|
||||
|
||||
const response = await server.handleRpcRequest({
|
||||
jsonrpc: '2.0',
|
||||
id: 2,
|
||||
method: 'tools/call',
|
||||
params: { name: 'example', arguments: {} },
|
||||
});
|
||||
|
||||
assert.equal(response.error, undefined);
|
||||
assert.equal(response.result.isError, true);
|
||||
assert.deepEqual(response.result.content, [{ type: 'text', text: 'bad arguments' }]);
|
||||
});
|
||||
|
||||
test('tool object results include structuredContent', async () => {
|
||||
const value = { ok: true, count: 2 };
|
||||
const server = createServer({
|
||||
callToolDetailed: async () => ({
|
||||
value,
|
||||
text: JSON.stringify(value, null, 2),
|
||||
}),
|
||||
});
|
||||
|
||||
const response = await server.handleRpcRequest({
|
||||
jsonrpc: '2.0',
|
||||
id: 4,
|
||||
method: 'tools/call',
|
||||
params: { name: 'example', arguments: {} },
|
||||
});
|
||||
|
||||
assert.deepEqual(response.result.structuredContent, value);
|
||||
assert.equal(response.result.content[0].type, 'text');
|
||||
});
|
||||
|
||||
test('structuredContent sanitizes circular values', async () => {
|
||||
const value = { ok: true };
|
||||
value.self = value;
|
||||
const server = createServer({
|
||||
callToolDetailed: async () => ({
|
||||
value,
|
||||
text: '{ "ok": true, "self": "[Circular]" }',
|
||||
}),
|
||||
});
|
||||
|
||||
const response = await server.handleRpcRequest({
|
||||
jsonrpc: '2.0',
|
||||
id: 5,
|
||||
method: 'tools/call',
|
||||
params: { name: 'example', arguments: {} },
|
||||
});
|
||||
|
||||
assert.deepEqual(response.result.structuredContent, { ok: true, self: '[Circular]' });
|
||||
});
|
||||
|
||||
test('unsupported protocol version headers are rejected when present after initialize', () => {
|
||||
const server = createServer();
|
||||
const response = server.validateProtocolVersionHeader(
|
||||
{ headers: { 'mcp-protocol-version': '1999-01-01' } },
|
||||
{ jsonrpc: '2.0', id: 3, method: 'tools/list' }
|
||||
);
|
||||
|
||||
assert.equal(response.error.code, -32600);
|
||||
assert.match(response.error.message, /Unsupported MCP protocol version/);
|
||||
});
|
||||
@@ -0,0 +1,46 @@
|
||||
'use strict';
|
||||
|
||||
const assert = require('node:assert/strict');
|
||||
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')) {
|
||||
return createToolRegistry({
|
||||
getRuntimeContext: () => ({
|
||||
config: { toolProfile: profile },
|
||||
projectPath,
|
||||
}),
|
||||
interactionLog: { add() {} },
|
||||
sceneBridge: { call: async () => ({ ok: true }) },
|
||||
editorExecutor: async () => ({ ok: true }),
|
||||
});
|
||||
}
|
||||
|
||||
test('core profile exposes the documented focused tool set', () => {
|
||||
const tools = createRegistry('core').listTools();
|
||||
assert.equal(tools.length, 19);
|
||||
assert.equal(tools.some((tool) => tool.name === 'execute_javascript'), 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, 67);
|
||||
assert.equal(tools.some((tool) => tool.name === 'write_file'), true);
|
||||
});
|
||||
|
||||
test('file tools reject writes outside the project root', async () => {
|
||||
const registry = createRegistry('full');
|
||||
await assert.rejects(
|
||||
() => registry.callTool('write_file', { path: '../outside.txt', content: 'x' }),
|
||||
/outside the Cocos project/
|
||||
);
|
||||
});
|
||||
|
||||
test('callToolDetailed preserves structured values and text output', async () => {
|
||||
const registry = createRegistry('core');
|
||||
const result = await registry.callToolDetailed('get_project_info', {});
|
||||
assert.equal(result.value.projectPath, path.resolve('/tmp/funplay-cocos-test-project'));
|
||||
assert.match(result.text, /projectPath/);
|
||||
});
|
||||
Reference in New Issue
Block a user