Skip to content

Plugin Development Guide

Comprehensive guide for developing custom security tools and extensions for RedQuanta MCP.

Plugin Architecture

Plugin Structure

custom-tool-plugin/
├── package.json
├── src/
│   ├── index.ts
│   ├── tool.ts
│   └── types.ts
├── tests/
│   └── tool.test.ts
├── config/
│   └── tool-config.json
└── README.md

Base Plugin Interface

// src/types.ts
export interface PluginManifest {
  name: string;
  version: string;
  description: string;
  author: string;
  license: string;
  tools: ToolDefinition[];
  dependencies?: string[];
  permissions?: Permission[];
}

export interface ToolDefinition {
  name: string;
  description: string;
  inputSchema: JSONSchema;
  outputSchema: JSONSchema;
  execute: (input: any) => Promise<ToolResult>;
  requiresDangerous?: boolean;
  timeout?: number;
}

export interface ToolResult {
  success: boolean;
  tool: string;
  version: string;
  target?: string;
  duration: number;
  exitCode?: number;
  stdout?: string;
  stderr?: string;
  data?: any;
  error?: string;
  metadata?: Record<string, any>;
}

Creating Custom Tools

Basic Tool Implementation

// src/tool.ts
import { BaseTool, ToolExecutionOptions } from '@redquanta/mcp-core';

export interface CustomToolOptions extends ToolExecutionOptions {
  target: string;
  customParam?: string;
  timeout?: number;
}

export class CustomTool extends BaseTool {
  constructor() {
    super('custom-tool', '1.0.0', {
      linux: 'custom-tool',
      darwin: 'custom-tool',
      windows: 'custom-tool.exe'
    });
  }

  async execute(options: CustomToolOptions): Promise<ToolResult> {
    const startTime = Date.now();

    try {
      // Validate input
      this.validateInput(options);

      // Build command arguments
      const args = this.buildArgs(options);

      // Execute tool
      const result = await this.executeCommand(args, {
        timeout: options.timeout || 30000,
        cwd: process.cwd()
      });

      // Process output
      const processedResult = this.processOutput(result.stdout, result.stderr);

      return {
        success: result.exitCode === 0,
        tool: this.getName(),
        version: this.getMinVersion(),
        target: options.target,
        duration: Date.now() - startTime,
        exitCode: result.exitCode,
        stdout: result.stdout,
        stderr: result.stderr,
        data: processedResult,
        metadata: {
          binaryUsed: this.getBinaryName(),
          arguments: args
        }
      };

    } catch (error) {
      return {
        success: false,
        tool: this.getName(),
        version: this.getMinVersion(),
        target: options.target,
        duration: Date.now() - startTime,
        error: error instanceof Error ? error.message : String(error)
      };
    }
  }

  private validateInput(options: CustomToolOptions): void {
    if (!options.target) {
      throw new Error('Target is required');
    }

    // Add custom validation logic
    if (options.customParam && options.customParam.length > 100) {
      throw new Error('Custom parameter too long');
    }
  }

  private buildArgs(options: CustomToolOptions): string[] {
    const args: string[] = [];

    // Add tool-specific arguments
    args.push('--target', options.target);

    if (options.customParam) {
      args.push('--custom', options.customParam);
    }

    return args;
  }

  private processOutput(stdout: string, stderr: string): any {
    // Process and parse tool output
    try {
      if (stdout.includes('JSON:')) {
        const jsonStart = stdout.indexOf('JSON:') + 5;
        return JSON.parse(stdout.substring(jsonStart));
      }

      return {
        rawOutput: stdout,
        errorOutput: stderr
      };
    } catch {
      return { rawOutput: stdout };
    }
  }

  async getVersion(): Promise<string> {
    try {
      const result = await this.executeCommand(['--version']);
      const versionMatch = result.stdout.match(/version (\d+\.\d+\.\d+)/);
      return versionMatch?.[1] || 'unknown';
    } catch {
      return 'unknown';
    }
  }

  async isAvailable(): Promise<boolean> {
    try {
      await this.executeCommand(['--help']);
      return true;
    } catch {
      return false;
    }
  }
}

Plugin Entry Point

// src/index.ts
import { PluginManifest, RedQuantaPlugin } from '@redquanta/mcp-core';
import { CustomTool } from './tool.js';

export class CustomToolPlugin implements RedQuantaPlugin {
  private customTool: CustomTool;

  constructor() {
    this.customTool = new CustomTool();
  }

