🐺 Initial commit - Lupul Augmentat MCP Server

- MCP server cu stdio transport pentru performanță maximă
- Tool-uri pentru file operations, HTTP requests, system commands
- Suport NATS pentru comunicare inter-module
- Configurare nginx cu API key auth și SSL
- Arhitectură modulară și extensibilă

🤖 Generated with Claude Code
This commit is contained in:
Claude (Lupul Augmentat)
2025-10-09 06:24:58 +02:00
commit 475f89af74
59 changed files with 12827 additions and 0 deletions

View File

@@ -0,0 +1,94 @@
#!/usr/bin/env node
import jwt from 'jsonwebtoken';
import { config } from '../config';
// Script pentru generarea token-urilor JWT
const args = process.argv.slice(2);
if (args.length < 1 || args.includes('--help') || args.includes('-h')) {
console.log(`
Usage: generate-token [options]
Options:
--id <user-id> User ID (default: admin)
--permissions <perms> Comma-separated permissions (default: all)
--expires <duration> Token expiration (e.g., 1h, 7d, 30d) (default: 30d)
--secret <secret> JWT secret (default: from config)
Examples:
# Generate admin token with all permissions
npm run generate-token
# Generate limited token
npm run generate-token -- --id user123 --permissions file:read,file:list --expires 1h
Available permissions:
- file:read
- file:write
- file:list
- system:exec
- network:http
`);
process.exit(0);
}
// Parse arguments
const getArg = (name: string, defaultValue: string): string => {
const index = args.indexOf(`--${name}`);
if (index === -1 || index + 1 >= args.length) {
return defaultValue;
}
return args[index + 1] || defaultValue;
};
const userId = getArg('id', 'admin');
const permissionsStr = getArg('permissions', 'file:read,file:write,file:list,system:exec,network:http');
const expires = getArg('expires', '30d');
const secret = getArg('secret', config.security.jwtSecret);
const permissions = permissionsStr.split(',').map(p => p.trim());
// Generate token
const payload = {
id: userId,
permissions,
iat: Math.floor(Date.now() / 1000),
};
const options: jwt.SignOptions = {
expiresIn: expires as any,
};
const token = jwt.sign(payload, secret, options);
console.log('\n🔐 JWT Token Generated:');
console.log('='.repeat(80));
console.log(token);
console.log('='.repeat(80));
console.log('\nToken Details:');
console.log(` User ID: ${userId}`);
console.log(` Permissions: ${permissions.join(', ')}`);
console.log(` Expires: ${expires}`);
console.log('\n📋 Usage Examples:');
console.log('\nHTTP Header:');
console.log(` Authorization: Bearer ${token}`);
console.log('\ncURL:');
console.log(` curl -X POST https://mcp.runningwolf.com/ \\
-H "Authorization: Bearer ${token}" \\
-H "Content-Type: application/json" \\
-d '{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{}}'`);
console.log('\nJavaScript:');
console.log(` fetch('https://mcp.runningwolf.com/', {
method: 'POST',
headers: {
'Authorization': 'Bearer ${token}',
'Content-Type': 'application/json'
},
body: JSON.stringify({
jsonrpc: '2.0',
id: 1,
method: 'tools/list',
params: {}
})
})`);
console.log();

54
src/config.ts Normal file
View File

@@ -0,0 +1,54 @@
import { z } from 'zod';
import dotenv from 'dotenv';
dotenv.config();
const ConfigSchema = z.object({
mcp: z.object({
host: z.string().default('127.0.0.1'),
port: z.number().default(19017),
logLevel: z.enum(['debug', 'info', 'warn', 'error']).default('info'),
}),
nats: z.object({
url: z.string().default('nats://localhost:4222'),
reconnectTimeWait: z.number().default(2000),
maxReconnectAttempts: z.number().default(10),
}),
security: z.object({
jwtSecret: z.string().min(32),
authEnabled: z.boolean().default(true),
}),
modules: z.object({
startupTimeout: z.number().default(5000),
healthCheckInterval: z.number().default(30000),
}),
});
type Config = z.infer<typeof ConfigSchema>;
function loadConfig(): Config {
const config = {
mcp: {
host: process.env.MCP_HOST || '127.0.0.1',
port: parseInt(process.env.MCP_PORT || '19017', 10),
logLevel: process.env.MCP_LOG_LEVEL || 'info',
},
nats: {
url: process.env.NATS_URL || 'nats://localhost:4222',
reconnectTimeWait: parseInt(process.env.NATS_RECONNECT_TIME_WAIT || '2000', 10),
maxReconnectAttempts: parseInt(process.env.NATS_MAX_RECONNECT_ATTEMPTS || '10', 10),
},
security: {
jwtSecret: process.env.JWT_SECRET || 'development-secret-change-in-production-minimum-32-chars',
authEnabled: process.env.AUTH_ENABLED !== 'false',
},
modules: {
startupTimeout: parseInt(process.env.MODULE_STARTUP_TIMEOUT || '5000', 10),
healthCheckInterval: parseInt(process.env.MODULE_HEALTH_CHECK_INTERVAL || '30000', 10),
},
};
return ConfigSchema.parse(config);
}
export const config = loadConfig();

137
src/http-server.ts Normal file
View File

