Skip to main content
Glama
portel-dev

NCP - Natural Context Provider

by portel-dev
mcp-wrapper.ts6.31 kB
/** * MCP Wrapper for Clean Console Output * * Creates a wrapper script that redirects MCP server output to logs * while preserving JSON-RPC communication, similar to Claude Desktop. */ import { createWriteStream, WriteStream, existsSync, mkdirSync, readdirSync, statSync, unlinkSync, writeFileSync } from 'fs'; import { join } from 'path'; import { homedir, tmpdir } from 'os'; import { logger } from './logger.js'; export class MCPWrapper { private readonly LOG_DIR: string; private readonly WRAPPER_DIR: string; private readonly MAX_LOG_AGE_DAYS = 7; // Keep logs for 1 week constructor() { this.LOG_DIR = join(homedir(), '.ncp', 'logs'); this.WRAPPER_DIR = join(tmpdir(), 'ncp-wrappers'); this.ensureDirectories(); this.cleanupOldLogs(); } /** * Ensure required directories exist */ private ensureDirectories(): void { if (!existsSync(this.LOG_DIR)) { mkdirSync(this.LOG_DIR, { recursive: true }); } if (!existsSync(this.WRAPPER_DIR)) { mkdirSync(this.WRAPPER_DIR, { recursive: true }); } } /** * Get log file path for current week */ private getLogFilePath(mcpName: string): string { const now = new Date(); const year = now.getFullYear(); const week = this.getWeekNumber(now); return join(this.LOG_DIR, `mcp-${mcpName}-${year}w${week.toString().padStart(2, '0')}.log`); } /** * Get ISO week number */ private getWeekNumber(date: Date): number { const d = new Date(Date.UTC(date.getFullYear(), date.getMonth(), date.getDate())); const dayNum = d.getUTCDay() || 7; d.setUTCDate(d.getUTCDate() + 4 - dayNum); const yearStart = new Date(Date.UTC(d.getUTCFullYear(), 0, 1)); return Math.ceil((((d.getTime() - yearStart.getTime()) / 86400000) + 1) / 7); } /** * Clean up old log files (older than 1 week) */ private cleanupOldLogs(): void { try { if (!existsSync(this.LOG_DIR)) return; const files = readdirSync(this.LOG_DIR); const cutoffTime = Date.now() - (this.MAX_LOG_AGE_DAYS * 24 * 60 * 60 * 1000); for (const file of files) { if (file.startsWith('mcp-') && file.endsWith('.log')) { const filePath = join(this.LOG_DIR, file); const stats = statSync(filePath); if (stats.mtime.getTime() < cutoffTime) { unlinkSync(filePath); logger.debug(`Cleaned up old log file: ${file}`); } } } } catch (error) { logger.error('Failed to cleanup old logs:', error); } } /** * Create a wrapper script that redirects MCP server output to logs */ createWrapper(mcpName: string, command: string, args: string[] = []): { command: string; args: string[] } { const logFile = this.getLogFilePath(mcpName); const wrapperPath = join(this.WRAPPER_DIR, `mcp-${mcpName}-wrapper.js`); // Escape strings for embedding in JavaScript - handles Windows backslashes const escapeForJS = (str: string) => JSON.stringify(str); // Create Node.js wrapper script const wrapperScript = `#!/usr/bin/env node /** * MCP Wrapper for ${mcpName} * Redirects stdout/stderr to logs while preserving JSON-RPC */ const { spawn } = require('child_process'); const fs = require('fs'); // Paths are JSON-encoded to handle Windows backslashes properly const logFile = ${escapeForJS(logFile)}; const command = ${escapeForJS(command)}; const args = ${JSON.stringify(args)}; // Ensure log directory exists const logDir = require('path').dirname(logFile); if (!fs.existsSync(logDir)) { fs.mkdirSync(logDir, { recursive: true }); } // Create log stream const logStream = fs.createWriteStream(logFile, { flags: 'a' }); logStream.write(\`\\n--- MCP \${process.argv[2] || '${mcpName}'} Session Started: \${new Date().toISOString()} ---\\n\`); // Spawn the actual MCP server const child = spawn(command, args, { env: process.env, stdio: ['pipe', 'pipe', 'pipe'] }); // Forward stdin to child (for JSON-RPC requests) process.stdin.pipe(child.stdin); // Handle stdout: Log everything, but forward JSON-RPC to parent child.stdout.on('data', (chunk) => { const text = chunk.toString(); logStream.write(\`[STDOUT] \${text}\`); // Check if this looks like JSON-RPC and forward it text.split('\\n').forEach(line => { line = line.trim(); if (line) { try { const parsed = JSON.parse(line); if (parsed.jsonrpc === '2.0' || (typeof parsed.id !== 'undefined' && (parsed.method || parsed.result || parsed.error))) { // This is JSON-RPC, forward to parent process.stdout.write(line + '\\n'); } } catch (e) { // Not JSON-RPC, just log it logStream.write(\`[NON-JSONRPC] \${line}\\n\`); } } }); }); // Handle stderr: Log everything (these are usually startup messages) child.stderr.on('data', (chunk) => { const text = chunk.toString(); logStream.write(\`[STDERR] \${text}\`); }); // Handle child process events child.on('error', (error) => { logStream.write(\`[ERROR] Process error: \${error.message}\\n\`); process.exit(1); }); child.on('exit', (code, signal) => { logStream.write(\`[EXIT] Process exited with code \${code}, signal \${signal}\\n\`); logStream.write(\`--- MCP Session Ended: \${new Date().toISOString()} ---\\n\\n\`); logStream.end(); process.exit(code || 0); }); // Handle parent process signals process.on('SIGTERM', () => child.kill('SIGTERM')); process.on('SIGINT', () => child.kill('SIGINT')); `; // Write wrapper script writeFileSync(wrapperPath, wrapperScript, { mode: 0o755 }); // Return wrapper command instead of original return { command: 'node', args: [wrapperPath, mcpName] }; } /** * Get current log file path for an MCP (for debugging) */ getLogFile(mcpName: string): string { return this.getLogFilePath(mcpName); } /** * List all current log files */ listLogFiles(): string[] { try { if (!existsSync(this.LOG_DIR)) return []; return readdirSync(this.LOG_DIR) .filter(file => file.startsWith('mcp-') && file.endsWith('.log')) .map(file => join(this.LOG_DIR, file)); } catch { return []; } } } // Singleton instance export const mcpWrapper = new MCPWrapper();

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/portel-dev/ncp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server