kindlyguard
Version:
Security-focused MCP server protecting against unicode attacks, injection threats, and other AI vulnerabilities
300 lines (250 loc) • 7.56 kB
JavaScript
/**
* KindlyGuard - Security-focused MCP server for AI protection
*
* Protects against unicode attacks, injection threats, and other AI vulnerabilities
* Visit https://github.com/samduchaine/kindly-guard for more information.
*/
const { spawn, spawnSync } = require('child_process');
const path = require('path');
const fs = require('fs');
const platform = require('./platform');
class KindlyGuard {
constructor(options = {}) {
this.options = {
stdio: true,
config: null,
shield: false,
logLevel: 'info',
timeout: 30000,
...options
};
this.serverProcess = null;
this.binPath = this._findBinary();
}
_findBinary() {
const binaryName = 'kindlyguard';
const binaryPath = platform.getBinaryPath(binaryName);
const validation = platform.validateBinary(binaryPath);
if (!validation.valid) {
throw new Error(
`KindlyGuard binary not found at ${binaryPath}. ` +
`${validation.error}. ` +
`Run 'npm install' to complete installation.`
);
}
return binaryPath;
}
/**
* Start the KindlyGuard MCP server
* @returns {ChildProcess} The spawned server process
*/
start() {
if (this.serverProcess) {
throw new Error('Server is already running');
}
const args = [];
if (this.options.stdio) {
args.push('--stdio');
}
if (this.options.config) {
args.push('--config', this.options.config);
}
if (this.options.shield) {
args.push('--shield');
}
const env = {
...process.env,
RUST_LOG: this.options.logLevel === 'debug' ? 'kindly_guard=debug' : 'kindly_guard=info'
};
this.serverProcess = spawn(this.binPath, args, {
stdio: this.options.stdio ? 'pipe' : 'inherit',
env
});
this.serverProcess.on('error', (error) => {
console.error('Failed to start KindlyGuard server:', error);
this.serverProcess = null;
});
this.serverProcess.on('exit', (code, signal) => {
this.serverProcess = null;
if (code !== 0 && code !== null) {
console.error(`KindlyGuard server exited with code ${code}`);
}
});
// Handle stdio if enabled
if (this.options.stdio && this.serverProcess.stdin && this.serverProcess.stdout) {
process.stdin.pipe(this.serverProcess.stdin);
this.serverProcess.stdout.pipe(process.stdout);
this.serverProcess.stderr.pipe(process.stderr);
}
return this.serverProcess;
}
/**
* Stop the KindlyGuard server gracefully
* @param {number} timeout - Milliseconds to wait before force killing
*/
async stop(timeout = 5000) {
if (!this.serverProcess) {
return;
}
return new Promise((resolve) => {
let timeoutId = null;
this.serverProcess.once('exit', () => {
if (timeoutId) {
clearTimeout(timeoutId);
}
this.serverProcess = null;
resolve();
});
// Try graceful shutdown first
this.serverProcess.kill('SIGTERM');
// Force kill after timeout
timeoutId = setTimeout(() => {
if (this.serverProcess) {
this.serverProcess.kill('SIGKILL');
}
}, timeout);
});
}
/**
* Get server status and statistics
* @returns {Promise<Object>} Status information
*/
async status() {
const result = spawnSync(this.binPath, ['status', '--json'], {
encoding: 'utf8',
timeout: this.options.timeout
});
if (result.error) {
throw result.error;
}
if (result.status !== 0) {
throw new Error(`Status command failed: ${result.stderr || result.stdout}`);
}
try {
return JSON.parse(result.stdout);
} catch (e) {
return {
exitCode: result.status,
stdout: result.stdout,
stderr: result.stderr
};
}
}
/**
* Scan text or file for threats
* @param {string} input - Text to scan or file path
* @param {Object} options - Scan options
* @returns {Promise<Object>} Scan results
*/
async scan(input, options = {}) {
const args = ['scan'];
if (options.file) {
args.push('--file', input);
} else {
args.push('--text', input);
}
if (options.format) {
args.push('--format', options.format);
}
if (options.detailed) {
args.push('--detailed');
}
const result = spawnSync(this.binPath, args, {
encoding: 'utf8',
timeout: this.options.timeout,
maxBuffer: 10 * 1024 * 1024 // 10MB
});
if (result.error) {
throw result.error;
}
if (result.status !== 0 && result.status !== 1) { // Exit code 1 means threats found
throw new Error(`Scan failed: ${result.stderr || result.stdout}`);
}
if (options.format === 'json' && result.stdout) {
try {
// Extract just the JSON part - look for complete JSON object
// The JSON ends at the first } that closes the root object
let braceCount = 0;
let inString = false;
let escapeNext = false;
let jsonEnd = -1;
for (let i = 0; i < result.stdout.length; i++) {
const char = result.stdout[i];
if (escapeNext) {
escapeNext = false;
continue;
}
if (char === '\\') {
escapeNext = true;
continue;
}
if (char === '"' && !escapeNext) {
inString = !inString;
continue;
}
if (!inString) {
if (char === '{') braceCount++;
else if (char === '}') {
braceCount--;
if (braceCount === 0) {
jsonEnd = i + 1;
break;
}
}
}
}
if (jsonEnd > 0) {
const jsonStr = result.stdout.substring(0, jsonEnd);
return JSON.parse(jsonStr);
}
// Fallback to parsing the whole output
return JSON.parse(result.stdout);
} catch (e) {
throw new Error(`Failed to parse scan results: ${e.message}`);
}
}
return {
exitCode: result.status,
threatsFound: result.status === 1,
stdout: result.stdout,
stderr: result.stderr
};
}
/**
* Get binary version information
* @returns {Promise<string>} Version string
*/
async version() {
const result = spawnSync(this.binPath, ['--version'], {
encoding: 'utf8',
timeout: 5000
});
if (result.error) {
throw result.error;
}
return result.stdout.trim();
}
}
// Factory function for creating instances
function createKindlyGuard(options) {
return new KindlyGuard(options);
}
// Quick scan helper
async function scan(input, options) {
const instance = new KindlyGuard();
return instance.scan(input, options);
}
// Start server helper
function startServer(options) {
const instance = new KindlyGuard(options);
return instance.start();
}
// Export both CommonJS and ES module compatible interface
module.exports = createKindlyGuard;
module.exports.KindlyGuard = KindlyGuard;
module.exports.createKindlyGuard = createKindlyGuard;
module.exports.scan = scan;
module.exports.startServer = startServer;
module.exports.default = createKindlyGuard;
// Platform utilities for advanced usage
module.exports.platform = platform;