@@ -0,0 +1,137 @@
import express from 'express';
import cors from 'cors';
import { config } from './config';
import { createLogger } from './utils/logger';
import { ToolRegistry } from './registry/ToolRegistry';
import { NatsClient } from './nats/NatsClient';
import { authMiddleware, AuthRequest } from './middleware/auth';
const logger = createLogger('HTTPServer');
async function startHTTPServer() {
const app = express();
app.use(cors());
app.use(express.json());
// Initialize dependencies
const natsClient = new NatsClient();
await natsClient.connect();
const toolRegistry = new ToolRegistry(natsClient);
await toolRegistry.initialize();
// Health check endpoint
app.get('/health', (_req, res) => {
res.send('healthy\n');
});
// Auth status endpoint
app.get('/auth/status', (_req, res) => {
res.json({
authEnabled: config.security.authEnabled,
message: config.security.authEnabled
? 'Authentication is ENABLED. Use Bearer token in Authorization header.'
: 'Authentication is DISABLED.',
});
});
// Generate token endpoint (only in development)
if (process.env.NODE_ENV !== 'production') {
app.post('/auth/token', (req, res) => {
const { userId = 'test-user', permissions } = req.body;
const { generateToken } = require('./middleware/auth');
const defaultPermissions = permissions || [
'file:read',
'file:write',
'system:exec',
'network:http'
];
const token = generateToken(userId, defaultPermissions);
res.json({
token,
userId,
permissions: defaultPermissions,
expiresIn: '24h',
usage: 'Add to Authorization header as: Bearer <token>'
});
});
}
// MCP JSON-RPC endpoint - protected by auth
app.post('/', authMiddleware, async (req: AuthRequest, res) => {
try {
const { method, params, id } = req.body;
logger.debug({ method, params, id }, 'Received JSON-RPC request');
if (method === 'tools/list') {
const tools = await toolRegistry.listTools();
res.json({
jsonrpc: '2.0',
id,
result: {
tools: tools.map((tool) => ({
name: tool.name,
description: tool.description,
inputSchema: tool.inputSchema,
})),
},
});
} else if (method === 'tools/call') {
const result = await toolRegistry.executeTool(
params.name,
params.arguments,
);
res.json({
jsonrpc: '2.0',
id,
result: {
content: [{ type: 'text', text: JSON.stringify(result) }],
},
});
} else {
res.status(400).json({
jsonrpc: '2.0',
id,
error: {
code: -32601,
message: 'Method not found',
},
});
}
} catch (error) {
logger.error({ error }, 'Error processing request');
res.status(500).json({
jsonrpc: '2.0',
id: req.body.id,
error: {
code: -32603,
message: 'Internal error',
data: error instanceof Error ? error.message : 'Unknown error',
},
});
}
});
app.listen(config.mcp.port, config.mcp.host, () => {
logger.info(
{ host: config.mcp.host, port: config.mcp.port },
'HTTP Server started',
);
});
// Graceful shutdown
process.on('SIGTERM', async () => {
logger.info('Shutting down HTTP server');
await natsClient.disconnect();
process.exit(0);
});
}
startHTTPServer().catch((error) => {
logger.error({ error }, 'Failed to start HTTP server');
process.exit(1);
});

101
src/middleware/auth.ts Normal file
View File

@@ -0,0 +1,101 @@
import { Request, Response, NextFunction } from 'express';
import jwt, { Secret } from 'jsonwebtoken';
import { config } from '../config';
import { createLogger } from '../utils/logger';
const logger = createLogger('AuthMiddleware');
export interface AuthRequest extends Request {
user?: {
id: string;
permissions: string[];
};
}
// Get JWT secret with proper typing
function getJwtSecret(): string {
return process.env.JWT_SECRET || 'development-secret-change-in-production-minimum-32-chars';
}
export function authMiddleware(req: AuthRequest, res: Response, next: NextFunction): void {
// Skip auth if disabled
if (!config.security.authEnabled) {
req.user = {
id: 'anonymous',
permissions: ['file:read', 'file:write', 'system:exec', 'network:http'],
};
return next();
}
const authHeader = req.headers.authorization;
if (!authHeader) {
res.status(401).json({
jsonrpc: '2.0',
id: req.body.id || null,
error: {
code: -32001,
message: 'Authorization header required',
},
});
return;
}
const parts = authHeader.split(' ');
if (parts.length !== 2 || parts[0] !== 'Bearer') {
res.status(401).json({
jsonrpc: '2.0',
id: req.body.id || null,
error: {
code: -32002,
message: 'Invalid authorization header format. Use: Bearer <token>',
},
});
return;
}
const token = parts[1];
try {
const secret = getJwtSecret();
const decoded = jwt.verify(token, secret as Secret) as any;
if (typeof decoded === 'object' && decoded !== null && 'id' in decoded) {
req.user = {
id: (decoded as any).id,
permissions: (decoded as any).permissions || [],
};
logger.debug({ userId: req.user.id }, 'User authenticated');
next();
} else {
throw new Error('Invalid token payload');
}
} catch (error) {
logger.warn({ error }, 'JWT verification failed');
res.status(401).json({
jsonrpc: '2.0',
id: req.body.id || null,
error: {
code: -32003,
message: 'Invalid or expired token',
},
});
}
}
// Helper function to generate tokens
export function generateToken(userId: string, permissions: string[] = []): string {
const secret = getJwtSecret();
return jwt.sign(
{
id: userId,
permissions,
},
secret as Secret,
{
expiresIn: '24h',
}
);
}

91
src/nats/NatsClient.ts Normal file
View File

@@ -0,0 +1,91 @@
import { connect, NatsConnection, JSONCodec } from 'nats';
import { config } from '../config';
import { createLogger } from '../utils/logger';
import { ToolRequest, ToolResponse } from '../types';
const logger = createLogger('NatsClient');
export class NatsClient {
private connection?: NatsConnection;
private jsonCodec = JSONCodec();
async connect(): Promise<void> {
try {
this.connection = await connect({
servers: config.nats.url,
reconnectTimeWait: config.nats.reconnectTimeWait,
maxReconnectAttempts: config.nats.maxReconnectAttempts,
name: 'mcp-core',
});
logger.info({ url: config.nats.url }, 'Connected to NATS');
// Setup connection event handlers
(async () => {
for await (const status of this.connection!.status()) {
logger.info({ status: status.type, data: status.data }, 'NATS connection status');
}
})().catch((err) => logger.error({ error: err }, 'NATS status error'));
} catch (error) {
logger.error({ error }, 'Failed to connect to NATS');
throw error;
}
}
async disconnect(): Promise<void> {
if (this.connection) {
await this.connection.drain();
await this.connection.close();
logger.info('Disconnected from NATS');
}
}
async request(subject: string, data: ToolRequest, timeout = 30000): Promise<ToolResponse> {
if (!this.connection) {
throw new Error('NATS not connected');
}
try {
const msg = await this.connection.request(
subject,
this.jsonCodec.encode(data),
{ timeout },
);
return this.jsonCodec.decode(msg.data) as ToolResponse;
} catch (error) {
logger.error({ error, subject }, 'NATS request failed');
throw error;
}
}
async publish(subject: string, data: unknown): Promise<void> {
if (!this.connection) {
throw new Error('NATS not connected');
}
this.connection.publish(subject, this.jsonCodec.encode(data));
}
subscribe(subject: string, callback: (data: unknown) => void): void {
if (!this.connection) {
throw new Error('NATS not connected');
}
const sub = this.connection.subscribe(subject);
(async () => {
for await (const msg of sub) {
try {
const data = this.jsonCodec.decode(msg.data);
callback(data);
} catch (error) {
logger.error({ error, subject }, 'Error processing message');
}
}
})().catch((err) => logger.error({ error: err }, 'Subscription error'));
}
get isConnected(): boolean {
return this.connection?.isClosed() === false;
}
}

