mirror of
https://github.com/czlonkowski/n8n-mcp.git
synced 2026-02-06 05:23:08 +00:00
refactor: Improve DISABLED_TOOLS implementation based on code review
Performance Optimization: - Add caching to getDisabledTools() to prevent 3x parsing per request - Cache result as instance property disabledToolsCache - Reduces overhead from 3x to 1x per server instance Security Improvements: - Fix information disclosure in error responses - Only reveal the attempted tool name, not full list of disabled tools - Prevents leaking security configuration details Safety Limits: - Add 10KB maximum length for DISABLED_TOOLS environment variable - Add 200-tool maximum limit to prevent abuse - Include warnings when limits are exceeded Code Quality: - Add clarifying comment for defense-in-depth guard in executeTool() - Change logging level from info to debug for frequent operations - Add comprehensive JSDoc to TestableN8NMCPServer test classes - Document test wrapper pattern and exposed methods Test Updates: - Update test to verify 200-tool safety limit enforcement - All 45 tests passing with improved coverage Conceived by Romuald Członkowski - https://www.aiadvisors.pl/en 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -70,6 +70,7 @@ export class N8NDocumentationMCPServer {
|
|||||||
private previousTool: string | null = null;
|
private previousTool: string | null = null;
|
||||||
private previousToolTimestamp: number = Date.now();
|
private previousToolTimestamp: number = Date.now();
|
||||||
private earlyLogger: EarlyErrorLogger | null = null;
|
private earlyLogger: EarlyErrorLogger | null = null;
|
||||||
|
private disabledToolsCache: Set<string> | null = null;
|
||||||
|
|
||||||
constructor(instanceContext?: InstanceContext, earlyLogger?: EarlyErrorLogger) {
|
constructor(instanceContext?: InstanceContext, earlyLogger?: EarlyErrorLogger) {
|
||||||
this.instanceContext = instanceContext;
|
this.instanceContext = instanceContext;
|
||||||
@@ -327,24 +328,46 @@ export class N8NDocumentationMCPServer {
|
|||||||
* Parse and cache disabled tools from DISABLED_TOOLS environment variable.
|
* Parse and cache disabled tools from DISABLED_TOOLS environment variable.
|
||||||
* Returns a Set of tool names that should be filtered from registration.
|
* Returns a Set of tool names that should be filtered from registration.
|
||||||
*
|
*
|
||||||
|
* Cached after first call since environment variables don't change at runtime.
|
||||||
|
* Includes safety limits: max 10KB env var length, max 200 tools.
|
||||||
|
*
|
||||||
* @returns Set of disabled tool names
|
* @returns Set of disabled tool names
|
||||||
*/
|
*/
|
||||||
private getDisabledTools(): Set<string> {
|
private getDisabledTools(): Set<string> {
|
||||||
const disabledToolsEnv = process.env.DISABLED_TOOLS || '';
|
// Return cached value if available
|
||||||
if (!disabledToolsEnv) {
|
if (this.disabledToolsCache !== null) {
|
||||||
return new Set();
|
return this.disabledToolsCache;
|
||||||
}
|
}
|
||||||
|
|
||||||
const tools = disabledToolsEnv
|
let disabledToolsEnv = process.env.DISABLED_TOOLS || '';
|
||||||
|
if (!disabledToolsEnv) {
|
||||||
|
this.disabledToolsCache = new Set();
|
||||||
|
return this.disabledToolsCache;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Safety limit: prevent abuse with very long environment variables
|
||||||
|
if (disabledToolsEnv.length > 10000) {
|
||||||
|
logger.warn(`DISABLED_TOOLS environment variable too long (${disabledToolsEnv.length} chars), truncating to 10000`);
|
||||||
|
disabledToolsEnv = disabledToolsEnv.substring(0, 10000);
|
||||||
|
}
|
||||||
|
|
||||||
|
let tools = disabledToolsEnv
|
||||||
.split(',')
|
.split(',')
|
||||||
.map(t => t.trim())
|
.map(t => t.trim())
|
||||||
.filter(Boolean);
|
.filter(Boolean);
|
||||||
|
|
||||||
|
// Safety limit: prevent abuse with too many tools
|
||||||
|
if (tools.length > 200) {
|
||||||
|
logger.warn(`DISABLED_TOOLS contains ${tools.length} tools, limiting to first 200`);
|
||||||
|
tools = tools.slice(0, 200);
|
||||||
|
}
|
||||||
|
|
||||||
if (tools.length > 0) {
|
if (tools.length > 0) {
|
||||||
logger.info(`Disabled tools configured: ${tools.join(', ')}`);
|
logger.info(`Disabled tools configured: ${tools.join(', ')}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
return new Set(tools);
|
this.disabledToolsCache = new Set(tools);
|
||||||
|
return this.disabledToolsCache;
|
||||||
}
|
}
|
||||||
|
|
||||||
private setupHandlers(): void {
|
private setupHandlers(): void {
|
||||||
@@ -445,7 +468,7 @@ export class N8NDocumentationMCPServer {
|
|||||||
// Log filtered tools count if any tools are disabled
|
// Log filtered tools count if any tools are disabled
|
||||||
if (disabledTools.size > 0) {
|
if (disabledTools.size > 0) {
|
||||||
const totalAvailableTools = n8nDocumentationToolsFinal.length + (shouldIncludeManagementTools ? n8nManagementTools.length : 0);
|
const totalAvailableTools = n8nDocumentationToolsFinal.length + (shouldIncludeManagementTools ? n8nManagementTools.length : 0);
|
||||||
logger.info(`Filtered ${disabledTools.size} disabled tools, ${tools.length}/${totalAvailableTools} tools available`);
|
logger.debug(`Filtered ${disabledTools.size} disabled tools, ${tools.length}/${totalAvailableTools} tools available`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if client is n8n (from initialization)
|
// Check if client is n8n (from initialization)
|
||||||
@@ -498,7 +521,7 @@ export class N8NDocumentationMCPServer {
|
|||||||
text: JSON.stringify({
|
text: JSON.stringify({
|
||||||
error: 'TOOL_DISABLED',
|
error: 'TOOL_DISABLED',
|
||||||
message: `Tool '${name}' is not available in this deployment. It has been disabled via DISABLED_TOOLS environment variable.`,
|
message: `Tool '${name}' is not available in this deployment. It has been disabled via DISABLED_TOOLS environment variable.`,
|
||||||
disabledTools: Array.from(disabledTools)
|
tool: name
|
||||||
}, null, 2)
|
}, null, 2)
|
||||||
}]
|
}]
|
||||||
};
|
};
|
||||||
@@ -906,7 +929,9 @@ export class N8NDocumentationMCPServer {
|
|||||||
// Ensure args is an object and validate it
|
// Ensure args is an object and validate it
|
||||||
args = args || {};
|
args = args || {};
|
||||||
|
|
||||||
// Safety check for disabled tools (defense in depth)
|
// Defense in depth: This should never be reached since CallToolRequestSchema
|
||||||
|
// handler already checks disabled tools (line 514-528), but we guard here
|
||||||
|
// in case of future refactoring or direct executeTool() calls
|
||||||
const disabledTools = this.getDisabledTools();
|
const disabledTools = this.getDisabledTools();
|
||||||
if (disabledTools.has(name)) {
|
if (disabledTools.has(name)) {
|
||||||
throw new Error(`Tool '${name}' is disabled via DISABLED_TOOLS environment variable`);
|
throw new Error(`Tool '${name}' is disabled via DISABLED_TOOLS environment variable`);
|
||||||
|
|||||||
@@ -7,13 +7,26 @@ vi.mock('../../../src/database/node-repository');
|
|||||||
vi.mock('../../../src/templates/template-service');
|
vi.mock('../../../src/templates/template-service');
|
||||||
vi.mock('../../../src/utils/logger');
|
vi.mock('../../../src/utils/logger');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test wrapper class that exposes private methods for unit testing.
|
||||||
|
* This pattern is preferred over modifying production code visibility
|
||||||
|
* or using reflection-based testing utilities.
|
||||||
|
*/
|
||||||
class TestableN8NMCPServer extends N8NDocumentationMCPServer {
|
class TestableN8NMCPServer extends N8NDocumentationMCPServer {
|
||||||
// Expose the private getDisabledTools method for testing
|
/**
|
||||||
|
* Expose getDisabledTools() for testing environment variable parsing.
|
||||||
|
* @returns Set of disabled tool names from DISABLED_TOOLS env var
|
||||||
|
*/
|
||||||
public testGetDisabledTools(): Set<string> {
|
public testGetDisabledTools(): Set<string> {
|
||||||
return (this as any).getDisabledTools();
|
return (this as any).getDisabledTools();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Expose the private executeTool method for testing
|
/**
|
||||||
|
* Expose executeTool() for testing the defense-in-depth guard.
|
||||||
|
* @param name - Tool name to execute
|
||||||
|
* @param args - Tool arguments
|
||||||
|
* @returns Tool execution result
|
||||||
|
*/
|
||||||
public async testExecuteTool(name: string, args: any): Promise<any> {
|
public async testExecuteTool(name: string, args: any): Promise<any> {
|
||||||
return (this as any).executeTool(name, args);
|
return (this as any).executeTool(name, args);
|
||||||
}
|
}
|
||||||
@@ -198,7 +211,7 @@ describe('Disabled Tools Additional Coverage (Issue #410)', () => {
|
|||||||
expect(duration).toBeLessThan(50); // Should be very fast
|
expect(duration).toBeLessThan(50); // Should be very fast
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should handle 1000 disabled tools efficiently', () => {
|
it('should handle 1000 disabled tools efficiently and enforce 200 tool limit', () => {
|
||||||
const manyTools = Array.from({ length: 1000 }, (_, i) => `tool_${i}`);
|
const manyTools = Array.from({ length: 1000 }, (_, i) => `tool_${i}`);
|
||||||
process.env.DISABLED_TOOLS = manyTools.join(',');
|
process.env.DISABLED_TOOLS = manyTools.join(',');
|
||||||
|
|
||||||
@@ -207,7 +220,8 @@ describe('Disabled Tools Additional Coverage (Issue #410)', () => {
|
|||||||
const disabledTools = server.testGetDisabledTools();
|
const disabledTools = server.testGetDisabledTools();
|
||||||
const duration = Date.now() - start;
|
const duration = Date.now() - start;
|
||||||
|
|
||||||
expect(disabledTools.size).toBe(1000);
|
// Safety limit: max 200 tools enforced
|
||||||
|
expect(disabledTools.size).toBe(200);
|
||||||
expect(duration).toBeLessThan(100); // Should still be fast
|
expect(duration).toBeLessThan(100); // Should still be fast
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -9,13 +9,26 @@ vi.mock('../../../src/database/node-repository');
|
|||||||
vi.mock('../../../src/templates/template-service');
|
vi.mock('../../../src/templates/template-service');
|
||||||
vi.mock('../../../src/utils/logger');
|
vi.mock('../../../src/utils/logger');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test wrapper class that exposes private methods for unit testing.
|
||||||
|
* This pattern is preferred over modifying production code visibility
|
||||||
|
* or using reflection-based testing utilities.
|
||||||
|
*/
|
||||||
class TestableN8NMCPServer extends N8NDocumentationMCPServer {
|
class TestableN8NMCPServer extends N8NDocumentationMCPServer {
|
||||||
// Expose the private getDisabledTools method for testing
|
/**
|
||||||
|
* Expose getDisabledTools() for testing environment variable parsing.
|
||||||
|
* @returns Set of disabled tool names from DISABLED_TOOLS env var
|
||||||
|
*/
|
||||||
public testGetDisabledTools(): Set<string> {
|
public testGetDisabledTools(): Set<string> {
|
||||||
return (this as any).getDisabledTools();
|
return (this as any).getDisabledTools();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Expose the private executeTool method for testing
|
/**
|
||||||
|
* Expose executeTool() for testing the defense-in-depth guard.
|
||||||
|
* @param name - Tool name to execute
|
||||||
|
* @param args - Tool arguments
|
||||||
|
* @returns Tool execution result
|
||||||
|
*/
|
||||||
public async testExecuteTool(name: string, args: any): Promise<any> {
|
public async testExecuteTool(name: string, args: any): Promise<any> {
|
||||||
return (this as any).executeTool(name, args);
|
return (this as any).executeTool(name, args);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user