  getManifest(): PluginManifest {
    return {
      name: 'custom-tool-plugin',
      version: '1.0.0',
      description: 'Custom security tool integration',
      author: 'Security Team',
      license: 'MIT',
      tools: [
        {
          name: 'custom_tool_scan',
          description: 'Custom security scanning tool',
          inputSchema: {
            type: 'object',
            properties: {
              target: {
                type: 'string',
                description: 'Target to scan'
              },
              customParam: {
                type: 'string',
                description: 'Custom parameter',
                default: 'default-value'
              }
            },
            required: ['target']
          },
          outputSchema: {
            type: 'object',
            properties: {
              success: { type: 'boolean' },
              data: { type: 'object' },
              metadata: { type: 'object' }
            }
          },
          execute: this.customTool.execute.bind(this.customTool),
          timeout: 60000
        }
      ],
      permissions: [
        'network.scan',
        'filesystem.read'
      ]
    };
  }

  async initialize(): Promise<void> {
    // Plugin initialization logic
    console.log('Initializing custom tool plugin...');

    // Check tool availability
    const available = await this.customTool.isAvailable();
    if (!available) {
      throw new Error('Custom tool not available on this system');
    }
  }

  async cleanup(): Promise<void> {
    // Plugin cleanup logic
    console.log('Cleaning up custom tool plugin...');
  }
}

// Export plugin instance
export default new CustomToolPlugin();

Advanced Plugin Features

Configuration Management

// src/config.ts
export interface PluginConfig {
  enabled: boolean;
  logLevel: string;
  customSettings: {
    apiKey?: string;
    endpoint?: string;
    timeout: number;
  };
  toolPaths: Record<string, string>;
}

export class ConfigManager {
  private config: PluginConfig;

  constructor(configPath: string) {
    this.config = this.loadConfig(configPath);
  }

  private loadConfig(path: string): PluginConfig {
    try {
      return JSON.parse(fs.readFileSync(path, 'utf-8'));
    } catch (error) {
      return this.getDefaultConfig();
    }
  }

  private getDefaultConfig(): PluginConfig {
    return {
      enabled: true,
      logLevel: 'info',
      customSettings: {
        timeout: 30000
      },
      toolPaths: {}
    };
  }

  get<T>(key: string): T {
    return this.config[key] as T;
  }

  set(key: string, value: any): void {
    this.config[key] = value;
  }
}

State Management

// src/state.ts
export class PluginState {
  private state: Map<string, any> = new Map();
  private persistPath?: string;

  constructor(persistPath?: string) {
    this.persistPath = persistPath;
    this.load();
  }

  set(key: string, value: any): void {
    this.state.set(key, value);
    this.persist();
  }

  get<T>(key: string): T | undefined {
    return this.state.get(key) as T;
  }

  has(key: string): boolean {
    return this.state.has(key);
  }

  delete(key: string): boolean {
    const result = this.state.delete(key);
    this.persist();
    return result;
  }

  clear(): void {
    this.state.clear();
    this.persist();
  }

  private load(): void {
    if (!this.persistPath) return;

    try {
      const data = fs.readFileSync(this.persistPath, 'utf-8');
      const parsed = JSON.parse(data);
      this.state = new Map(Object.entries(parsed));
    } catch {
      // File doesn't exist or is invalid, start with empty state
    }
  }

  private persist(): void {
    if (!this.persistPath) return;

    try {
      const data = Object.fromEntries(this.state);
      fs.writeFileSync(this.persistPath, JSON.stringify(data, null, 2));
    } catch (error) {
      console.error('Failed to persist state:', error);
    }
  }
}

Event System

// src/events.ts
export interface PluginEvent {
  type: string;
  timestamp: Date;
  data: any;
  source: string;
}

export class EventManager {
  private listeners: Map<string, Set<(event: PluginEvent) => void>> = new Map();

  on(eventType: string, listener: (event: PluginEvent) => void): void {
    if (!this.listeners.has(eventType)) {
      this.listeners.set(eventType, new Set());
    }
    this.listeners.get(eventType)!.add(listener);
  }

  off(eventType: string, listener: (event: PluginEvent) => void): void {
    const listeners = this.listeners.get(eventType);
    if (listeners) {
      listeners.delete(listener);
    }
  }

  emit(eventType: string, data: any, source: string = 'unknown'): void {
    const event: PluginEvent = {
      type: eventType,
      timestamp: new Date(),
      data,
      source
    };

    const listeners = this.listeners.get(eventType);
    if (listeners) {
      listeners.forEach(listener => {
        try {
          listener(event);
        } catch (error) {
          console.error(`Error in event listener for ${eventType}:`, error);
        }
      });
    }
  }
}

Web-Based Tools Integration

HTTP API Tool

// src/web-tool.ts
import axios from 'axios';

export interface WebToolOptions {
  endpoint: string;
  method: 'GET' | 'POST' | 'PUT' | 'DELETE';
  headers?: Record<string, string>;
  data?: any;
  timeout?: number;
}

