Files
Oleg Maslov 0c3e2ead3b
Some checks failed
Installer Smoke / installer-smoke (push) Has been cancelled
moving
2026-09-02 10:10:29 +02:00

137 lines
4.8 KiB
TypeScript

import { describe, expect, it } from 'vitest';
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import {
parseScopes,
isFullAccess,
isToolAllowed,
scopeGatedServer,
READ_TOOLS,
WRITE_TOOLS,
} from '../src/scope.js';
import { registerMemoryTools } from '../src/tools/memory.js';
import { registerKnowledgeTools } from '../src/tools/knowledge.js';
import { registerIdentityTools } from '../src/tools/identity.js';
import { registerAwarenessTools } from '../src/tools/awareness.js';
import { registerWorkspaceTools } from '../src/tools/workspace.js';
import { registerHarvestTools } from '../src/tools/harvest.js';
import { registerCleanupTools } from '../src/tools/cleanup.js';
import { registerIngestTools } from '../src/tools/ingest.js';
import { registerWikiTools } from '../src/tools/wiki.js';
function makeStub(): { server: McpServer; names: () => string[] } {
const tools: string[] = [];
const server = {
tool: (name: string) => { tools.push(name); },
resource: () => { /* read-only resources, not gated */ },
} as unknown as McpServer;
return { server, names: () => tools.slice().sort() };
}
function registerAll(server: McpServer): void {
registerMemoryTools(server);
registerKnowledgeTools(server);
registerIdentityTools(server);
registerAwarenessTools(server);
registerWorkspaceTools(server);
registerHarvestTools(server);
registerCleanupTools(server);
registerIngestTools(server);
registerWikiTools(server);
}
describe('parseScopes', () => {
it('defaults to full read+write when unset', () => {
const s = parseScopes(undefined);
expect(s.has('memory:read')).toBe(true);
expect(s.has('memory:write')).toBe(true);
expect(isFullAccess(s)).toBe(true);
});
it('defaults to full read+write when empty/whitespace', () => {
expect(isFullAccess(parseScopes(''))).toBe(true);
expect(isFullAccess(parseScopes(' '))).toBe(true);
});
it('read-only scope grants read but not write', () => {
const s = parseScopes('memory:read');
expect(s.has('memory:read')).toBe(true);
expect(s.has('memory:write')).toBe(false);
expect(isFullAccess(s)).toBe(false);
});
it('write scope implies read (write-implies-read)', () => {
const s = parseScopes('memory:write');
expect(s.has('memory:read')).toBe(true);
expect(s.has('memory:write')).toBe(true);
});
it('comma list with both scopes parses to full (order/space tolerant)', () => {
expect(isFullAccess(parseScopes('memory:read,memory:write'))).toBe(true);
expect(isFullAccess(parseScopes(' memory:write , memory:read '))).toBe(true);
});
it('explicit-but-unrecognized value falls closed to read-only', () => {
const s = parseScopes('memory:bogus');
expect(s.has('memory:read')).toBe(true);
expect(s.has('memory:write')).toBe(false);
});
});
describe('isToolAllowed', () => {
const ro = parseScopes('memory:read');
const rw = parseScopes('memory:write');
it('read-only allows every read tool', () => {
for (const name of READ_TOOLS) expect(isToolAllowed(name, ro)).toBe(true);
});
it('read-only denies every write tool', () => {
for (const name of WRITE_TOOLS) expect(isToolAllowed(name, ro)).toBe(false);
});
it('write scope allows both read and write tools', () => {
for (const name of [...READ_TOOLS, ...WRITE_TOOLS]) {
expect(isToolAllowed(name, rw)).toBe(true);
}
});
it('unknown tool names fail safe (treated as write)', () => {
expect(isToolAllowed('totally_new_tool', ro)).toBe(false);
expect(isToolAllowed('totally_new_tool', rw)).toBe(true);
});
});
describe('scopeGatedServer registration gating', () => {
it('read-only scope registers only the 9 read tools — never save/cleanup/ingest', () => {
const { server, names } = makeStub();
const gated = scopeGatedServer(server, parseScopes('memory:read'));
registerAll(gated);
expect(names()).toEqual([...READ_TOOLS].sort());
expect(names()).not.toContain('save_memory');
expect(names()).not.toContain('cleanup_frames');
expect(names()).not.toContain('cleanup_entities');
expect(names()).not.toContain('ingest_source');
expect(names()).toHaveLength(9);
});
it('write scope (implies read) registers all 21 tools', () => {
const { server, names } = makeStub();
const gated = scopeGatedServer(server, parseScopes('memory:write'));
registerAll(gated);
expect(names()).toHaveLength(21);
});
it('default (unset) is unchanged — proxy skipped, all 21 tools register directly', () => {
const { server, names } = makeStub();
const scopes = parseScopes(undefined);
// mirror index.ts: full access skips the proxy entirely
const targetServer = isFullAccess(scopes) ? server : scopeGatedServer(server, scopes);
expect(targetServer).toBe(server);
registerAll(targetServer);
expect(names()).toHaveLength(21);
});
});