test: add phase security/smoke/e2e coverage and tighten MCP contract handling
This commit is contained in:
@@ -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
@@ -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
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user