export class WebBasedTool extends BaseTool {
  private httpClient: axios.AxiosInstance;

  constructor() {
    super('web-tool', '1.0.0', {
      linux: 'web-api',
      darwin: 'web-api',
      windows: 'web-api'
    });

    this.httpClient = axios.create({
      timeout: 30000,
      headers: {
        'User-Agent': 'RedQuanta-MCP/1.0.0'
      }
    });
  }

  async execute(options: WebToolOptions): Promise<ToolResult> {
    const startTime = Date.now();

    try {
      const response = await this.httpClient.request({
        url: options.endpoint,
        method: options.method,
        headers: options.headers,
        data: options.data,
        timeout: options.timeout || 30000
      });

      return {
        success: response.status >= 200 && response.status < 300,
        tool: this.getName(),
        version: this.getMinVersion(),
        duration: Date.now() - startTime,
        data: response.data,
        metadata: {
          statusCode: response.status,
          headers: response.headers,
          responseSize: JSON.stringify(response.data).length
        }
      };

    } catch (error) {
      if (axios.isAxiosError(error)) {
        return {
          success: false,
          tool: this.getName(),
          version: this.getMinVersion(),
          duration: Date.now() - startTime,
          error: error.message,
          metadata: {
            statusCode: error.response?.status,
            responseData: error.response?.data
          }
        };
      }

      throw error;
    }
  }

  async getVersion(): Promise<string> {
    return '1.0.0';
  }

  async isAvailable(): Promise<boolean> {
    return true; // Web tools are always "available"
  }
}

Custom Protocol Handler

// src/protocol-handler.ts
export interface ProtocolHandler {
  name: string;
  schemes: string[];
  handle(url: URL, options: any): Promise<ToolResult>;
}

export class CustomProtocolHandler implements ProtocolHandler {
  name = 'custom-protocol';
  schemes = ['custom://'];

  async handle(url: URL, options: any): Promise<ToolResult> {
    const startTime = Date.now();

    try {
      // Parse custom protocol URL
      const { hostname, pathname, searchParams } = url;

      // Implement custom protocol logic
      const result = await this.processCustomRequest(hostname, pathname, searchParams);

      return {
        success: true,
        tool: 'custom-protocol-handler',
        version: '1.0.0',
        duration: Date.now() - startTime,
        data: result
      };

    } catch (error) {
      return {
        success: false,
        tool: 'custom-protocol-handler',
        version: '1.0.0',
        duration: Date.now() - startTime,
        error: error instanceof Error ? error.message : String(error)
      };
    }
  }

  private async processCustomRequest(
    hostname: string, 
    pathname: string, 
    params: URLSearchParams
  ): Promise<any> {
    // Implement custom protocol processing logic
    return {
      hostname,
      pathname,
      parameters: Object.fromEntries(params)
    };
  }
}

Plugin Testing

Unit Testing Framework

// tests/tool.test.ts
import { describe, it, expect, beforeEach, vi } from 'vitest';
import { CustomTool } from '../src/tool.js';

describe('CustomTool', () => {
  let customTool: CustomTool;

  beforeEach(() => {
    customTool = new CustomTool();
  });

  describe('Input Validation', () => {
    it('should require target parameter', async () => {
      const options = {} as any;

      const result = await customTool.execute(options);

      expect(result.success).toBe(false);
      expect(result.error).toContain('Target is required');
    });

    it('should validate custom parameter length', async () => {
      const options = {
        target: 'example.com',
        customParam: 'a'.repeat(101) // Too long
      };

      const result = await customTool.execute(options);

      expect(result.success).toBe(false);
      expect(result.error).toContain('Custom parameter too long');
    });
  });

  describe('Command Execution', () => {
    it('should execute tool successfully', async () => {
      // Mock command execution
      vi.spyOn(customTool as any, 'executeCommand').mockResolvedValue({
        exitCode: 0,
        stdout: 'JSON:{"result": "success"}',
        stderr: ''
      });

      const options = {
        target: 'example.com'
      };

      const result = await customTool.execute(options);

      expect(result.success).toBe(true);
      expect(result.data).toEqual({ result: 'success' });
    });

    it('should handle execution failures', async () => {
      vi.spyOn(customTool as any, 'executeCommand').mockRejectedValue(
        new Error('Command not found')
      );

      const options = {
        target: 'example.com'
      };

      const result = await customTool.execute(options);

      expect(result.success).toBe(false);
      expect(result.error).toBe('Command not found');
    });
  });

  describe('Tool Availability', () => {
    it('should check if tool is available', async () => {
      vi.spyOn(customTool as any, 'executeCommand').mockResolvedValue({
        exitCode: 0,
        stdout: 'Help text',
        stderr: ''
      });

      const available = await customTool.isAvailable();

      expect(available).toBe(true);
    });

    it('should handle unavailable tools', async () => {
      vi.spyOn(customTool as any, 'executeCommand').mockRejectedValue(
        new Error('ENOENT')
      );

      const available = await customTool.isAvailable();

      expect(available).toBe(false);
    });
  });
});