View File

@@ -0,0 +1,147 @@
import { spawn, ChildProcess } from 'child_process';
import { createLogger } from '../utils/logger';
import { NatsClient } from '../nats/NatsClient';
import { ToolRegistry } from './ToolRegistry';
import { ModuleConfig } from '../types';
import { config } from '../config';
const logger = createLogger('ModuleManager');
export class ModuleManager {
private modules = new Map<string, ModuleInfo>();
constructor(
private natsClient: NatsClient,
private toolRegistry: ToolRegistry,
) {
// These will be used when implementing module communication
this.natsClient;
this.toolRegistry;
}
async startAll(): Promise<void> {
// Load module configurations
const modulesConfig = await this.loadModuleConfigs();
for (const moduleConfig of modulesConfig) {
try {
await this.startModule(moduleConfig);
} catch (error) {
logger.error({ error, module: moduleConfig.name }, 'Failed to start module');
}
}
}
async stopAll(): Promise<void> {
for (const [name, info] of this.modules) {
try {
await this.stopModule(name, info);
} catch (error) {
logger.error({ error, module: name }, 'Failed to stop module');
}
}
}
private async loadModuleConfigs(): Promise<ModuleConfig[]> {
try {
// For now, return empty array - modules will be added later
return [];
} catch (error) {
logger.warn('No modules configuration found');
return [];
}
}
private async startModule(moduleConfig: ModuleConfig): Promise<void> {
logger.info({ module: moduleConfig.name }, 'Starting module');
const token = this.generateModuleToken(moduleConfig);
const env = {
...process.env,
MODULE_TOKEN: token,
MODULE_NAME: moduleConfig.name,
NATS_URL: config.nats.url,
};
const proc = spawn(moduleConfig.executable, [], {
env,
stdio: ['inherit', 'pipe', 'pipe'],
});
const info: ModuleInfo = {
config: moduleConfig,
process: proc,
status: 'starting',
startTime: Date.now(),
};
this.modules.set(moduleConfig.name, info);
// Setup process handlers
proc.stdout?.on('data', (data) => {
logger.debug({ module: moduleConfig.name, output: data.toString() }, 'Module output');
});
proc.stderr?.on('data', (data) => {
logger.error({ module: moduleConfig.name, error: data.toString() }, 'Module error');
});
proc.on('exit', (code) => {
logger.warn({ module: moduleConfig.name, code }, 'Module exited');
info.status = 'stopped';
});
// Wait for module to be ready
await this.waitForModuleReady(moduleConfig.name, moduleConfig.startupTimeout);
}
private async stopModule(name: string, info: ModuleInfo): Promise<void> {
logger.info({ module: name }, 'Stopping module');
info.process.kill('SIGTERM');
// Wait for graceful shutdown
await new Promise<void>((resolve) => {
const timeout = setTimeout(() => {
info.process.kill('SIGKILL');
resolve();
}, 5000);
info.process.once('exit', () => {
clearTimeout(timeout);
resolve();
});
});
this.modules.delete(name);
}
private async waitForModuleReady(name: string, timeout = 5000): Promise<void> {
const startTime = Date.now();
while (Date.now() - startTime < timeout) {
const info = this.modules.get(name);
if (info?.status === 'ready') {
return;
}
await new Promise((resolve) => setTimeout(resolve, 100));
}
throw new Error(`Module ${name} failed to start within timeout`);
}
private generateModuleToken(moduleConfig: ModuleConfig): string {
// For now, return a simple token - will implement JWT later
return `module-token-${moduleConfig.name}`;
}
}
interface ModuleInfo {
config: ModuleConfig;
process: ChildProcess;
status: 'starting' | 'ready' | 'stopped';
startTime: number;
}

View File

@@ -0,0 +1,107 @@
import { createLogger } from '../utils/logger';
import { NatsClient } from '../nats/NatsClient';
import { ToolDefinition, ToolRequest } from '../types';
import { randomUUID } from 'crypto';
import { createBuiltinTools, ToolHandler, ToolContext } from '../tools';
const logger = createLogger('ToolRegistry');
export class ToolRegistry {
private tools = new Map<string, ToolDefinition>();
private builtinHandlers = createBuiltinTools();
constructor(private natsClient: NatsClient) {
this.registerBuiltinTools();
}
async initialize(): Promise<void> {
this.setupDiscovery();
}
private setupDiscovery(): void {
// Listen for tool announcements
this.natsClient.subscribe('tools.discovery', (data) => {
const announcement = data as {
module: string;
tools: ToolDefinition[];
};
for (const tool of announcement.tools) {
this.registerTool(tool);
}
});
}
registerTool(tool: ToolDefinition): void {
this.tools.set(tool.name, tool);
logger.info({ tool: tool.name, module: tool.module }, 'Tool registered');
}
async listTools(): Promise<ToolDefinition[]> {
return Array.from(this.tools.values());
}
async executeTool(toolName: string, params: unknown): Promise<unknown> {
// Check if it's a built-in tool
const builtinHandler = this.builtinHandlers.get(toolName);
if (builtinHandler) {
return this.executeBuiltinTool(builtinHandler, params);
}
// Otherwise, execute via NATS
const tool = this.tools.get(toolName);
if (!tool) {
throw new Error(`Tool not found: ${toolName}`);
}
const request: ToolRequest = {
id: randomUUID(),
tool: toolName,
method: 'execute',
params,
timeout: 30000,
metadata: {
timestamp: Date.now(),
},
};
const subject = `tools.${tool.module}.${toolName}.execute`;
logger.debug({ subject, request }, 'Executing tool');
const response = await this.natsClient.request(subject, request);
if (response.status === 'error' && response.error) {
throw new Error(response.error.message);
}
return response.data;
}
private async executeBuiltinTool(handler: ToolHandler, params: unknown): Promise<unknown> {
const context: ToolContext = {
requestId: randomUUID(),
permissions: ['file:read', 'file:write', 'system:exec', 'network:http'], // TODO: get from auth
};
return handler.execute(params, context);
}
getTool(name: string): ToolDefinition | undefined {
return this.tools.get(name);
}
private registerBuiltinTools(): void {
for (const [name, handler] of this.builtinHandlers) {
const tool: ToolDefinition = {
name: handler.name,
description: handler.description,
inputSchema: handler.schema,
module: 'builtin',
permissions: [],
};
this.tools.set(name, tool);
logger.info({ tool: name }, 'Registered built-in tool');
}
}
}

