You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
This guide documents the tool registration system for adding new MCP tools to CyberChef-MCP. The system provides a modular, extensible architecture for integrating external project functionality.
Architecture
Registration Flow
External Tool Module
|
v
Tool Definition (name, schema, handler)
|
v
ToolRegistry.register()
|
v
Validation (naming, schema, conflicts)
|
v
Category Index Update
|
v
MCP Server Tool List Updated
Core Components
Component
File
Purpose
ToolRegistry
src/node/tools/registry.mjs
Central tool registration and lookup
BaseTool
src/node/tools/base-tool.mjs
Base class for tool implementations
ToolLoader
src/node/tools/loader.mjs
Dynamic tool discovery and loading
MCP Integration
src/node/mcp-server.mjs
Exposes tools via MCP protocol
ToolRegistry Implementation
Registry Class
// src/node/tools/registry.mjsimport{log}from'../logging.mjs';exportclassToolRegistry{
#tools =newMap();
#categories =newMap();
#aliases =newMap();/** * Register a new tool * @param {MCPTool} tool - Tool definition * @throws {Error} If validation fails */register(tool){this.validateTool(tool);// Register main toolthis.#tools.set(tool.name,tool);// Add to category indexthis.#addToCategory(tool);// Register aliases if presentif(tool.aliases){for(constaliasoftool.aliases){this.#aliases.set(alias,tool.name);}}log.info(`Registered tool: ${tool.name} (category: ${tool.category})`);}/** * Unregister a tool * @param {string} name - Tool name */unregister(name){consttool=this.#tools.get(name);if(!tool)return;this.#tools.delete(name);this.#removeFromCategory(tool);// Remove aliasesif(tool.aliases){for(constaliasoftool.aliases){this.#aliases.delete(alias);}}log.info(`Unregistered tool: ${name}`);}/** * Get tool by name or alias * @param {string} name - Tool name or alias * @returns {MCPTool | undefined} */getTool(name){// Check direct matchif(this.#tools.has(name)){returnthis.#tools.get(name);}// Check aliasconstactualName=this.#aliases.get(name);if(actualName){returnthis.#tools.get(actualName);}returnundefined;}/** * Get all registered tools * @returns {MCPTool[]} */getAllTools(){return[...this.#tools.values()];}/** * Get tools by category * @param {string} category * @returns {MCPTool[]} */getByCategory(category){returnthis.#categories.get(category)||[];}/** * Get all categories * @returns {string[]} */getCategories(){return[...this.#categories.keys()];}/** * Check if tool exists * @param {string} name * @returns {boolean} */hasTool(name){returnthis.#tools.has(name)||this.#aliases.has(name);}/** * Get registry statistics * @returns {object} */getStats(){return{totalTools: this.#tools.size,categories: this.#categories.size,aliases: this.#aliases.size,byCategory: Object.fromEntries([...this.#categories.entries()].map(([k,v])=>[k,v.length]))};}/** * Validate tool definition * @param {MCPTool} tool * @throws {Error} If validation fails */validateTool(tool){// Required fieldsif(!tool.name){thrownewError('Tool must have a name');}// Naming conventionif(!tool.name.startsWith('cyberchef_')){thrownewError(`Tool name must start with 'cyberchef_': ${tool.name}`);}// Name format (snake_case)if(!/^cyberchef_[a-z][a-z0-9_]*$/.test(tool.name)){thrownewError(`Invalid tool name format: ${tool.name} (must be snake_case)`);}// Description requiredif(!tool.description){thrownewError(`Tool ${tool.name} must have a description`);}// Execute method requiredif(!tool.execute||typeoftool.execute!=='function'){thrownewError(`Tool ${tool.name} must have an execute method`);}// Input schema requiredif(!tool.inputSchema){thrownewError(`Tool ${tool.name} must have an inputSchema`);}// Check for conflictsif(this.#tools.has(tool.name)){thrownewError(`Tool ${tool.name} is already registered`);}// Check alias conflictsif(tool.aliases){for(constaliasoftool.aliases){if(this.#aliases.has(alias)||this.#tools.has(alias)){thrownewError(`Alias ${alias} conflicts with existing tool/alias`);}}}}/** * Add tool to category index * @private */
#addToCategory(tool){constcategory=tool.category||'general';if(!this.#categories.has(category)){this.#categories.set(category,[]);}this.#categories.get(category).push(tool);}/** * Remove tool from category index * @private */
#removeFromCategory(tool){constcategory=tool.category||'general';constcategoryTools=this.#categories.get(category);if(categoryTools){constidx=categoryTools.findIndex(t=>t.name===tool.name);if(idx>=0){categoryTools.splice(idx,1);}}}}// Singleton instanceexportconsttoolRegistry=newToolRegistry();
BaseTool Class
Base Implementation
// src/node/tools/base-tool.mjs/** * Base class for MCP tools * All external tool integrations should extend this class */exportclassBaseTool{/** * Create a new tool * @param {object} config - Tool configuration */constructor(config){this.name=config.name;this.description=config.description;this.category=config.category||'general';this.inputSchema=config.inputSchema||this.defaultInputSchema();this.timeout=config.timeout||5000;this.version=config.version||'1.0.0';this.aliases=config.aliases||[];}/** * Default input schema (override in subclass) * @returns {object} */defaultInputSchema(){return{type: 'object',properties: {input: {type: 'string',description: 'Input data'}},required: ['input']};}/** * Execute the tool (must override) * @param {object} args - Tool arguments * @returns {Promise<ToolResult>} */asyncexecute(args){thrownewError(`${this.name}: execute() must be implemented`);}/** * Validate input arguments * @param {object} args * @throws {Error} If validation fails */validateArgs(args){// Check required fields from schemaconstrequired=this.inputSchema.required||[];for(constfieldofrequired){if(args[field]===undefined){thrownewError(`Missing required argument: ${field}`);}}// Type validationconstproperties=this.inputSchema.properties||{};for(const[key,spec]ofObject.entries(properties)){if(args[key]!==undefined){this.validateType(key,args[key],spec);}}}/** * Validate argument type * @private */validateType(name,value,spec){consttype=spec.type;if(type==='string'&&typeofvalue!=='string'){thrownewError(`Argument ${name} must be a string`);}if(type==='integer'&&!Number.isInteger(value)){thrownewError(`Argument ${name} must be an integer`);}if(type==='number'&&typeofvalue!=='number'){thrownewError(`Argument ${name} must be a number`);}if(type==='boolean'&&typeofvalue!=='boolean'){thrownewError(`Argument ${name} must be a boolean`);}if(type==='array'&&!Array.isArray(value)){thrownewError(`Argument ${name} must be an array`);}// Enum validationif(spec.enum&&!spec.enum.includes(value)){thrownewError(`Argument ${name} must be one of: ${spec.enum.join(', ')}`);}// Range validationif(spec.minimum!==undefined&&value<spec.minimum){thrownewError(`Argument ${name} must be >= ${spec.minimum}`);}if(spec.maximum!==undefined&&value>spec.maximum){thrownewError(`Argument ${name} must be <= ${spec.maximum}`);}}/** * Format successful result * @param {any} output * @param {object} metadata * @returns {ToolResult} */formatResult(output,metadata={}){return{success: true,
output,metadata: {tool: this.name,version: this.version,executionTime: metadata.executionTime||0,
...metadata}};}/** * Format error result * @param {Error} error * @returns {ToolResult} */formatError(error){return{success: false,error: {code: error.code||'TOOL_ERROR',message: error.message,tool: this.name}};}/** * Execute with timeout wrapper * @param {Function} fn - Function to execute * @param {number} timeout - Timeout in ms * @returns {Promise<any>} */asyncwithTimeout(fn,timeout=this.timeout){returnPromise.race([fn(),newPromise((_,reject)=>setTimeout(()=>reject(newError(`Tool ${this.name} timed out after ${timeout}ms`)),timeout))]);}/** * Convert tool to MCP tool definition * @returns {object} */toMCPDefinition(){return{name: this.name,description: this.description,inputSchema: this.inputSchema};}}
Tool Loader
Dynamic Loading
// src/node/tools/loader.mjsimportfsfrom'fs';importpathfrom'path';import{pathToFileURL}from'url';import{log}from'../logging.mjs';/** * Load external tools from directory * @param {string} toolsDir - Directory containing tool modules * @returns {Promise<MCPTool[]>} */exportasyncfunctionloadExternalTools(toolsDir='./src/node/tools'){consttools=[];constcategories=['pwntools','katana','cryptii','rsactftool','john','recipes'];for(constcategoryofcategories){constcategoryDir=path.join(toolsDir,category);if(!fs.existsSync(categoryDir)){log.debug(`Tool category directory not found: ${categoryDir}`);continue;}try{// Load index.mjs from category directoryconstindexPath=path.join(categoryDir,'index.mjs');if(fs.existsSync(indexPath)){constmoduleUrl=pathToFileURL(indexPath).href;constmodule=awaitimport(moduleUrl);if(module.tools&&Array.isArray(module.tools)){tools.push(...module.tools);log.info(`Loaded ${module.tools.length} tools from ${category}`);}if(module.registerTools&&typeofmodule.registerTools==='function'){constcategoryTools=awaitmodule.registerTools();tools.push(...categoryTools);log.info(`Loaded ${categoryTools.length} tools via registerTools() from ${category}`);}}}catch(error){log.error(`Failed to load tools from ${category}: ${error.message}`);}}returntools;}/** * Load a single tool module * @param {string} modulePath - Path to tool module * @returns {Promise<MCPTool | null>} */exportasyncfunctionloadToolModule(modulePath){try{constmoduleUrl=pathToFileURL(modulePath).href;constmodule=awaitimport(moduleUrl);if(module.default&&module.default.name){returnmodule.default;}if(module.tool&&module.tool.name){returnmodule.tool;}returnnull;}catch(error){log.error(`Failed to load tool module ${modulePath}: ${error.message}`);returnnull;}}
MCP Server Integration
Extending MCP Server
// src/node/mcp-server.mjs (additions)import{toolRegistry}from'./tools/registry.mjs';import{loadExternalTools}from'./tools/loader.mjs';// Initialize external tools on startupasyncfunctioninitializeExternalTools(){constexternalTools=awaitloadExternalTools();for(consttoolofexternalTools){try{toolRegistry.register(tool);}catch(error){log.warn(`Failed to register tool ${tool.name}: ${error.message}`);}}log.info(`External tools initialized: ${toolRegistry.getStats().totalTools} total`);}// Extended tools/list handlerserver.setRequestHandler(ListToolsRequestSchema,async()=>{// Get CyberChef operation toolsconstcyberchefTools=generateCyberChefTools();// Get external toolsconstexternalTools=toolRegistry.getAllTools().map(tool=>tool.toMCPDefinition());return{tools: [...cyberchefTools, ...externalTools]};});// Extended tools/call handlerserver.setRequestHandler(CallToolRequestSchema,async(request)=>{const{ name,arguments: args}=request.params;// Check external tools first (allows overriding)constexternalTool=toolRegistry.getTool(name);if(externalTool){try{conststartTime=Date.now();constresult=awaitexternalTool.execute(args);constexecutionTime=Date.now()-startTime;returnformatMCPResponse({
...result,metadata: { ...result.metadata, executionTime }});}catch(error){returnformatMCPError(error,name);}}// Fall back to CyberChef toolsreturnhandleCyberChefTool(name,args);});// Initialize on startupawaitinitializeExternalTools();
// src/node/tools/example/index.mjsimportmyToolfrom'./my-tool.mjs';importanotherToolfrom'./another-tool.mjs';// Export as arrayexportconsttools=[myTool,anotherTool];// Or export registration functionexportasyncfunctionregisterTools(){return[myTool,anotherTool];}
Naming Conventions
Tool Names
Pattern
Example
Use Case
cyberchef_<category>_<action>
cyberchef_crypto_xor_brute
Category-specific tools
cyberchef_<action>_<target>
cyberchef_detect_encoding
Detection tools
cyberchef_<source>_<action>
cyberchef_pwntools_pack
Source attribution
Categories
Category
Description
Examples
crypto
Cryptographic operations
RSA attacks, cipher detection
encoding
Encoding/decoding
Base variants, multi-layer
binary
Binary manipulation
Pack/unpack, hexdump
analysis
Data analysis
Entropy, frequency
detection
Auto-detection
Encoding, cipher, file type
recipes
Recipe management
List, search, run
Error Handling
Standard Error Codes
Code
Description
When to Use
INVALID_INPUT
Input validation failed
Missing/malformed arguments
TIMEOUT
Operation timed out
Long-running operations
TOOL_ERROR
General tool error
Implementation errors
NOT_FOUND
Resource not found
Recipe not found, etc.
INCOMPATIBLE
Incompatible operation
Node.js API limitations
Error Format
{success: false,error: {code: 'INVALID_INPUT',message: 'Input must be a valid hex string',tool: 'cyberchef_from_hex',details: {field: 'input',received: 'invalid!@#',expected: 'hexadecimal string'}}}
Testing Tools
Test Pattern
// tests/tools/example/my-tool.test.mjsimport{describe,it,expect,beforeAll,afterAll}from'vitest';import{toolRegistry}from'../../../src/node/tools/registry.mjs';importmyToolfrom'../../../src/node/tools/example/my-tool.mjs';describe('MyTool',()=>{beforeAll(()=>{toolRegistry.register(myTool);});afterAll(()=>{toolRegistry.unregister(myTool.name);});describe('registration',()=>{it('should be registered in registry',()=>{expect(toolRegistry.hasTool('cyberchef_my_tool')).toBe(true);});it('should have correct category',()=>{consttool=toolRegistry.getTool('cyberchef_my_tool');expect(tool.category).toBe('example');});});describe('execute()',()=>{it('should process input with default option',async()=>{constresult=awaitmyTool.execute({input: 'hello'});expect(result.success).toBe(true);expect(result.output).toBe('HELLO');});it('should handle option b',async()=>{constresult=awaitmyTool.execute({input: 'HELLO',option: 'b'});expect(result.output).toBe('hello');});it('should fail on missing input',async()=>{constresult=awaitmyTool.execute({});expect(result.success).toBe(false);expect(result.error.code).toBe('TOOL_ERROR');});});});
Best Practices
Do
Extend BaseTool for consistent behavior
Use validateArgs() for input validation
Use withTimeout() for potentially long operations
Return proper formatResult() or formatError() responses
Include meaningful metadata in results
Document inputSchema thoroughly
Add comprehensive tests
Don't
Don't hardcode timeouts (use configurable values)
Don't swallow errors silently
Don't modify global state
Don't use blocking I/O without async
Don't exceed 100KB response size without pagination
Don't use non-standard dependencies without approval