test: add phase security/smoke/e2e coverage and tighten MCP contract handling

This commit is contained in:
newblacc
2026-03-30 15:52:24 +02:00
parent a605a15282
commit f1db126566
22 changed files with 1986 additions and 136 deletions
+7 -5
View File
@@ -526,17 +526,19 @@ export function listTenants(): Tenant[] {
// ── Projects ──
export function createProject(name: string, description?: string): Project {
export function createProject(name: string, description?: string, tenantId?: string): Project {
const tid = tenantId ?? activeTenantId;
const id = generateId();
const now = new Date().toISOString();
db.prepare(
'INSERT INTO projects (id, name, description, tenant_id, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?)'
).run(id, name, description || null, activeTenantId, now, now);
return { id, name, description: description || null, tenant_id: activeTenantId, created_at: now, updated_at: now };
).run(id, name, description || null, tid, now, now);
return { id, name, description: description || null, tenant_id: tid, created_at: now, updated_at: now };
}
export function listProjects(): Project[] {
return db.prepare('SELECT * FROM projects WHERE tenant_id = ? ORDER BY updated_at DESC').all(activeTenantId) as Project[];
export function listProjects(tenantId?: string): Project[] {
const tid = tenantId ?? activeTenantId;
return db.prepare('SELECT * FROM projects WHERE tenant_id = ? ORDER BY updated_at DESC').all(tid) as Project[];
}
export function getProjectForTenant(projectId: string, tenantId: string): Project | undefined {
+16 -6
View File
@@ -13,7 +13,9 @@ import {
CallToolRequestSchema,
ListToolsRequestSchema,
CallToolRequest,
Tool
Tool,
McpError,
ErrorCode
} from '@modelcontextprotocol/sdk/types.js';
import { z } from 'zod';
import dotenv from 'dotenv';
@@ -41,8 +43,10 @@ import {
getElementHistory as dbGetElementHistory, getProjectHistory as dbGetProjectHistory,
ensureTenant as dbEnsureTenant, setActiveTenant as dbSetActiveTenant,
getActiveTenant as dbGetActiveTenant, getActiveTenantId as dbGetActiveTenantId,
getActiveProjectId as dbGetActiveProjectId,
listTenants as dbListTenants
} from './db.js';
import { assertNoDangerousKeys } from './security.js';
// Load environment variables
dotenv.config();
@@ -1890,8 +1894,10 @@ server.setRequestHandler(CallToolRequestSchema, async (request: CallToolRequest)
const safeImportPath = sanitizeFilePath(params.filePath);
const fileContent = fs.readFileSync(safeImportPath, 'utf-8');
sceneData = JSON.parse(fileContent);
assertNoDangerousKeys(sceneData, 'import_scene filePath');
} else if (params.data) {
sceneData = JSON.parse(params.data);
assertNoDangerousKeys(sceneData, 'import_scene data');
} else {
throw new Error('Either filePath or data must be provided');
}
@@ -2708,7 +2714,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request: CallToolRequest)
const params = z.object({ query: z.string() }).parse(args);
logger.info('Searching elements via MCP', { query: params.query });
const results = dbSearchElements(params.query);
const results = dbSearchElements(params.query, dbGetActiveProjectId());
return {
content: [{
type: 'text',
@@ -2721,7 +2727,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request: CallToolRequest)
case 'list_projects': {
logger.info('Listing projects via MCP');
const projects = dbListProjects();
const projects = dbListProjects(dbGetActiveTenantId());
const active = dbGetActiveProject();
return {
content: [{
@@ -2739,7 +2745,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request: CallToolRequest)
}).parse(args || {});
if (params.createName) {
const newProject = dbCreateProject(params.createName, params.createDescription);
const newProject = dbCreateProject(params.createName, params.createDescription, dbGetActiveTenantId());
dbSetActiveProject(newProject.id);
logger.info('Created and switched to new project', { project: newProject });
return {
@@ -2834,9 +2840,12 @@ server.setRequestHandler(CallToolRequestSchema, async (request: CallToolRequest)
}
default:
throw new Error(`Unknown tool: ${name}`);
throw new McpError(ErrorCode.MethodNotFound, `Unknown tool: ${name}`);
}
} catch (error) {
if (error instanceof McpError) {
throw error;
}
logger.error(`Error handling tool call: ${(error as Error).message}`, { error });
return {
content: [{ type: 'text', text: `Error: ${(error as Error).message}` }],
@@ -3021,4 +3030,5 @@ if (isMainModule()) {
}
}
export default runServer;
export default runServer;
export { server, tools };
+7 -1
View File
@@ -111,6 +111,12 @@ export function sanitizeBody(req: Request, res: Response, next: NextFunction): v
next();
}
export function assertNoDangerousKeys(obj: unknown, context = 'input'): void {
if (hasDangerousKey(obj)) {
throw new Error(`${context} contains disallowed keys (__proto__, constructor, prototype)`);
}
}
// ── Mermaid Input Validation ──────────────────────────────────────────────────
const MAX_MERMAID_LENGTH = 50 * 1024; // 50 KB
const MAX_MERMAID_CONFIG_KEYS = 10;
@@ -205,7 +211,7 @@ export function sanitizeSearchQuery(query: string): string {
if (/\b(?:AND|OR|NOT|NEAR(?:\/\d+)?)\b/i.test(trimmed)) {
throw new InvalidSearchQueryError();
}
if (/[*(){}^]/.test(trimmed)) {
if (/[*(){}^:]/.test(trimmed)) {
throw new InvalidSearchQueryError();
}