180
src/server.ts Normal file
View File

@@ -0,0 +1,180 @@
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import { z } from 'zod';
import { config } from './config';
import { createLogger } from './utils/logger';
import { ToolRegistry } from './registry/ToolRegistry';
import { NatsClient } from './nats/NatsClient';
import { ModuleManager } from './registry/ModuleManager';
import { HttpServerTransport } from './transport/HttpServerTransport';
const logger = createLogger('MCPServer');
export class MCPServer {
private server: Server;
private httpServer?: Server;
private natsClient: NatsClient;
private toolRegistry: ToolRegistry;
private moduleManager: ModuleManager;
constructor() {
this.server = new Server(
{
name: 'lupul-augmentat',
version: '0.1.0',
},
{
capabilities: {
tools: {},
},
},
);
this.natsClient = new NatsClient();
this.toolRegistry = new ToolRegistry(this.natsClient);
this.moduleManager = new ModuleManager(this.natsClient, this.toolRegistry);
}
async start(): Promise<void> {
try {
logger.info({ config: config.mcp }, 'Starting MCP Server');
// Connect to NATS
await this.natsClient.connect();
logger.info('Connected to NATS');
// Initialize tool registry after NATS connection
await this.toolRegistry.initialize();
logger.info('Tool registry initialized');
// Start module manager
await this.moduleManager.startAll();
logger.info('Modules started');
// Setup MCP handlers
this.setupHandlers();
// Start both transports
await this.startTransports();
logger.info(
{ host: config.mcp.host, port: config.mcp.port },
'MCP Server started successfully',
);
// Setup graceful shutdown
this.setupGracefulShutdown();
} catch (error) {
logger.error({ error }, 'Failed to start MCP Server');
process.exit(1);
}
}
private setupHandlers(server?: Server): void {
const targetServer = server || this.server;
// List available tools
const ListToolsSchema = z.object({
method: z.literal('tools/list'),
});
targetServer.setRequestHandler(ListToolsSchema, async () => {
const tools = await this.toolRegistry.listTools();
return {
tools: tools.map((tool) => ({
name: tool.name,
description: tool.description,
inputSchema: tool.inputSchema,
})),
};
});
// Execute tool
const CallToolSchema = z.object({
method: z.literal('tools/call'),
params: z.object({
name: z.string(),
arguments: z.unknown().optional(),
}),
});
targetServer.setRequestHandler(CallToolSchema, async (request) => {
try {
const result = await this.toolRegistry.executeTool(
request.params.name,
request.params.arguments,
);
return { content: [{ type: 'text', text: JSON.stringify(result) }] };
} catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
return {
content: [{ type: 'text', text: `Error: ${errorMessage}` }],
isError: true,
};
}
});
}
private async startTransports(): Promise<void> {
// Check if running in stdio mode (default for Claude Desktop)
const isStdio = !process.env.MCP_TRANSPORT || process.env.MCP_TRANSPORT === 'stdio';
if (isStdio) {
// Start stdio transport
const transport = new StdioServerTransport();
await this.server.connect(transport);
logger.info('Started with stdio transport');
} else {
// Start HTTP transport
const httpTransport = new HttpServerTransport(config.mcp.host, config.mcp.port);
await this.server.connect(httpTransport);
logger.info({ host: config.mcp.host, port: config.mcp.port }, 'Started with HTTP transport');
// Also create HTTP server instance for non-MCP endpoints
this.httpServer = new Server(
{
name: 'lupul-augmentat-http',
version: '0.1.0',
},
{
capabilities: {
tools: {},
},
},
);
// Setup handlers for HTTP server
this.setupHandlers(this.httpServer);
}
}
private setupGracefulShutdown(): void {
const shutdown = async (signal: string): Promise<void> => {
logger.info({ signal }, 'Shutting down gracefully');
try {
await this.moduleManager.stopAll();
await this.natsClient.disconnect();
await this.server.close();
if (this.httpServer) {
await this.httpServer.close();
}
logger.info('Shutdown complete');
process.exit(0);
} catch (error) {
logger.error({ error }, 'Error during shutdown');
process.exit(1);
}
};
process.on('SIGTERM', () => void shutdown('SIGTERM'));
process.on('SIGINT', () => void shutdown('SIGINT'));
}
}
// Main entry point
if (require.main === module) {
const server = new MCPServer();
void server.start();
}

View File