Integration Testing

// tests/integration.test.ts
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
import { TestHarness } from '@redquanta/mcp-testing';
import { CustomToolPlugin } from '../src/index.js';

describe('CustomToolPlugin Integration', () => {
  let testHarness: TestHarness;
  let plugin: CustomToolPlugin;

  beforeAll(async () => {
    testHarness = new TestHarness();
    plugin = new CustomToolPlugin();

    await testHarness.initialize();
    await testHarness.loadPlugin(plugin);
  });

  afterAll(async () => {
    await testHarness.cleanup();
  });

  it('should register plugin successfully', async () => {
    const registeredPlugins = await testHarness.getRegisteredPlugins();

    expect(registeredPlugins).toContain('custom-tool-plugin');
  });

  it('should expose tool endpoints', async () => {
    const availableTools = await testHarness.getAvailableTools();

    expect(availableTools).toContain('custom_tool_scan');
  });

  it('should execute tool via API', async () => {
    const result = await testHarness.executeTool('custom_tool_scan', {
      target: 'example.com'
    });

    expect(result).toHaveProperty('success');
    expect(result).toHaveProperty('tool', 'custom-tool');
  });
});

Plugin Distribution

Package Configuration

{
  "name": "@redquanta/custom-tool-plugin",
  "version": "1.0.0",
  "description": "Custom security tool integration for RedQuanta MCP",
  "main": "dist/index.js",
  "types": "dist/index.d.ts",
  "files": [
    "dist/",
    "config/",
    "README.md"
  ],
  "scripts": {
    "build": "tsc",
    "test": "vitest",
    "lint": "eslint src/",
    "prepublishOnly": "npm run build && npm test"
  },
  "keywords": [
    "redquanta",
    "security",
    "plugin",
    "mcp"
  ],
  "author": "Security Team",
  "license": "MIT",
  "peerDependencies": {
    "@redquanta/mcp-core": "^0.3.0"
  },
  "devDependencies": {
    "@types/node": "^20.0.0",
    "typescript": "^5.0.0",
    "vitest": "^1.0.0"
  }
}

Plugin Registry

// src/registry.ts
export interface PluginRegistry {
  plugins: Map<string, RedQuantaPlugin>;
  register(plugin: RedQuantaPlugin): Promise<void>;
  unregister(name: string): Promise<void>;
  get(name: string): RedQuantaPlugin | undefined;
  list(): string[];
}

export class DefaultPluginRegistry implements PluginRegistry {
  plugins = new Map<string, RedQuantaPlugin>();

  async register(plugin: RedQuantaPlugin): Promise<void> {
    const manifest = plugin.getManifest();

    // Validate plugin
    this.validatePlugin(manifest);

    // Initialize plugin
    await plugin.initialize();

    // Register plugin
    this.plugins.set(manifest.name, plugin);

    console.log(`Plugin ${manifest.name} registered successfully`);
  }

  async unregister(name: string): Promise<void> {
    const plugin = this.plugins.get(name);
    if (plugin) {
      await plugin.cleanup();
      this.plugins.delete(name);
      console.log(`Plugin ${name} unregistered`);
    }
  }

  get(name: string): RedQuantaPlugin | undefined {
    return this.plugins.get(name);
  }

  list(): string[] {
    return Array.from(this.plugins.keys());
  }

  private validatePlugin(manifest: PluginManifest): void {
    if (!manifest.name || !manifest.version) {
      throw new Error('Plugin must have name and version');
    }

    if (this.plugins.has(manifest.name)) {
      throw new Error(`Plugin ${manifest.name} already registered`);
    }
  }
}

Best Practices

Security Considerations

  • Input Validation: Always validate and sanitize inputs
  • Privilege Minimization: Request only necessary permissions
  • Error Handling: Implement comprehensive error handling
  • Resource Management: Clean up resources properly
  • Audit Logging: Log all significant operations

Performance Optimization

  • Async Operations: Use async/await for I/O operations
  • Resource Pooling: Reuse connections and resources
  • Caching: Cache expensive operations
  • Timeouts: Implement reasonable timeouts
  • Memory Management: Avoid memory leaks

Documentation Standards

  • README: Comprehensive installation and usage guide
  • API Documentation: Document all public interfaces
  • Examples: Provide working examples
  • Changelog: Maintain version history
  • Contributing: Guidelines for contributors

Next Steps