@@ -0,0 +1,121 @@
import { z, ZodSchema } from 'zod';
import { zodToJsonSchema } from 'zod-to-json-schema';
import { createLogger } from '../../utils/logger';
export interface ToolHandlerOptions {
name: string;
description: string;
version?: string;
timeout?: number;
}
export interface ToolContext {
requestId: string;
userId?: string;
sessionId?: string;
permissions: string[];
}
export abstract class ToolHandler<TInput = unknown, TOutput = unknown> {
protected logger;
constructor(
protected options: ToolHandlerOptions,
protected inputSchema: ZodSchema<TInput>,
protected outputSchema?: ZodSchema<TOutput>,
) {
this.logger = createLogger(`Tool:${options.name}`);
}
get name(): string {
return this.options.name;
}
get description(): string {
return this.options.description;
}
get schema(): Record<string, unknown> {
return zodToJsonSchema(this.inputSchema) as Record<string, unknown>;
}
async execute(input: unknown, context: ToolContext): Promise<TOutput> {
try {
// Lifecycle: validate
const validatedInput = await this.validate(input);
// Lifecycle: checkPermissions
await this.checkPermissions(context);
// Lifecycle: beforeExecute
await this.beforeExecute(validatedInput, context);
// Lifecycle: handle with timeout
const timeout = this.options.timeout || 30000;
const result = await this.withTimeout(
this.handle(validatedInput, context),
timeout,
);
// Lifecycle: afterExecute
const finalResult = await this.afterExecute(result, context);
// Validate output if schema provided
if (this.outputSchema) {
return this.outputSchema.parse(finalResult);
}
return finalResult as TOutput;
} catch (error) {
// Lifecycle: onError
await this.onError(error as Error, context);
throw error;
} finally {
// Lifecycle: cleanup
await this.cleanup(context);
}
}
protected async validate(input: unknown): Promise<TInput> {
try {
return this.inputSchema.parse(input);
} catch (error) {
if (error instanceof z.ZodError) {
throw new Error(`Validation error: ${error.errors.map(e => e.message).join(', ')}`);
}
throw error;
}
}
protected async checkPermissions(_context: ToolContext): Promise<void> {
// Override in subclass if permission checking needed
}
protected async beforeExecute(_input: TInput, _context: ToolContext): Promise<void> {
// Override in subclass for pre-execution logic
}
protected abstract handle(input: TInput, context: ToolContext): Promise<TOutput>;
protected async afterExecute(result: TOutput, _context: ToolContext): Promise<TOutput> {
// Override in subclass for post-execution logic
return result;
}
protected async onError(error: Error, context: ToolContext): Promise<void> {
this.logger.error({ error, context }, 'Tool execution failed');
}
protected async cleanup(_context: ToolContext): Promise<void> {
// Override in subclass for cleanup logic
}
private async withTimeout<T>(promise: Promise<T>, timeout: number): Promise<T> {
return Promise.race([
promise,
new Promise<T>((_, reject) =>
setTimeout(() => reject(new Error(`Tool execution timed out after ${timeout}ms`)), timeout),
),
]);
}
}

View File

@@ -0,0 +1,141 @@
import { z } from 'zod';
import { promises as fs } from 'fs';
import * as path from 'path';
import * as os from 'os';
import { ToolHandler, ToolContext } from '../base/ToolHandler';
const FileListInputSchema = z.object({
path: z.string().min(1, 'Path is required'),
recursive: z.boolean().default(false).optional(),
includeHidden: z.boolean().default(false).optional(),
pattern: z.string().optional(),
});
const FileEntrySchema = z.object({
name: z.string(),
path: z.string(),
type: z.enum(['file', 'directory', 'symlink', 'other']),
size: z.number(),
modified: z.string(),
});
const FileListOutputSchema = z.object({
path: z.string(),
entries: z.array(FileEntrySchema),
totalSize: z.number(),
});
type FileListInput = z.infer<typeof FileListInputSchema>;
type FileListOutput = z.infer<typeof FileListOutputSchema>;
export class FileListTool extends ToolHandler<FileListInput, FileListOutput> {
constructor() {
super(
{
name: 'file_list',
description: 'List files in a directory',
timeout: 10000,
},
FileListInputSchema,
FileListOutputSchema,
);
}
protected override async checkPermissions(context: ToolContext): Promise<void> {
if (!context.permissions.includes('file:read')) {
throw new Error('Permission denied: file:read required');
}
}
protected override async handle(input: FileListInput, _context: ToolContext): Promise<FileListOutput> {
const dirPath = path.resolve(input.path);
// Security: prevent directory traversal by checking if resolved path is within allowed directories
const cwd = process.cwd();
const tmpDir = os.tmpdir();
const allowedPaths = [cwd, tmpDir];
if (!allowedPaths.some(allowed => dirPath.startsWith(allowed))) {
throw new Error('Invalid path: directory traversal not allowed');
}
try {
const stats = await fs.stat(dirPath);
if (!stats.isDirectory()) {
throw new Error('Path is not a directory');
}
const entries = await this.listDirectory(dirPath, input);
const totalSize = entries.reduce((sum, entry) => sum + entry.size, 0);
this.logger.info({ path: dirPath, count: entries.length }, 'Directory listed successfully');
return {
path: dirPath,
entries,
totalSize,
};
} catch (error) {
if ((error as NodeJS.ErrnoException).code === 'ENOENT') {
throw new Error(`Directory not found: ${input.path}`);
}
throw error;
}
}
private async listDirectory(dirPath: string, options: FileListInput): Promise<Array<z.infer<typeof FileEntrySchema>>> {
const entries: Array<z.infer<typeof FileEntrySchema>> = [];
const items = await fs.readdir(dirPath);
for (const item of items) {
// Skip hidden files if not requested
if (!options.includeHidden && item.startsWith('.')) {
continue;
}
// Apply pattern filter if provided
if (options.pattern && !this.matchPattern(item, options.pattern)) {
continue;
}
const itemPath = path.join(dirPath, item);
try {
const stats = await fs.stat(itemPath);
entries.push({
name: item,
path: itemPath,
type: this.getFileType(stats),
size: stats.size,
modified: stats.mtime.toISOString(),
});
// Recursive listing
if (options.recursive && stats.isDirectory()) {
const subEntries = await this.listDirectory(itemPath, options);
entries.push(...subEntries);
}
} catch (error) {
// Skip items we can't access
this.logger.debug({ path: itemPath, error }, 'Skipping inaccessible item');
}
}
return entries;
}
private getFileType(stats: any): 'file' | 'directory' | 'symlink' | 'other' {
if (stats.isFile()) return 'file';
if (stats.isDirectory()) return 'directory';
if (stats.isSymbolicLink()) return 'symlink';
return 'other';
}
private matchPattern(name: string, pattern: string): boolean {
// Simple glob pattern matching (just * for now)
const regex = new RegExp('^' + pattern.replace(/\*/g, '.*') + '$');
return regex.test(name);
}
}

View File

@@ -0,0 +1,80 @@
import { z } from 'zod';
import { promises as fs } from 'fs';
import * as path from 'path';
import * as os from 'os';
import { ToolHandler, ToolContext } from '../base/ToolHandler';
const FileReadInputSchema = z.object({
path: z.string().min(1, 'Path is required'),
encoding: z.enum(['utf8', 'binary']).default('utf8').optional(),
});
const FileReadOutputSchema = z.object({
content: z.string(),
size: z.number(),
path: z.string(),
});
type FileReadInput = z.infer<typeof FileReadInputSchema>;
type FileReadOutput = z.infer<typeof FileReadOutputSchema>;
export class FileReadTool extends ToolHandler<FileReadInput, FileReadOutput> {
constructor() {
super(
{
name: 'file_read',
description: 'Read contents of a file',
timeout: 5000,
},
FileReadInputSchema,
FileReadOutputSchema,
);
}
protected override async checkPermissions(context: ToolContext): Promise<void> {
if (!context.permissions.includes('file:read')) {
throw new Error('Permission denied: file:read required');
}
}
protected override async handle(input: FileReadInput, _context: ToolContext): Promise<FileReadOutput> {
// Get absolute path
const filePath = path.resolve(input.path);
// Security: prevent directory traversal by checking if resolved path is within allowed directories
const cwd = process.cwd();
const tmpDir = os.tmpdir();
const allowedPaths = [cwd, tmpDir];
if (!allowedPaths.some(allowed => filePath.startsWith(allowed))) {
throw new Error('Invalid path: directory traversal not allowed');
}
try {
const stats = await fs.stat(filePath);
if (!stats.isFile()) {
throw new Error('Path is not a file');
}
if (stats.size > 10 * 1024 * 1024) { // 10MB limit
throw new Error('File too large (max 10MB)');
}
const content = await fs.readFile(filePath, input.encoding || 'utf8');
this.logger.info({ path: filePath, size: stats.size }, 'File read successfully');
return {
content: content.toString(),
size: stats.size,
path: filePath,
};
} catch (error) {
if ((error as NodeJS.ErrnoException).code === 'ENOENT') {
throw new Error(`File not found: ${input.path}`);
}
throw error;
}
}
}

View File

@@ -0,0 +1,98 @@
import { z } from 'zod';
import { promises as fs } from 'fs';
import * as path from 'path';
import * as os from 'os';
import { ToolHandler, ToolContext } from '../base/ToolHandler';
const FileWriteInputSchema = z.object({
path: z.string().min(1, 'Path is required'),
content: z.string(),
encoding: z.enum(['utf8', 'binary', 'base64']).default('utf8').optional(),
mode: z.enum(['overwrite', 'append']).default('overwrite').optional(),
});
const FileWriteOutputSchema = z.object({
path: z.string(),
size: z.number(),
mode: z.string(),
});
type FileWriteInput = z.infer<typeof FileWriteInputSchema>;
type FileWriteOutput = z.infer<typeof FileWriteOutputSchema>;
export class FileWriteTool extends ToolHandler<FileWriteInput, FileWriteOutput> {
constructor() {
super(
{
name: 'file_write',
description: 'Write content to a file',
timeout: 5000,
},
FileWriteInputSchema,
FileWriteOutputSchema,
);
}
protected override async checkPermissions(context: ToolContext): Promise<void> {
if (!context.permissions.includes('file:write')) {
throw new Error('Permission denied: file:write required');
}
}
protected override async handle(input: FileWriteInput, _context: ToolContext): Promise<FileWriteOutput> {
const filePath = path.resolve(input.path);
// Security: prevent directory traversal by checking if resolved path is within allowed directories
const cwd = process.cwd();
const tmpDir = os.tmpdir();
const allowedPaths = [cwd, tmpDir];
if (!allowedPaths.some(allowed => filePath.startsWith(allowed))) {
throw new Error('Invalid path: directory traversal not allowed');
}
// Security: prevent writing to system directories
const restrictedPaths = ['/etc', '/sys', '/proc', '/dev'];
if (restrictedPaths.some(restricted => filePath.startsWith(restricted))) {
throw new Error('Cannot write to system directories');
}
try {
// Ensure directory exists
const dir = path.dirname(filePath);
await fs.mkdir(dir, { recursive: true });
// Prepare content based on encoding
let contentToWrite: string | Buffer = input.content;
let writeEncoding: BufferEncoding | undefined = (input.encoding || 'utf8') as BufferEncoding;
if (input.encoding === 'base64') {
contentToWrite = Buffer.from(input.content, 'base64');
writeEncoding = undefined;
}
// Write file
if (input.mode === 'append') {
await fs.appendFile(filePath, contentToWrite, writeEncoding);
} else {
await fs.writeFile(filePath, contentToWrite, writeEncoding);
}
// Get file size
const stats = await fs.stat(filePath);
this.logger.info({ path: filePath, size: stats.size, mode: input.mode }, 'File written successfully');
return {
path: filePath,
size: stats.size,
mode: input.mode || 'overwrite',
};
} catch (error) {
if ((error as NodeJS.ErrnoException).code === 'EACCES') {
throw new Error(`Permission denied: ${input.path}`);
}
throw error;
}
}
}

View File

@@ -0,0 +1,173 @@
import { z } from 'zod';
import { ToolHandler, ToolContext } from '../base/ToolHandler';
const HttpRequestInputSchema = z.object({
url: z.string().url('Invalid URL'),
method: z.enum(['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'HEAD', 'OPTIONS']).default('GET').optional(),
headers: z.record(z.string()).optional(),
body: z.union([z.string(), z.record(z.any())]).optional(),
timeout: z.number().min(100).max(60000).default(30000).optional(),
followRedirects: z.boolean().default(true).optional(),
});
const HttpResponseSchema = z.object({
status: z.number(),
statusText: z.string(),
headers: z.record(z.string()),
body: z.any(),
duration: z.number(),
});
type HttpRequestInput = z.infer<typeof HttpRequestInputSchema>;
type HttpResponse = z.infer<typeof HttpResponseSchema>;
export class HttpRequestTool extends ToolHandler<HttpRequestInput, HttpResponse> {
constructor() {
super(
{
name: 'http_request',
description: 'Make HTTP requests',
timeout: 60000,
},
HttpRequestInputSchema as any, // Type mismatch due to default values
HttpResponseSchema,
);
}
protected override async checkPermissions(context: ToolContext): Promise<void> {
if (!context.permissions.includes('network:http')) {
throw new Error('Permission denied: network:http required');
}
}
protected override async validate(input: unknown): Promise<HttpRequestInput> {
const validated = await super.validate(input);
// Security: prevent requests to internal IPs
const url = new URL(validated.url);
if (this.isInternalUrl(url)) {
throw new Error('Requests to internal networks are not allowed');
}
return validated;
}
protected override async handle(input: HttpRequestInput, _context: ToolContext): Promise<HttpResponse> {
const startTime = Date.now();
const controller = new AbortController();
// Setup timeout
const timeoutId = setTimeout(() => controller.abort(), input.timeout || 30000);
try {
const options: RequestInit = {
method: input.method || 'GET',
headers: this.prepareHeaders(input.headers),
signal: controller.signal,
redirect: input.followRedirects ? 'follow' : 'manual',
};
// Add body if needed
if (input.body && ['POST', 'PUT', 'PATCH'].includes(input.method || 'GET')) {
if (typeof input.body === 'object') {
options.body = JSON.stringify(input.body);
options.headers = {
...options.headers,
'Content-Type': 'application/json',
};
} else {
options.body = input.body;
}
}
const response = await fetch(input.url, options);
// Parse response
const contentType = response.headers.get('content-type') || '';
let body: any;
if (contentType.includes('application/json')) {
body = await response.json();
} else if (contentType.includes('text/')) {
body = await response.text();
} else {
// For binary data, return base64
const buffer = await response.arrayBuffer();
body = Buffer.from(buffer).toString('base64');
}
const duration = Date.now() - startTime;
this.logger.info({
url: input.url,
method: input.method,
status: response.status,
duration
}, 'HTTP request completed');
return {
status: response.status,
statusText: response.statusText,
headers: this.headersToObject(response.headers),
body,
duration,
};
} catch (error) {
if (error instanceof Error) {
if (error.name === 'AbortError') {
throw new Error(`Request timeout after ${input.timeout}ms`);
}
throw new Error(`HTTP request failed: ${error.message}`);
}
throw error;
} finally {
clearTimeout(timeoutId);
}
}
private prepareHeaders(headers?: Record<string, string>): Record<string, string> {
const defaultHeaders = {
'User-Agent': 'MCP-Server/1.0',
};
return {
...defaultHeaders,
...headers,
};
}
private headersToObject(headers: Headers): Record<string, string> {
const obj: Record<string, string> = {};
headers.forEach((value, key) => {
obj[key] = value;
});
return obj;
}
private isInternalUrl(url: URL): boolean {
const hostname = url.hostname;
// Check for localhost and local IPs
if (hostname === 'localhost' || hostname === '127.0.0.1' || hostname === '::1') {
return true;
}
// Check for private IP ranges
const parts = hostname.split('.');
if (parts.length === 4) {
const first = parseInt(parts[0]!, 10);
const second = parseInt(parts[1]!, 10);
// 10.0.0.0/8
if (first === 10) return true;
// 172.16.0.0/12
if (first === 172 && second >= 16 && second <= 31) return true;
// 192.168.0.0/16
if (first === 192 && second === 168) return true;
}
return false;
}
}

View File

@@ -0,0 +1,149 @@
import { z } from 'zod';
import { spawn } from 'child_process';
import { ToolHandler, ToolContext } from '../base/ToolHandler';
const SystemCommandInputSchema = z.object({
command: z.string().min(1, 'Command is required'),
args: z.array(z.string()).default([]).optional(),
cwd: z.string().optional(),
env: z.record(z.string()).optional(),
timeout: z.number().min(100).max(300000).default(30000).optional(),
stdin: z.string().optional(),
});
const SystemCommandOutputSchema = z.object({
stdout: z.string(),
stderr: z.string(),
exitCode: z.number(),
duration: z.number(),
});
type SystemCommandInput = z.infer<typeof SystemCommandInputSchema>;
type SystemCommandOutput = z.infer<typeof SystemCommandOutputSchema>;
export class SystemCommandTool extends ToolHandler<SystemCommandInput, SystemCommandOutput> {
private allowedCommands = new Set([
'ls', 'cat', 'grep', 'find', 'echo', 'pwd', 'date',
'curl', 'wget', 'git', 'npm', 'node', 'python', 'pip',
'docker', 'kubectl', 'terraform',
]);
constructor() {
super(
{
name: 'system_command',
description: 'Execute system commands',
timeout: 60000,
},
SystemCommandInputSchema,
SystemCommandOutputSchema,
);
}
protected override async checkPermissions(context: ToolContext): Promise<void> {
if (!context.permissions.includes('system:exec')) {
throw new Error('Permission denied: system:exec required');
}
}
protected override async validate(input: unknown): Promise<SystemCommandInput> {
const validated = await super.validate(input);
// Security: check if command is allowed
if (!this.allowedCommands.has(validated.command)) {
throw new Error(`Command not allowed: ${validated.command}`);
}
// Security: prevent shell injection
if (this.containsShellCharacters(validated.command) ||
validated.args?.some(arg => this.containsShellCharacters(arg))) {
throw new Error('Shell characters not allowed in commands');
}
return validated;
}
protected override async handle(input: SystemCommandInput, _context: ToolContext): Promise<SystemCommandOutput> {
const startTime = Date.now();
return new Promise((resolve, reject) => {
const proc = spawn(input.command, input.args || [], {
cwd: input.cwd,
env: { ...process.env, ...input.env },
timeout: input.timeout,
});
let stdout = '';
let stderr = '';
let killed = false;
// Limit output size
const maxOutputSize = 1024 * 1024; // 1MB
proc.stdout.on('data', (data) => {
stdout += data.toString();
if (stdout.length > maxOutputSize) {
killed = true;
proc.kill();
reject(new Error('Output size exceeded limit'));
}
});
proc.stderr.on('data', (data) => {
stderr += data.toString();
if (stderr.length > maxOutputSize) {
killed = true;
proc.kill();
reject(new Error('Error output size exceeded limit'));
}
});
// Send stdin if provided
if (input.stdin) {
proc.stdin.write(input.stdin);
proc.stdin.end();
}
proc.on('error', (error) => {
reject(new Error(`Failed to execute command: ${error.message}`));
});
proc.on('close', (code) => {
if (killed) return;
const duration = Date.now() - startTime;
this.logger.info({
command: input.command,
args: input.args,
exitCode: code || 0,
duration
}, 'Command executed');
resolve({
stdout,
stderr,
exitCode: code || 0,
duration,
});
});
// Handle timeout
if (input.timeout) {
setTimeout(() => {
if (!killed) {
killed = true;
proc.kill();
reject(new Error(`Command timed out after ${input.timeout}ms`));
}
}, input.timeout);
}
});
}
private containsShellCharacters(str: string): boolean {
// Check for common shell injection characters
const dangerousChars = /[;&|`$<>(){}\[\]\\]/;
return dangerousChars.test(str);
}
}

27
src/tools/index.ts Normal file
View File

@@ -0,0 +1,27 @@
import { ToolHandler } from './base/ToolHandler';
import { FileReadTool } from './builtin/FileReadTool';
import { FileWriteTool } from './builtin/FileWriteTool';
import { FileListTool } from './builtin/FileListTool';
import { SystemCommandTool } from './builtin/SystemCommandTool';
import { HttpRequestTool } from './builtin/HttpRequestTool';
export * from './base/ToolHandler';
// Registry of all built-in tools
export const builtinTools: ToolHandler[] = [
new FileReadTool(),
new FileWriteTool(),
new FileListTool(),
new SystemCommandTool(),
new HttpRequestTool(),
];
export function createBuiltinTools(): Map<string, ToolHandler> {
const tools = new Map<string, ToolHandler>();
for (const tool of builtinTools) {
tools.set(tool.name, tool);
}
return tools;
}

View File

@@ -0,0 +1,172 @@
import { Transport } from '@modelcontextprotocol/sdk/shared/transport.js';
import { JSONRPCMessage } from '@modelcontextprotocol/sdk/types.js';
import * as http from 'http';
import * as https from 'https';
import jwt from 'jsonwebtoken';
import { createLogger } from '../utils/logger';
import { config } from '../config';
const logger = createLogger('HttpServerTransport');
export class HttpServerTransport implements Transport {
private server?: http.Server | https.Server;
private connections: Set<http.ServerResponse> = new Set();
onclose?: () => void;
onerror?: (error: Error) => void;
onmessage?: (message: JSONRPCMessage) => void;
constructor(
private host: string,
private port: number,
private options?: https.ServerOptions
) {}
async start(): Promise<void> {
return new Promise((resolve, reject) => {
const requestHandler = async (req: http.IncomingMessage, res: http.ServerResponse) => {
// CORS headers
res.setHeader('Access-Control-Allow-Origin', '*');
res.setHeader('Access-Control-Allow-Methods', 'POST, OPTIONS');
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
if (req.method === 'OPTIONS') {
res.writeHead(200);
res.end();
return;
}
if (req.method !== 'POST') {
res.writeHead(405);
res.end('Method Not Allowed');
return;
}
// Check authentication if enabled
if (config.security.authEnabled) {
const authHeader = req.headers.authorization;
if (!authHeader || !authHeader.startsWith('Bearer ')) {
res.writeHead(401, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({
jsonrpc: '2.0',
error: { code: -32001, message: 'Unauthorized: Missing or invalid authorization header' },
id: null
}));
return;
}
const token = authHeader.substring(7);
try {
const decoded = jwt.verify(token, config.security.jwtSecret) as any;
// Token is valid, continue processing
logger.debug({ userId: decoded.id }, 'Authenticated request');
} catch (error) {
res.writeHead(401, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({
jsonrpc: '2.0',
error: { code: -32001, message: 'Unauthorized: Invalid token' },
id: null
}));
return;
}
}
let body = '';
req.on('data', chunk => body += chunk);
req.on('end', async () => {
try {
const message = JSON.parse(body) as JSONRPCMessage;
logger.debug({ message }, 'Received JSON-RPC message');
// Store response for sending reply
this.connections.add(res);
// Pass message to handler
if (this.onmessage) {
this.onmessage(message);
}
// Wait for response (simplified - in production would use message correlation)
setTimeout(() => {
if (!res.headersSent) {
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end('{}');
}
this.connections.delete(res);
}, 30000); // 30 second timeout
} catch (error) {
logger.error({ error }, 'Failed to parse JSON-RPC message');
res.writeHead(400);
res.end('Invalid JSON-RPC message');
this.connections.delete(res);
}
});
};
if (this.options) {
this.server = https.createServer(this.options, requestHandler);
} else {
this.server = http.createServer(requestHandler);
}
this.server.listen(this.port, this.host, () => {
logger.info({ host: this.host, port: this.port }, 'HTTP transport started');
resolve();
});
this.server.on('error', (error) => {
logger.error({ error }, 'Server error');
if (this.onerror) {
this.onerror(error);
}
reject(error);
});
});
}
async send(message: JSONRPCMessage): Promise<void> {
// Send response to the most recent connection that matches the message ID
const messageStr = JSON.stringify(message);
for (const res of this.connections) {
if (!res.headersSent) {
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(messageStr);
this.connections.delete(res);
logger.debug({ message }, 'Sent JSON-RPC response');
return;
}
}
logger.warn({ message }, 'No active connection to send response');
}
async close(): Promise<void> {
return new Promise((resolve) => {
if (this.server) {
// Close all active connections
for (const res of this.connections) {
if (!res.headersSent) {
res.writeHead(503);
res.end('Server shutting down');
}
}
this.connections.clear();
this.server.close(() => {
logger.info('HTTP transport closed');
if (this.onclose) {
this.onclose();
}
resolve();
});
} else {
resolve();
}
});
}
}

47
src/types/index.ts Normal file
View File

@@ -0,0 +1,47 @@
export interface ToolRequest {
id: string;
tool: string;
method: 'execute' | 'describe' | 'validate';
params: unknown;
timeout: number;
metadata: {
user?: string;
session?: string;
timestamp: number;
};
}
export interface ToolResponse {
id: string;
status: 'success' | 'error';
data?: unknown;
error?: {
code: string;
message: string;
details?: unknown;
};
duration: number;
}
export interface ToolDefinition {
name: string;
description: string;
inputSchema: Record<string, unknown>;
module: string;
permissions: string[];
}
export interface ModuleConfig {
name: string;
language: string;
executable: string;
tools: string[];
startupTimeout?: number;
}
export interface ModuleToken {
module_id: string;
allowed_tools: string[];
permissions: string[];
expires_at: number;
}

18
src/utils/logger.ts Normal file
View File

@@ -0,0 +1,18 @@
import pino from 'pino';
import { config } from '../config';
export const logger = pino({
level: config.mcp.logLevel,
transport: {
target: 'pino-pretty',
options: {
colorize: true,
translateTime: 'HH:MM:ss Z',
ignore: 'pid,hostname',
},
},
});
export function createLogger(name: string): pino.Logger {
return logger.child({ component: name });
}