#
tokens: 44876/50000 10/67 files (page 2/2)
lines: on (toggle) GitHub
raw markdown copy reset
This is page 2 of 2. Use http://codebase.md/cyanheads/filesystem-mcp-server?lines=true&page={x} to view the full context.

# Directory Structure

```
├── .clinerules
├── .dockerignore
├── .github
│   └── workflows
│       └── publish.yml
├── .gitignore
├── CHANGELOG.md
├── Dockerfile
├── docs
│   └── tree.md
├── LICENSE
├── mcp.json
├── package-lock.json
├── package.json
├── README.md
├── repomix.config.json
├── scripts
│   ├── clean.ts
│   └── tree.ts
├── smithery.yaml
├── src
│   ├── config
│   │   └── index.ts
│   ├── index.ts
│   ├── mcp-server
│   │   ├── server.ts
│   │   ├── state.ts
│   │   ├── tools
│   │   │   ├── copyPath
│   │   │   │   ├── copyPathLogic.ts
│   │   │   │   ├── index.ts
│   │   │   │   └── registration.ts
│   │   │   ├── createDirectory
│   │   │   │   ├── createDirectoryLogic.ts
│   │   │   │   ├── index.ts
│   │   │   │   └── registration.ts
│   │   │   ├── deleteDirectory
│   │   │   │   ├── deleteDirectoryLogic.ts
│   │   │   │   ├── index.ts
│   │   │   │   └── registration.ts
│   │   │   ├── deleteFile
│   │   │   │   ├── deleteFileLogic.ts
│   │   │   │   ├── index.ts
│   │   │   │   └── registration.ts
│   │   │   ├── listFiles
│   │   │   │   ├── index.ts
│   │   │   │   ├── listFilesLogic.ts
│   │   │   │   └── registration.ts
│   │   │   ├── movePath
│   │   │   │   ├── index.ts
│   │   │   │   ├── movePathLogic.ts
│   │   │   │   └── registration.ts
│   │   │   ├── readFile
│   │   │   │   ├── index.ts
│   │   │   │   ├── readFileLogic.ts
│   │   │   │   └── registration.ts
│   │   │   ├── setFilesystemDefault
│   │   │   │   ├── index.ts
│   │   │   │   ├── registration.ts
│   │   │   │   └── setFilesystemDefaultLogic.ts
│   │   │   ├── updateFile
│   │   │   │   ├── index.ts
│   │   │   │   ├── registration.ts
│   │   │   │   └── updateFileLogic.ts
│   │   │   └── writeFile
│   │   │       ├── index.ts
│   │   │       ├── registration.ts
│   │   │       └── writeFileLogic.ts
│   │   └── transports
│   │       ├── authentication
│   │       │   └── authMiddleware.ts
│   │       ├── httpTransport.ts
│   │       └── stdioTransport.ts
│   ├── types-global
│   │   ├── errors.ts
│   │   ├── mcp.ts
│   │   └── tool.ts
│   └── utils
│       ├── index.ts
│       ├── internal
│       │   ├── errorHandler.ts
│       │   ├── index.ts
│       │   ├── logger.ts
│       │   └── requestContext.ts
│       ├── metrics
│       │   ├── index.ts
│       │   └── tokenCounter.ts
│       ├── parsing
│       │   ├── dateParser.ts
│       │   ├── index.ts
│       │   └── jsonParser.ts
│       └── security
│           ├── idGenerator.ts
│           ├── index.ts
│           ├── rateLimiter.ts
│           └── sanitization.ts
└── tsconfig.json
```

# Files

--------------------------------------------------------------------------------
/src/mcp-server/transports/authentication/authMiddleware.ts:
--------------------------------------------------------------------------------

```typescript
  1 | /**
  2 |  * @fileoverview MCP Authentication Middleware for Bearer Token Validation (JWT).
  3 |  *
  4 |  * This middleware validates JSON Web Tokens (JWT) passed via the 'Authorization' header
  5 |  * using the 'Bearer' scheme (e.g., "Authorization: Bearer <your_token>").
  6 |  * It verifies the token's signature and expiration using the secret key defined
  7 |  * in the configuration (`config.mcpAuthSecretKey`).
  8 |  *
  9 |  * If the token is valid, an object conforming to the MCP SDK's `AuthInfo` type
 10 |  * (expected to contain `token`, `clientId`, and `scopes`) is attached to `req.auth`.
 11 |  * If the token is missing, invalid, or expired, it sends an HTTP 401 Unauthorized response.
 12 |  *
 13 |  * @see {@link https://github.com/modelcontextprotocol/modelcontextprotocol/blob/main/docs/specification/2025-03-26/basic/authorization.mdx | MCP Authorization Specification}
 14 |  * @module src/mcp-server/transports/authentication/authMiddleware
 15 |  */
 16 | 
 17 | import { AuthInfo } from "@modelcontextprotocol/sdk/server/auth/types.js"; // Import from SDK
 18 | import { NextFunction, Request, Response } from "express";
 19 | import jwt from "jsonwebtoken";
 20 | import { config, environment } from "../../../config/index.js";
 21 | import { logger, requestContextService } from "../../../utils/index.js";
 22 | 
 23 | // Extend the Express Request interface to include the optional 'auth' property
 24 | // using the imported AuthInfo type from the SDK.
 25 | declare global {
 26 |   // eslint-disable-next-line @typescript-eslint/no-namespace
 27 |   namespace Express {
 28 |     interface Request {
 29 |       /** Authentication information derived from the JWT, conforming to MCP SDK's AuthInfo. */
 30 |       auth?: AuthInfo;
 31 |     }
 32 |   }
 33 | }
 34 | 
 35 | // Startup Validation: Validate secret key presence on module load.
 36 | if (environment === "production" && !config.mcpAuthSecretKey) {
 37 |   const error = new Error(
 38 |     "CRITICAL: MCP_AUTH_SECRET_KEY must be set in production environment for JWT authentication."
 39 |   );
 40 |   logger.fatal(
 41 |     "CRITICAL: MCP_AUTH_SECRET_KEY is not set in production environment. Authentication cannot proceed securely.",
 42 |   );
 43 |   // Force process exit in production to prevent insecure startup
 44 |   process.exit(1);
 45 | } else if (!config.mcpAuthSecretKey) {
 46 |   logger.warning(
 47 |     "MCP_AUTH_SECRET_KEY is not set. Authentication middleware will bypass checks (DEVELOPMENT ONLY). This is insecure for production.",
 48 |   );
 49 | }
 50 | 
 51 | /**
 52 |  * Express middleware for verifying JWT Bearer token authentication.
 53 |  */
 54 | export function mcpAuthMiddleware(
 55 |   req: Request,
 56 |   res: Response,
 57 |   next: NextFunction,
 58 | ): void {
 59 |   const context = requestContextService.createRequestContext({
 60 |     operation: "mcpAuthMiddleware",
 61 |     method: req.method,
 62 |     path: req.path,
 63 |   });
 64 |   logger.debug(
 65 |     "Running MCP Authentication Middleware (Bearer Token Validation)...",
 66 |     context,
 67 |   );
 68 | 
 69 |   // Development Mode Bypass
 70 |   if (!config.mcpAuthSecretKey) {
 71 |     if (environment !== "production") {
 72 |       logger.warning(
 73 |         "Bypassing JWT authentication: MCP_AUTH_SECRET_KEY is not set (DEVELOPMENT ONLY).",
 74 |         context,
 75 |       );
 76 |       // Populate req.auth strictly according to SDK's AuthInfo
 77 |       req.auth = {
 78 |         token: "dev-mode-placeholder-token",
 79 |         clientId: "dev-client-id",
 80 |         scopes: ["dev-scope"],
 81 |       };
 82 |       // Log dev mode details separately, not attaching to req.auth if not part of AuthInfo
 83 |       logger.debug("Dev mode auth object created.", {
 84 |         ...context,
 85 |         authDetails: req.auth,
 86 |       });
 87 |       return next();
 88 |     } else {
 89 |       logger.error(
 90 |         "FATAL: MCP_AUTH_SECRET_KEY is missing in production. Cannot bypass auth.",
 91 |         context,
 92 |       );
 93 |       res.status(500).json({
 94 |         error: "Server configuration error: Authentication key missing.",
 95 |       });
 96 |       return;
 97 |     }
 98 |   }
 99 | 
100 |   const authHeader = req.headers.authorization;
101 |   if (!authHeader || !authHeader.startsWith("Bearer ")) {
102 |     logger.warning(
103 |       "Authentication failed: Missing or malformed Authorization header (Bearer scheme required).",
104 |       context,
105 |     );
106 |     res.status(401).json({
107 |       error: "Unauthorized: Missing or invalid authentication token format.",
108 |     });
109 |     return;
110 |   }
111 | 
112 |   const tokenParts = authHeader.split(" ");
113 |   if (tokenParts.length !== 2 || tokenParts[0] !== "Bearer" || !tokenParts[1]) {
114 |     logger.warning("Authentication failed: Malformed Bearer token.", context);
115 |     res
116 |       .status(401)
117 |       .json({ error: "Unauthorized: Malformed authentication token." });
118 |     return;
119 |   }
120 |   const rawToken = tokenParts[1];
121 | 
122 |   try {
123 |     const decoded = jwt.verify(rawToken, config.mcpAuthSecretKey);
124 | 
125 |     if (typeof decoded === "string") {
126 |       logger.warning(
127 |         "Authentication failed: JWT decoded to a string, expected an object payload.",
128 |         context,
129 |       );
130 |       res
131 |         .status(401)
132 |         .json({ error: "Unauthorized: Invalid token payload format." });
133 |       return;
134 |     }
135 | 
136 |     // Extract and validate fields for SDK's AuthInfo
137 |     const clientIdFromToken =
138 |       typeof decoded.cid === "string"
139 |         ? decoded.cid
140 |         : typeof decoded.client_id === "string"
141 |           ? decoded.client_id
142 |           : undefined;
143 |     if (!clientIdFromToken) {
144 |       logger.warning(
145 |         "Authentication failed: JWT 'cid' or 'client_id' claim is missing or not a string.",
146 |         { ...context, jwtPayloadKeys: Object.keys(decoded) },
147 |       );
148 |       res.status(401).json({
149 |         error: "Unauthorized: Invalid token, missing client identifier.",
150 |       });
151 |       return;
152 |     }
153 | 
154 |     let scopesFromToken: string[];
155 |     if (
156 |       Array.isArray(decoded.scp) &&
157 |       decoded.scp.every((s: unknown) => typeof s === "string")
158 |     ) {
159 |       scopesFromToken = decoded.scp as string[];
160 |     } else if (
161 |       typeof decoded.scope === "string" &&
162 |       decoded.scope.trim() !== ""
163 |     ) {
164 |       scopesFromToken = decoded.scope.split(" ").filter((s: string) => s);
165 |       if (scopesFromToken.length === 0 && decoded.scope.trim() !== "") {
166 |         // handles case " " -> [""]
167 |         scopesFromToken = [decoded.scope.trim()];
168 |       } else if (scopesFromToken.length === 0 && decoded.scope.trim() === "") {
169 |         // If scope is an empty string, treat as no scopes rather than erroring, or use a default.
170 |         // Depending on strictness, could also error here. For now, allow empty array if scope was empty string.
171 |         logger.debug(
172 |           "JWT 'scope' claim was an empty string, resulting in empty scopes array.",
173 |           context,
174 |         );
175 |       }
176 |     } else {
177 |       // If scopes are strictly mandatory and not found or invalid format
178 |       logger.warning(
179 |         "Authentication failed: JWT 'scp' or 'scope' claim is missing, not an array of strings, or not a valid space-separated string. Assigning default empty array.",
180 |         { ...context, jwtPayloadKeys: Object.keys(decoded) },
181 |       );
182 |       scopesFromToken = []; // Default to empty array if scopes are mandatory but not found/invalid
183 |       // Or, if truly mandatory and must be non-empty:
184 |       // res.status(401).json({ error: "Unauthorized: Invalid token, missing or invalid scopes." });
185 |       // return;
186 |     }
187 | 
188 |     // Construct req.auth with only the properties defined in SDK's AuthInfo
189 |     // All other claims from 'decoded' are not part of req.auth for type safety.
190 |     req.auth = {
191 |       token: rawToken,
192 |       clientId: clientIdFromToken,
193 |       scopes: scopesFromToken,
194 |     };
195 | 
196 |     // Log separately if other JWT claims like 'sub' (sessionId) are needed for app logic
197 |     const subClaimForLogging =
198 |       typeof decoded.sub === "string" ? decoded.sub : undefined;
199 |     logger.debug("JWT verified successfully. AuthInfo attached to request.", {
200 |       ...context,
201 |       mcpSessionIdContext: subClaimForLogging,
202 |       clientId: req.auth.clientId,
203 |       scopes: req.auth.scopes,
204 |     });
205 |     next();
206 |   } catch (error: unknown) {
207 |     let errorMessage = "Invalid token";
208 |     if (error instanceof jwt.TokenExpiredError) {
209 |       errorMessage = "Token expired";
210 |       logger.warning("Authentication failed: Token expired.", {
211 |         ...context,
212 |         expiredAt: error.expiredAt, // Accessing error.expiredAt safely
213 |       });
214 |     } else if (error instanceof jwt.JsonWebTokenError) { // This already implies error is an instance of Error
215 |       errorMessage = `Invalid token: ${error.message}`; // Accessing error.message safely
216 |       logger.warning(`Authentication failed: ${errorMessage}`, { ...context });
217 |     } else if (error instanceof Error) { // Catch other generic Errors
218 |       errorMessage = `Verification error: ${error.message}`; // Accessing error.message safely
219 |       logger.error(
220 |         "Authentication failed: Unexpected error during token verification.",
221 |         { ...context, error: error.message },
222 |       );
223 |     } else { // Handle truly unknown types
224 |       errorMessage = "Unknown verification error";
225 |       logger.error(
226 |         "Authentication failed: Unexpected non-error exception during token verification.",
227 |         { ...context, error: String(error) }, // Convert unknown error to string for logging
228 |       );
229 |     }
230 |     res.status(401).json({ error: `Unauthorized: ${errorMessage}.` });
231 |   }
232 | }
233 | 
```

--------------------------------------------------------------------------------
/src/mcp-server/tools/updateFile/updateFileLogic.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import fs from 'fs/promises';
  2 | import { z } from 'zod';
  3 | import { BaseErrorCode, McpError } from '../../../types-global/errors.js';
  4 | import { logger } from '../../../utils/internal/logger.js';
  5 | import { RequestContext } from '../../../utils/internal/requestContext.js';
  6 | import { serverState } from '../../state.js';
  7 | 
  8 | // Define the structure for a single search/replace block
  9 | const DiffBlockSchema = z.object({
 10 |   search: z.string().min(1, 'Search pattern cannot be empty'),
 11 |   replace: z.string(), // Allow empty replace string for deletions
 12 | });
 13 | 
 14 | // Define the input schema using Zod for validation
 15 | export const UpdateFileInputSchema = z.object({
 16 |   path: z.string().min(1, 'Path cannot be empty')
 17 |     .describe('The path to the file to update. Can be relative or absolute (resolved like readFile). The file must exist.'),
 18 |   blocks: z.array(DiffBlockSchema).min(1, 'At least one search/replace block is required.')
 19 |     .describe('An array of objects, each with a `search` (string) and `replace` (string) property.'),
 20 |   useRegex: z.boolean().default(false)
 21 |     .describe('If true, treat the `search` field of each block as a JavaScript regular expression pattern. Defaults to false (exact string matching).'),
 22 |   replaceAll: z.boolean().default(false)
 23 |     .describe('If true, replace all occurrences matching the SEARCH criteria within the file. If false, only replace the first occurrence. Defaults to false.'),
 24 | });
 25 | 
 26 | // Define the TypeScript type for the input
 27 | export type UpdateFileInput = z.infer<typeof UpdateFileInputSchema>;
 28 | 
 29 | // Define the TypeScript type for a single block based on the schema, adding internal tracking
 30 | export type DiffBlock = z.infer<typeof DiffBlockSchema> & { applied?: boolean };
 31 | 
 32 | // Define the TypeScript type for the output
 33 | export interface UpdateFileOutput {
 34 |   message: string;
 35 |   updatedPath: string;
 36 |   blocksApplied: number;
 37 |   blocksFailed: number; // Track blocks that didn't find a match
 38 | }
 39 | 
 40 | /**
 41 |  * Applies an array of search/replace blocks sequentially to the file content.
 42 |  *
 43 |  * @param {UpdateFileInput} input - The input object containing path, blocks, and options.
 44 |  * @param {RequestContext} context - The request context.
 45 |  * @returns {Promise<UpdateFileOutput>} A promise resolving with update status.
 46 |  * @throws {McpError} For path errors, file not found, I/O errors, or invalid regex patterns.
 47 |  */
 48 | export const updateFileLogic = async (input: UpdateFileInput, context: RequestContext): Promise<UpdateFileOutput> => {
 49 |   // Destructure validated input
 50 |   const { path: requestedPath, blocks: inputBlocks, useRegex, replaceAll } = input;
 51 |   const logicContext = { ...context, useRegex, replaceAll };
 52 |   logger.debug(`updateFileLogic: Received request for path "${requestedPath}" with ${inputBlocks.length} blocks`, logicContext);
 53 | 
 54 |   // Resolve the path
 55 |   const absolutePath = serverState.resolvePath(requestedPath, context);
 56 |   logger.debug(`updateFileLogic: Resolved path to "${absolutePath}"`, { ...context, requestedPath });
 57 | 
 58 |   try {
 59 |     // 1. Read the existing file content
 60 |     let currentContent: string;
 61 |     try {
 62 |       currentContent = await fs.readFile(absolutePath, 'utf8');
 63 |       logger.debug(`updateFileLogic: Successfully read existing file "${absolutePath}"`, { ...context, requestedPath });
 64 |     } catch (readError: any) {
 65 |       if (readError.code === 'ENOENT') {
 66 |         logger.warning(`updateFileLogic: File not found at "${absolutePath}"`, { ...context, requestedPath });
 67 |         throw new McpError(BaseErrorCode.NOT_FOUND, `File not found at path: ${absolutePath}. Cannot update a non-existent file.`, { ...context, requestedPath, resolvedPath: absolutePath, originalError: readError });
 68 |       }
 69 |       throw readError; // Re-throw other read errors
 70 |     }
 71 | 
 72 |     // 2. Input blocks are already parsed and validated by Zod
 73 |     const diffBlocks: DiffBlock[] = inputBlocks.map(block => ({ ...block, applied: false })); // Add internal 'applied' flag
 74 | 
 75 |     // 3. Apply blocks sequentially
 76 |     let updatedContent = currentContent;
 77 |     let blocksApplied = 0;
 78 |     let blocksFailed = 0;
 79 |     let totalReplacementsMade = 0; // Track individual replacements if replaceAll is true
 80 | 
 81 |     for (let i = 0; i < diffBlocks.length; i++) {
 82 |       const block = diffBlocks[i];
 83 |       // Create context specific to this block's processing
 84 |       const blockContext = { ...logicContext, blockIndex: i, searchPreview: block.search.substring(0, 50) };
 85 |       let blockMadeChange = false;
 86 |       let replacementsInBlock = 0; // Count replacements made by *this specific block*
 87 | 
 88 |       try {
 89 |         if (useRegex) {
 90 |           // Treat search as regex pattern
 91 |           // Create the regex. Add 'g' flag if replaceAll is true.
 92 |           const regex = new RegExp(block.search, replaceAll ? 'g' : '');
 93 |           const matches = updatedContent.match(regex); // Find matches before replacing
 94 | 
 95 |           if (matches && matches.length > 0) {
 96 |              updatedContent = updatedContent.replace(regex, block.replace);
 97 |              replacementsInBlock = matches.length; // Count actual matches found
 98 |              blockMadeChange = true;
 99 |              logger.debug(`Applied regex block`, blockContext);
100 |           }
101 |         } else {
102 |           // Treat search as exact string
103 |           if (replaceAll) {
104 |             let startIndex = 0;
105 |             let index;
106 |             let replaced = false;
107 |             // Use split/join for robust replacement of all occurrences
108 |             const parts = updatedContent.split(block.search);
109 |             if (parts.length > 1) { // Check if the search string was found at all
110 |                 updatedContent = parts.join(block.replace);
111 |                 replacementsInBlock = parts.length - 1; // Number of replacements is one less than the number of parts
112 |                 replaced = true;
113 |             }
114 | 
115 |             if (replaced) {
116 |                blockMadeChange = true;
117 |                logger.debug(`Applied string block (replaceAll=true)`, blockContext);
118 |             }
119 |           } else {
120 |             // Replace only the first occurrence
121 |             const index = updatedContent.indexOf(block.search);
122 |             if (index !== -1) {
123 |               updatedContent = updatedContent.substring(0, index) + block.replace + updatedContent.substring(index + block.search.length);
124 |               replacementsInBlock = 1;
125 |               blockMadeChange = true;
126 |               logger.debug(`Applied string block (replaceAll=false)`, blockContext);
127 |             }
128 |           }
129 |         }
130 |       } catch (regexError: any) {
131 |          if (regexError instanceof SyntaxError && useRegex) {
132 |             logger.error('Invalid regex pattern provided in SEARCH block', { ...blockContext, error: regexError.message });
133 |             throw new McpError(BaseErrorCode.VALIDATION_ERROR, `Invalid regular expression pattern in block ${i + 1}: "${block.search}". Error: ${regexError.message}`, blockContext);
134 |          }
135 |          // Re-throw other unexpected errors during replacement
136 |          logger.error('Unexpected error during replacement operation', { ...blockContext, error: regexError.message });
137 |          throw new McpError(BaseErrorCode.INTERNAL_ERROR, `Error processing block ${i + 1}: ${regexError.message}`, blockContext);
138 |       }
139 | 
140 | 
141 |       if (blockMadeChange) {
142 |         block.applied = true; // Mark the block as having made a change
143 |         blocksApplied++;
144 |         totalReplacementsMade += replacementsInBlock; // Add replacements from this block to total
145 |       } else {
146 |         blocksFailed++;
147 |         logger.warning(`Diff block search criteria not found`, blockContext);
148 |       }
149 |     }
150 | 
151 |     // 4. Write the updated content back to the file only if changes were actually made
152 |     if (totalReplacementsMade > 0) { // Check if any replacement occurred across all blocks
153 |       logger.debug(`updateFileLogic: Writing updated content back to "${absolutePath}"`, logicContext);
154 |       await fs.writeFile(absolutePath, updatedContent, 'utf8');
155 |       logger.info(`updateFileLogic: Successfully updated file "${absolutePath}"`, { ...logicContext, requestedPath, blocksApplied, blocksFailed, totalReplacementsMade });
156 |       const replaceMsg = `Made ${totalReplacementsMade} replacement(s) across ${blocksApplied} block(s).`;
157 |       return {
158 |         message: `Successfully updated file ${absolutePath}. ${replaceMsg} ${blocksFailed} block(s) failed (search criteria not found).`,
159 |         updatedPath: absolutePath,
160 |         blocksApplied,
161 |         blocksFailed,
162 |       };
163 |     } else {
164 |       // No replacements were made, even if blocks were provided
165 |       logger.info(`updateFileLogic: No replacements made in file "${absolutePath}"`, { ...logicContext, requestedPath, blocksFailed });
166 |       return {
167 |         message: `No changes applied to file ${absolutePath}. ${blocksFailed} block(s) failed (search criteria not found).`,
168 |         updatedPath: absolutePath,
169 |         blocksApplied: 0, // No blocks resulted in a change
170 |         blocksFailed,
171 |       };
172 |     }
173 | 
174 |   } catch (error: any) {
175 |     logger.error(`updateFileLogic: Error updating file "${absolutePath}"`, { ...logicContext, requestedPath, error: error.message, code: error.code });
176 |     if (error instanceof McpError) {
177 |       throw error; // Re-throw known McpErrors
178 |     }
179 |     // Handle potential I/O errors during read or write
180 |     throw new McpError(BaseErrorCode.INTERNAL_ERROR, `Failed to update file: ${error.message || 'Unknown I/O error'}`, { ...context, requestedPath, resolvedPath: absolutePath, originalError: error });
181 |   }
182 | };
183 | 
```

--------------------------------------------------------------------------------
/src/utils/security/idGenerator.ts:
--------------------------------------------------------------------------------

```typescript
  1 | /**
  2 |  * @fileoverview Provides a utility class `IdGenerator` for creating customizable, prefixed unique identifiers,
  3 |  * and a standalone `generateUUID` function for generating standard UUIDs.
  4 |  * The `IdGenerator` supports entity-specific prefixes, custom character sets, and lengths.
  5 |  *
  6 |  * Note: Logging has been removed from this module to prevent circular dependencies
  7 |  * with the `requestContextService`, which itself uses `generateUUID` from this module.
  8 |  * This was causing `ReferenceError: Cannot access 'generateUUID' before initialization`
  9 |  * during application startup.
 10 |  * @module src/utils/security/idGenerator
 11 |  */
 12 | import { randomUUID as cryptoRandomUUID, randomBytes } from "crypto";
 13 | import { BaseErrorCode, McpError } from "../../types-global/errors.js";
 14 | // Removed: import { logger, requestContextService } from "../index.js";
 15 | 
 16 | /**
 17 |  * Defines the structure for configuring entity prefixes.
 18 |  * Keys are entity type names (e.g., "project", "task"), and values are their corresponding ID prefixes (e.g., "PROJ", "TASK").
 19 |  */
 20 | export interface EntityPrefixConfig {
 21 |   [key: string]: string;
 22 | }
 23 | 
 24 | /**
 25 |  * Defines options for customizing ID generation.
 26 |  */
 27 | export interface IdGenerationOptions {
 28 |   length?: number;
 29 |   separator?: string;
 30 |   charset?: string;
 31 | }
 32 | 
 33 | /**
 34 |  * A generic ID Generator class for creating and managing unique, prefixed identifiers.
 35 |  * Allows defining custom prefixes, generating random strings, and validating/normalizing IDs.
 36 |  */
 37 | export class IdGenerator {
 38 |   /**
 39 |    * Default character set for the random part of the ID.
 40 |    * @private
 41 |    */
 42 |   private static DEFAULT_CHARSET = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
 43 |   /**
 44 |    * Default separator character between prefix and random part.
 45 |    * @private
 46 |    */
 47 |   private static DEFAULT_SEPARATOR = "_";
 48 |   /**
 49 |    * Default length for the random part of the ID.
 50 |    * @private
 51 |    */
 52 |   private static DEFAULT_LENGTH = 6;
 53 | 
 54 |   /**
 55 |    * Stores the mapping of entity types to their prefixes.
 56 |    * @private
 57 |    */
 58 |   private entityPrefixes: EntityPrefixConfig = {};
 59 |   /**
 60 |    * Stores a reverse mapping from prefixes (case-insensitive) to entity types.
 61 |    * @private
 62 |    */
 63 |   private prefixToEntityType: Record<string, string> = {};
 64 | 
 65 |   /**
 66 |    * Constructs an `IdGenerator` instance.
 67 |    * @param entityPrefixes - An initial map of entity types to their prefixes.
 68 |    */
 69 |   constructor(entityPrefixes: EntityPrefixConfig = {}) {
 70 |     // Logging removed to prevent circular dependency with requestContextService.
 71 |     this.setEntityPrefixes(entityPrefixes);
 72 |   }
 73 | 
 74 |   /**
 75 |    * Sets or updates the entity prefix configuration and rebuilds the internal reverse lookup map.
 76 |    * @param entityPrefixes - A map where keys are entity type names and values are their desired ID prefixes.
 77 |    */
 78 |   public setEntityPrefixes(entityPrefixes: EntityPrefixConfig): void {
 79 |     // Logging removed.
 80 |     this.entityPrefixes = { ...entityPrefixes };
 81 | 
 82 |     this.prefixToEntityType = Object.entries(this.entityPrefixes).reduce(
 83 |       (acc, [type, prefix]) => {
 84 |         acc[prefix.toLowerCase()] = type; // Store lowercase for case-insensitive lookup
 85 |         return acc;
 86 |       },
 87 |       {} as Record<string, string>,
 88 |     );
 89 |   }
 90 | 
 91 |   /**
 92 |    * Retrieves a copy of the current entity prefix configuration.
 93 |    * @returns The current entity prefix configuration.
 94 |    */
 95 |   public getEntityPrefixes(): EntityPrefixConfig {
 96 |     return { ...this.entityPrefixes };
 97 |   }
 98 | 
 99 |   /**
100 |    * Generates a cryptographically secure random string.
101 |    * @param length - The desired length of the random string. Defaults to `IdGenerator.DEFAULT_LENGTH`.
102 |    * @param charset - The character set to use. Defaults to `IdGenerator.DEFAULT_CHARSET`.
103 |    * @returns The generated random string.
104 |    */
105 |   public generateRandomString(
106 |     length: number = IdGenerator.DEFAULT_LENGTH,
107 |     charset: string = IdGenerator.DEFAULT_CHARSET,
108 |   ): string {
109 |     const bytes = randomBytes(length);
110 |     let result = "";
111 |     for (let i = 0; i < length; i++) {
112 |       result += charset[bytes[i] % charset.length];
113 |     }
114 |     return result;
115 |   }
116 | 
117 |   /**
118 |    * Generates a unique ID, optionally prepended with a prefix.
119 |    * @param prefix - An optional prefix for the ID.
120 |    * @param options - Optional parameters for ID generation (length, separator, charset).
121 |    * @returns A unique identifier string.
122 |    */
123 |   public generate(prefix?: string, options: IdGenerationOptions = {}): string {
124 |     // Logging removed.
125 |     const {
126 |       length = IdGenerator.DEFAULT_LENGTH,
127 |       separator = IdGenerator.DEFAULT_SEPARATOR,
128 |       charset = IdGenerator.DEFAULT_CHARSET,
129 |     } = options;
130 | 
131 |     const randomPart = this.generateRandomString(length, charset);
132 |     const generatedId = prefix
133 |       ? `${prefix}${separator}${randomPart}`
134 |       : randomPart;
135 |     return generatedId;
136 |   }
137 | 
138 |   /**
139 |    * Generates a unique ID for a specified entity type, using its configured prefix.
140 |    * @param entityType - The type of entity (must be registered).
141 |    * @param options - Optional parameters for ID generation.
142 |    * @returns A unique identifier string for the entity (e.g., "PROJ_A6B3J0").
143 |    * @throws {McpError} If the `entityType` is not registered.
144 |    */
145 |   public generateForEntity(
146 |     entityType: string,
147 |     options: IdGenerationOptions = {},
148 |   ): string {
149 |     const prefix = this.entityPrefixes[entityType];
150 |     if (!prefix) {
151 |       throw new McpError(
152 |         BaseErrorCode.VALIDATION_ERROR,
153 |         `Unknown entity type: ${entityType}. No prefix registered.`,
154 |       );
155 |     }
156 |     return this.generate(prefix, options);
157 |   }
158 | 
159 |   /**
160 |    * Validates if an ID conforms to the expected format for a specific entity type.
161 |    * @param id - The ID string to validate.
162 |    * @param entityType - The expected entity type of the ID.
163 |    * @param options - Optional parameters used during generation for validation consistency.
164 |    * @returns `true` if the ID is valid, `false` otherwise.
165 |    */
166 |   public isValid(
167 |     id: string,
168 |     entityType: string,
169 |     options: IdGenerationOptions = {},
170 |   ): boolean {
171 |     const prefix = this.entityPrefixes[entityType];
172 |     const {
173 |       length = IdGenerator.DEFAULT_LENGTH,
174 |       separator = IdGenerator.DEFAULT_SEPARATOR,
175 |     } = options;
176 | 
177 |     if (!prefix) {
178 |       return false;
179 |     }
180 |     // Assumes default charset characters (uppercase letters and digits) for regex.
181 |     const pattern = new RegExp(
182 |       `^${this.escapeRegex(prefix)}${this.escapeRegex(separator)}[A-Z0-9]{${length}}$`,
183 |     );
184 |     return pattern.test(id);
185 |   }
186 | 
187 |   /**
188 |    * Escapes special characters in a string for use in a regular expression.
189 |    * @param str - The string to escape.
190 |    * @returns The escaped string.
191 |    * @private
192 |    */
193 |   private escapeRegex(str: string): string {
194 |     return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
195 |   }
196 | 
197 |   /**
198 |    * Strips the prefix and separator from an ID string.
199 |    * @param id - The ID string (e.g., "PROJ_A6B3J0").
200 |    * @param separator - The separator used in the ID. Defaults to `IdGenerator.DEFAULT_SEPARATOR`.
201 |    * @returns The ID part without the prefix, or the original ID if separator not found.
202 |    */
203 |   public stripPrefix(
204 |     id: string,
205 |     separator: string = IdGenerator.DEFAULT_SEPARATOR,
206 |   ): string {
207 |     const parts = id.split(separator);
208 |     return parts.length > 1 ? parts.slice(1).join(separator) : id; // Handle separators in random part
209 |   }
210 | 
211 |   /**
212 |    * Determines the entity type from an ID string by its prefix (case-insensitive).
213 |    * @param id - The ID string (e.g., "PROJ_A6B3J0").
214 |    * @param separator - The separator used in the ID. Defaults to `IdGenerator.DEFAULT_SEPARATOR`.
215 |    * @returns The determined entity type.
216 |    * @throws {McpError} If ID format is invalid or prefix is unknown.
217 |    */
218 |   public getEntityType(
219 |     id: string,
220 |     separator: string = IdGenerator.DEFAULT_SEPARATOR,
221 |   ): string {
222 |     const parts = id.split(separator);
223 |     if (parts.length < 2 || !parts[0]) {
224 |       throw new McpError(
225 |         BaseErrorCode.VALIDATION_ERROR,
226 |         `Invalid ID format: ${id}. Expected format like: PREFIX${separator}RANDOMLPART`,
227 |       );
228 |     }
229 | 
230 |     const prefix = parts[0];
231 |     const entityType = this.prefixToEntityType[prefix.toLowerCase()];
232 | 
233 |     if (!entityType) {
234 |       throw new McpError(
235 |         BaseErrorCode.VALIDATION_ERROR,
236 |         `Unknown entity type for prefix: ${prefix}`,
237 |       );
238 |     }
239 |     return entityType;
240 |   }
241 | 
242 |   /**
243 |    * Normalizes an entity ID to ensure the prefix matches the registered case
244 |    * and the random part is uppercase.
245 |    * @param id - The ID to normalize (e.g., "proj_a6b3j0").
246 |    * @param separator - The separator used in the ID. Defaults to `IdGenerator.DEFAULT_SEPARATOR`.
247 |    * @returns The normalized ID (e.g., "PROJ_A6B3J0").
248 |    * @throws {McpError} If the entity type cannot be determined from the ID.
249 |    */
250 |   public normalize(
251 |     id: string,
252 |     separator: string = IdGenerator.DEFAULT_SEPARATOR,
253 |   ): string {
254 |     const entityType = this.getEntityType(id, separator);
255 |     const registeredPrefix = this.entityPrefixes[entityType];
256 |     const idParts = id.split(separator);
257 |     const randomPart = idParts.slice(1).join(separator);
258 | 
259 |     return `${registeredPrefix}${separator}${randomPart.toUpperCase()}`;
260 |   }
261 | }
262 | 
263 | /**
264 |  * Default singleton instance of the `IdGenerator`.
265 |  * Initialize with `idGenerator.setEntityPrefixes({})` to configure.
266 |  */
267 | export const idGenerator = new IdGenerator();
268 | 
269 | /**
270 |  * Generates a standard Version 4 UUID (Universally Unique Identifier).
271 |  * Uses the Node.js `crypto` module. This function is independent of the IdGenerator instance
272 |  * to prevent circular dependencies when used by other utilities like requestContextService.
273 |  * @returns A new UUID string.
274 |  */
275 | export const generateUUID = (): string => {
276 |   return cryptoRandomUUID();
277 | };
278 | 
```

--------------------------------------------------------------------------------
/src/mcp-server/tools/listFiles/listFilesLogic.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import fs from 'fs/promises';
  2 | import path from 'path';
  3 | import { z } from 'zod';
  4 | import { BaseErrorCode, McpError } from '../../../types-global/errors.js';
  5 | import { logger } from '../../../utils/internal/logger.js';
  6 | import { RequestContext } from '../../../utils/internal/requestContext.js';
  7 | import { serverState } from '../../state.js';
  8 | 
  9 | // Define the input schema using Zod for validation
 10 | export const ListFilesInputSchema = z.object({
 11 |   path: z.string().min(1, 'Path cannot be empty')
 12 |     .describe('The path to the directory to list. Can be relative or absolute (resolved like readFile).'),
 13 |   includeNested: z.boolean().default(false)
 14 |     .describe('If true, list files and directories recursively. Defaults to false (top-level only).'),
 15 |   maxEntries: z.number().int().positive().optional().default(50) // Updated default to 50
 16 |     .describe('Maximum number of directory entries (files + folders) to return. Defaults to 50. Helps prevent excessive output for large directories.'),
 17 | });
 18 | 
 19 | // Define the TypeScript type for the input
 20 | export type ListFilesInput = z.infer<typeof ListFilesInputSchema>;
 21 | 
 22 | // Define the TypeScript type for the output
 23 | export interface ListFilesOutput {
 24 |   message: string;
 25 |   tree: string;
 26 |   requestedPath: string;
 27 |   resolvedPath: string;
 28 |   itemCount: number;
 29 |   truncated: boolean; // Added flag
 30 | }
 31 | 
 32 | interface DirectoryItem {
 33 |   name: string;
 34 |   isDirectory: boolean;
 35 |   children?: DirectoryItem[]; // Only populated if includeNested is true
 36 |   error?: string; // Added to indicate read errors for this directory
 37 | }
 38 | 
 39 | /**
 40 |  * Recursively reads directory contents and builds a tree structure.
 41 |  *
 42 |  * @param {string} dirPath - The absolute path to the directory.
 43 |  * @param {boolean} includeNested - Whether to recurse into subdirectories.
 44 |  * @param {RequestContext} context - The request context for logging.
 45 |  * @param {{ count: number, limit: number, truncated: boolean }} state - Mutable state to track count and limit across recursive calls.
 46 |  * @returns {Promise<DirectoryItem[]>} A promise resolving with the list of items.
 47 |  * @throws {McpError} If reading the directory fails.
 48 |  */
 49 | const readDirectoryRecursive = async (
 50 |   dirPath: string,
 51 |   includeNested: boolean,
 52 |   context: RequestContext,
 53 |   state: { count: number; limit: number; truncated: boolean } // Pass state object
 54 | ): Promise<DirectoryItem[]> => {
 55 |   if (state.truncated || state.count >= state.limit) {
 56 |     state.truncated = true; // Ensure truncated flag is set if limit reached before starting
 57 |     return []; // Stop processing if limit already reached
 58 |   }
 59 | 
 60 |   const items: DirectoryItem[] = [];
 61 |   let entries;
 62 |   try {
 63 |     entries = await fs.readdir(dirPath, { withFileTypes: true });
 64 |   } catch (error: any) {
 65 |     if (error.code === 'ENOENT') {
 66 |       logger.warning(`Directory not found: ${dirPath}`, context);
 67 |       throw new McpError(BaseErrorCode.NOT_FOUND, `Directory not found at path: ${dirPath}`, { ...context, dirPath, originalError: error });
 68 |     } else if (error.code === 'ENOTDIR') {
 69 |        logger.warning(`Path is not a directory: ${dirPath}`, context);
 70 |        throw new McpError(BaseErrorCode.VALIDATION_ERROR, `Path is not a directory: ${dirPath}`, { ...context, dirPath, originalError: error });
 71 |     }
 72 |     logger.error(`Failed to read directory: ${dirPath}`, { ...context, error: error.message });
 73 |     throw new McpError(BaseErrorCode.INTERNAL_ERROR, `Failed to read directory: ${error.message}`, { ...context, dirPath, originalError: error });
 74 |   }
 75 | 
 76 |   for (const entry of entries) {
 77 |     if (state.count >= state.limit) {
 78 |       state.truncated = true;
 79 |       logger.debug(`Max entries limit (${state.limit}) reached while processing ${dirPath}`, context);
 80 |       break; // Stop processing entries in this directory
 81 |     }
 82 | 
 83 |     state.count++; // Increment count for this entry
 84 | 
 85 |     const itemPath = path.join(dirPath, entry.name);
 86 |     const item: DirectoryItem = {
 87 |       name: entry.name,
 88 |       isDirectory: entry.isDirectory(),
 89 |     };
 90 | 
 91 |     if (item.isDirectory && includeNested) {
 92 |       // Recursively read subdirectory, passing the shared state object
 93 |       try {
 94 |         // Pass the same state object down
 95 |         item.children = await readDirectoryRecursive(itemPath, includeNested, { ...context, parentPath: dirPath }, state);
 96 |       } catch (recursiveError) {
 97 |          // Log the error from the recursive call but continue processing other entries
 98 |          logger.error(`Error reading nested directory ${itemPath}`, { ...context, error: (recursiveError as Error).message, code: (recursiveError as McpError).code });
 99 |          // Log the error and mark the item
100 |          const errorMessage = (recursiveError as McpError)?.message || (recursiveError as Error)?.message || 'Unknown error reading directory';
101 |          logger.error(`Error reading nested directory ${itemPath}`, { ...context, error: errorMessage, code: (recursiveError as McpError)?.code });
102 |          item.error = errorMessage; // Store the error message on the item
103 |          item.children = undefined; // Ensure no children are processed or displayed for errored directories
104 |       }
105 |     }
106 |     items.push(item);
107 | 
108 |     // Check limit again after potentially adding children (though count is incremented per item)
109 |     if (state.truncated) {
110 |        break; // Exit loop if limit was hit during recursive call
111 |     }
112 |   }
113 | 
114 |   // Sort items: directories first, then files, alphabetically
115 |   items.sort((a, b) => {
116 |     if (a.isDirectory !== b.isDirectory) {
117 |       return a.isDirectory ? -1 : 1; // Directories first
118 |     }
119 |     return a.name.localeCompare(b.name); // Then sort alphabetically
120 |   });
121 | 
122 |   return items;
123 | };
124 | 
125 | /**
126 |  * Formats the directory items into a tree-like string.
127 |  *
128 |  * @param {DirectoryItem[]} items - The items to format.
129 |  * @param {string} prefix - The prefix string for indentation.
130 |  * @param {boolean} truncated - Whether the listing was cut short due to limits.
131 |  * @returns {string} The formatted tree string.
132 |  */
133 | const formatTree = (items: DirectoryItem[], truncated: boolean, prefix = ''): string => {
134 |   let treeString = '';
135 |   items.forEach((item, index) => {
136 |     const isLast = index === items.length - 1;
137 |     const connector = isLast ? '└── ' : '├── ';
138 |     const itemPrefix = item.isDirectory ? '📁 ' : '📄 ';
139 |     const errorMarker = item.error ? ` [Error: ${item.error}]` : ''; // Add error marker if present
140 |     treeString += `${prefix}${connector}${itemPrefix}${item.name}${errorMarker}\n`;
141 | 
142 |     // Only recurse if it's a directory, has children defined (not errored), and children exist
143 |     if (item.isDirectory && !item.error && item.children && item.children.length > 0) {
144 |       const childPrefix = prefix + (isLast ? '    ' : '│   ');
145 |       // Pass truncated flag down, but don't add the message recursively
146 |       treeString += formatTree(item.children, false, childPrefix);
147 |     } else if (item.isDirectory && item.error) {
148 |       // Optionally add a specific marker for children of errored directories,
149 |       // but the error on the parent line is likely sufficient.
150 |     }
151 |   });
152 | 
153 |   // Add truncation message at the end of the current level if needed
154 |   if (truncated && prefix === '') { // Only add at the top level formatting call
155 |       treeString += `${prefix}...\n${prefix}[Listing truncated due to max entries limit]\n`;
156 |   }
157 | 
158 |   return treeString;
159 | };
160 | 
161 | /**
162 |  * Lists files and directories at a given path, optionally recursively.
163 |  *
164 |  * @param {ListFilesInput} input - The input object containing path and options.
165 |  * @param {RequestContext} context - The request context.
166 |  * @returns {Promise<ListFilesOutput>} A promise resolving with the listing results.
167 |  * @throws {McpError} For path errors, directory not found, or I/O errors.
168 |  */
169 | export const listFilesLogic = async (input: ListFilesInput, context: RequestContext): Promise<ListFilesOutput> => {
170 |   // Destructure validated input, including the new maxEntries
171 |   const { path: requestedPath, includeNested, maxEntries } = input;
172 |   const logicContext = { ...context, includeNested, maxEntries };
173 |   logger.debug(`listFilesLogic: Received request for path "${requestedPath}" with limit ${maxEntries}`, logicContext);
174 | 
175 |   // Resolve the path
176 |   const absolutePath = serverState.resolvePath(requestedPath, context);
177 |   logger.debug(`listFilesLogic: Resolved path to "${absolutePath}"`, { ...logicContext, requestedPath });
178 | 
179 |   try {
180 |     // Initialize state for tracking count and limit, using the potentially updated default
181 |     const state = { count: 0, limit: maxEntries, truncated: false };
182 | 
183 |     // Read directory structure using the state object
184 |     const items = await readDirectoryRecursive(absolutePath, includeNested, logicContext, state);
185 | 
186 |     // Format the tree, passing the final truncated state
187 |     const rootName = path.basename(absolutePath);
188 |     const tree = `📁 ${rootName}\n` + formatTree(items, state.truncated); // Pass truncated flag
189 | 
190 |     const message = state.truncated
191 |       ? `Successfully listed ${state.count} items in ${absolutePath} (truncated at limit of ${maxEntries}).` // Use maxEntries from input for message
192 |       : `Successfully listed ${state.count} items in ${absolutePath}.`;
193 | 
194 |     logger.info(`listFilesLogic: ${message}`, { ...logicContext, requestedPath, itemCount: state.count, truncated: state.truncated, limit: maxEntries });
195 | 
196 |     return {
197 |       message: message,
198 |       tree: tree,
199 |       requestedPath: requestedPath,
200 |       resolvedPath: absolutePath,
201 |       itemCount: state.count, // Return the actual count processed
202 |       truncated: state.truncated,
203 |     };
204 | 
205 |   } catch (error: any) {
206 |     // Errors during readDirectoryRecursive are already logged and potentially thrown as McpError
207 |     logger.error(`listFilesLogic: Error listing files at "${absolutePath}"`, { ...logicContext, requestedPath, error: error.message, code: error.code });
208 |     if (error instanceof McpError) {
209 |       throw error; // Re-throw known McpErrors
210 |     }
211 |     // Catch any other unexpected errors
212 |     throw new McpError(BaseErrorCode.INTERNAL_ERROR, `Failed to list files: ${error.message || 'Unknown I/O error'}`, { ...context, requestedPath, resolvedPath: absolutePath, originalError: error });
213 |   }
214 | };
215 | 
```

--------------------------------------------------------------------------------
/src/config/index.ts:
--------------------------------------------------------------------------------

```typescript
  1 | /**
  2 |  * @fileoverview Loads, validates, and exports application configuration.
  3 |  * This module centralizes configuration management, sourcing values from
  4 |  * environment variables and `package.json`. It uses Zod for schema validation
  5 |  * to ensure type safety and correctness of configuration parameters.
  6 |  *
  7 |  * Key responsibilities:
  8 |  * - Load environment variables from a `.env` file.
  9 |  * - Read `package.json` for default server name and version.
 10 |  * - Define a Zod schema for all expected environment variables.
 11 |  * - Validate environment variables against the schema.
 12 |  * - Construct and export a comprehensive `config` object.
 13 |  * - Export individual configuration values like `logLevel` and `environment` for convenience.
 14 |  *
 15 |  * @module src/config/index
 16 |  */
 17 | 
 18 | import dotenv from "dotenv";
 19 | import { existsSync, mkdirSync, readFileSync, statSync } from "fs";
 20 | import path, { dirname, join } from "path";
 21 | import { fileURLToPath } from "url";
 22 | import { z } from "zod";
 23 | 
 24 | dotenv.config();
 25 | 
 26 | // --- Determine Project Root ---
 27 | /**
 28 |  * Finds the project root directory by searching upwards for package.json.
 29 |  * @param startDir The directory to start searching from.
 30 |  * @returns The absolute path to the project root, or throws an error if not found.
 31 |  */
 32 | const findProjectRoot = (startDir: string): string => {
 33 |   let currentDir = startDir;
 34 |   while (true) {
 35 |     const packageJsonPath = join(currentDir, "package.json");
 36 |     if (existsSync(packageJsonPath)) {
 37 |       return currentDir;
 38 |     }
 39 |     const parentDir = dirname(currentDir);
 40 |     if (parentDir === currentDir) {
 41 |       // Reached the root of the filesystem without finding package.json
 42 |       throw new Error(
 43 |         `Could not find project root (package.json) starting from ${startDir}`,
 44 |       );
 45 |     }
 46 |     currentDir = parentDir;
 47 |   }
 48 | };
 49 | 
 50 | let projectRoot: string;
 51 | try {
 52 |   // For ESM, __dirname is not available directly.
 53 |   // import.meta.url gives the URL of the current module.
 54 |   const currentModuleDir = dirname(fileURLToPath(import.meta.url));
 55 |   projectRoot = findProjectRoot(currentModuleDir);
 56 | } catch (error: any) {
 57 |   console.error(`FATAL: Error determining project root: ${error.message}`);
 58 |   // Fallback to process.cwd() if project root cannot be determined.
 59 |   // This might happen in unusual execution environments.
 60 |   projectRoot = process.cwd();
 61 |   console.warn(
 62 |     `Warning: Using process.cwd() (${projectRoot}) as fallback project root.`,
 63 |   );
 64 | }
 65 | // --- End Determine Project Root ---
 66 | 
 67 | const pkgPath = join(projectRoot, "package.json"); // Use determined projectRoot
 68 | let pkg = { name: "mcp-ts-template", version: "0.0.0" };
 69 | 
 70 | try {
 71 |   pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
 72 | } catch (error) {
 73 |   if (process.stdout.isTTY) {
 74 |     console.error(
 75 |       "Warning: Could not read package.json for default config values. Using hardcoded defaults.",
 76 |       error,
 77 |     );
 78 |   }
 79 | }
 80 | 
 81 | /**
 82 |  * Zod schema for validating environment variables.
 83 |  * Provides type safety, validation, defaults, and clear error messages.
 84 |  * @private
 85 |  */
 86 | const EnvSchema = z.object({
 87 |   /** Optional. The desired name for the MCP server. Defaults to `package.json` name. */
 88 |   MCP_SERVER_NAME: z.string().optional(),
 89 |   /** Optional. The version of the MCP server. Defaults to `package.json` version. */
 90 |   MCP_SERVER_VERSION: z.string().optional(),
 91 |   /** Minimum logging level. See `McpLogLevel` in logger utility. Default: "debug". */
 92 |   MCP_LOG_LEVEL: z.string().default("debug"),
 93 |   /** Directory for log files. Defaults to "logs" in project root. */
 94 |   LOGS_DIR: z.string().default(path.join(projectRoot, "logs")),
 95 |   /** Runtime environment (e.g., "development", "production"). Default: "development". */
 96 |   NODE_ENV: z.string().default("development"),
 97 |   /** MCP communication transport ("stdio" or "http"). Default: "stdio". */
 98 |   MCP_TRANSPORT_TYPE: z.enum(["stdio", "http"]).default("stdio"),
 99 |   /** HTTP server port (if MCP_TRANSPORT_TYPE is "http"). Default: 3010. */
100 |   MCP_HTTP_PORT: z.coerce.number().int().positive().default(3010),
101 |   /** HTTP server host (if MCP_TRANSPORT_TYPE is "http"). Default: "127.0.0.1". */
102 |   MCP_HTTP_HOST: z.string().default("127.0.0.1"),
103 |   /** Optional. Comma-separated allowed origins for CORS (HTTP transport). */
104 |   MCP_ALLOWED_ORIGINS: z.string().optional(),
105 |   /** Optional. Secret key (min 32 chars) for auth tokens (HTTP transport). CRITICAL for production. */
106 |   MCP_AUTH_SECRET_KEY: z
107 |     .string()
108 |     .min(
109 |       32,
110 |       "MCP_AUTH_SECRET_KEY must be at least 32 characters long for security reasons.",
111 |     )
112 |     .optional(),
113 | 
114 |   /** Optional. Application URL for OpenRouter integration. */
115 |   OPENROUTER_APP_URL: z
116 |     .string()
117 |     .url("OPENROUTER_APP_URL must be a valid URL (e.g., http://localhost:3000)")
118 |     .optional(),
119 |   /** Optional. Application name for OpenRouter. Defaults to MCP_SERVER_NAME or package name. */
120 |   OPENROUTER_APP_NAME: z.string().optional(),
121 |   /** Optional. API key for OpenRouter services. */
122 |   OPENROUTER_API_KEY: z.string().optional(),
123 |   /** Default LLM model. Default: "google/gemini-2.5-flash-preview:thinking". */
124 |   LLM_DEFAULT_MODEL: z
125 |     .string()
126 |     .default("google/gemini-2.5-flash-preview-05-20"),
127 |   /** Optional. Default LLM temperature (0.0-2.0). */
128 |   LLM_DEFAULT_TEMPERATURE: z.coerce.number().min(0).max(2).optional(),
129 |   /** Optional. Default LLM top_p (0.0-1.0). */
130 |   LLM_DEFAULT_TOP_P: z.coerce.number().min(0).max(1).optional(),
131 |   /** Optional. Default LLM max tokens (positive integer). */
132 |   LLM_DEFAULT_MAX_TOKENS: z.coerce.number().int().positive().optional(),
133 |   /** Optional. Default LLM top_k (non-negative integer). */
134 |   LLM_DEFAULT_TOP_K: z.coerce.number().int().nonnegative().optional(),
135 |   /** Optional. Default LLM min_p (0.0-1.0). */
136 |   LLM_DEFAULT_MIN_P: z.coerce.number().min(0).max(1).optional(),
137 |   /** Optional. API key for Google Gemini services. */
138 |   GEMINI_API_KEY: z.string().optional(),
139 | 
140 |   /** Optional. OAuth provider authorization endpoint URL. */
141 |   OAUTH_PROXY_AUTHORIZATION_URL: z
142 |     .string()
143 |     .url("OAUTH_PROXY_AUTHORIZATION_URL must be a valid URL.")
144 |     .optional(),
145 |   /** Optional. OAuth provider token endpoint URL. */
146 |   OAUTH_PROXY_TOKEN_URL: z
147 |     .string()
148 |     .url("OAUTH_PROXY_TOKEN_URL must be a valid URL.")
149 |     .optional(),
150 |   /** Optional. OAuth provider revocation endpoint URL. */
151 |   OAUTH_PROXY_REVOCATION_URL: z
152 |     .string()
153 |     .url("OAUTH_PROXY_REVOCATION_URL must be a valid URL.")
154 |     .optional(),
155 |   /** Optional. OAuth provider issuer URL. */
156 |   OAUTH_PROXY_ISSUER_URL: z
157 |     .string()
158 |     .url("OAUTH_PROXY_ISSUER_URL must be a valid URL.")
159 |     .optional(),
160 |   /** Optional. OAuth service documentation URL. */
161 |   OAUTH_PROXY_SERVICE_DOCUMENTATION_URL: z
162 |     .string()
163 |     .url("OAUTH_PROXY_SERVICE_DOCUMENTATION_URL must be a valid URL.")
164 |     .optional(),
165 |   /** Optional. Comma-separated default OAuth client redirect URIs. */
166 |   OAUTH_PROXY_DEFAULT_CLIENT_REDIRECT_URIS: z.string().optional(),
167 |   /** Optional. Base directory for all filesystem operations. If set, tools cannot access paths outside this directory. Can be an absolute path or relative to the project root. */
168 |   FS_BASE_DIRECTORY: z.string().optional(),
169 | });
170 | 
171 | const parsedEnv = EnvSchema.safeParse(process.env);
172 | 
173 | if (!parsedEnv.success) {
174 |   if (process.stdout.isTTY) {
175 |     console.error(
176 |       "❌ Invalid environment variables found:",
177 |       parsedEnv.error.flatten().fieldErrors,
178 |     );
179 |   }
180 |   // Consider throwing an error in production for critical misconfigurations.
181 | }
182 | 
183 | let env = parsedEnv.success ? parsedEnv.data : EnvSchema.parse({});
184 | 
185 | // Resolve FS_BASE_DIRECTORY if it's relative
186 | let resolvedFsBaseDirectory: string | undefined = env.FS_BASE_DIRECTORY;
187 | if (env.FS_BASE_DIRECTORY && !path.isAbsolute(env.FS_BASE_DIRECTORY)) {
188 |   resolvedFsBaseDirectory = path.resolve(projectRoot, env.FS_BASE_DIRECTORY);
189 |   if (process.stdout.isTTY) {
190 |     console.log(
191 |       `Info: Relative FS_BASE_DIRECTORY "${env.FS_BASE_DIRECTORY}" resolved to "${resolvedFsBaseDirectory}".`
192 |     );
193 |   }
194 | }
195 | 
196 | if (process.stdout.isTTY) {
197 |   if (resolvedFsBaseDirectory) {
198 |     // Ensure the resolved directory exists, or attempt to create it.
199 |     // This is a good place to also check if it's a directory.
200 |     try {
201 |       if (!existsSync(resolvedFsBaseDirectory)) {
202 |         mkdirSync(resolvedFsBaseDirectory, { recursive: true });
203 |         console.log(`Info: Created FS_BASE_DIRECTORY at "${resolvedFsBaseDirectory}".`);
204 |       } else {
205 |         const stats = statSync(resolvedFsBaseDirectory);
206 |         if (!stats.isDirectory()) {
207 |           console.error(`Error: FS_BASE_DIRECTORY "${resolvedFsBaseDirectory}" exists but is not a directory. Restriction will not be applied.`);
208 |           resolvedFsBaseDirectory = undefined; // Disable restriction if path is invalid
209 |         }
210 |       }
211 |       if (resolvedFsBaseDirectory) {
212 |          console.log(
213 |           `Info: Filesystem operations will be restricted to base directory: ${resolvedFsBaseDirectory}`
214 |         );
215 |       }
216 |     } catch (error: any) {
217 |       console.error(`Error processing FS_BASE_DIRECTORY "${resolvedFsBaseDirectory}": ${error.message}. Restriction will not be applied.`);
218 |       resolvedFsBaseDirectory = undefined; // Disable restriction on error
219 |     }
220 |   } else {
221 |     console.warn(
222 |       "Warning: FS_BASE_DIRECTORY is not set. Filesystem operations will not be restricted to a base directory. This is a potential security risk."
223 |     );
224 |   }
225 | }
226 | 
227 | 
228 | // --- Directory Ensurance Function ---
229 | /**
230 |  * Ensures a directory exists and is within the project root.
231 |  * @param dirPath The desired path for the directory (can be relative or absolute).
232 |  * @param rootDir The root directory of the project to contain the directory.
233 |  * @param dirName The name of the directory type for logging (e.g., "logs").
234 |  * @returns The validated, absolute path to the directory, or null if invalid.
235 |  */
236 | const ensureDirectory = (
237 |   dirPath: string,
238 |   rootDir: string,
239 |   dirName: string,
240 | ): string | null => {
241 |   const resolvedDirPath = path.isAbsolute(dirPath)
242 |     ? dirPath
243 |     : path.resolve(rootDir, dirPath);
244 | 
245 |   // Ensure the resolved path is within the project root boundary
246 |   if (
247 |     !resolvedDirPath.startsWith(rootDir + path.sep) &&
248 |     resolvedDirPath !== rootDir
249 |   ) {
250 |     if (process.stdout.isTTY) {
251 |       console.error(
252 |         `Error: ${dirName} path "${dirPath}" resolves to "${resolvedDirPath}", which is outside the project boundary "${rootDir}".`,
253 |       );
254 |     }
255 |     return null;
256 |   }
257 | 
258 |   if (!existsSync(resolvedDirPath)) {
259 |     try {
260 |       mkdirSync(resolvedDirPath, { recursive: true });
261 |       if (process.stdout.isTTY) {
262 |         console.log(`Created ${dirName} directory: ${resolvedDirPath}`);
263 |       }
264 |     } catch (err: unknown) {
265 |       const errorMessage = err instanceof Error ? err.message : String(err);
266 |       if (process.stdout.isTTY) {
267 |         console.error(
268 |           `Error creating ${dirName} directory at ${resolvedDirPath}: ${errorMessage}`,
269 |         );
270 |       }
271 |       return null;
272 |     }
273 |   } else {
274 |     try {
275 |       const stats = statSync(resolvedDirPath);
276 |       if (!stats.isDirectory()) {
277 |         if (process.stdout.isTTY) {
278 |           console.error(
279 |             `Error: ${dirName} path ${resolvedDirPath} exists but is not a directory.`,
280 |           );
281 |         }
282 |         return null;
283 |       }
284 |     } catch (statError: any) {
285 |       if (process.stdout.isTTY) {
286 |         console.error(
287 |           `Error accessing ${dirName} path ${resolvedDirPath}: ${statError.message}`,
288 |         );
289 |       }
290 |       return null;
291 |     }
292 |   }
293 |   return resolvedDirPath;
294 | };
295 | // --- End Directory Ensurance Function ---
296 | 
297 | // --- Logs Directory Handling ---
298 | const validatedLogsPath = ensureDirectory(env.LOGS_DIR, projectRoot, "logs");
299 | 
300 | if (!validatedLogsPath) {
301 |   if (process.stdout.isTTY) {
302 |     console.error(
303 |       "FATAL: Logs directory configuration is invalid or could not be created. Please check permissions and path. Exiting.",
304 |     );
305 |   }
306 |   process.exit(1); // Exit if logs directory is not usable
307 | }
308 | // --- End Logs Directory Handling ---
309 | 
310 | /**
311 |  * Main application configuration object.
312 |  * Aggregates settings from validated environment variables and `package.json`.
313 |  */
314 | export const config = {
315 |   /** MCP server name. Env `MCP_SERVER_NAME` > `package.json` name > "mcp-ts-template". */
316 |   mcpServerName: env.MCP_SERVER_NAME || pkg.name,
317 |   /** MCP server version. Env `MCP_SERVER_VERSION` > `package.json` version > "0.0.0". */
318 |   mcpServerVersion: env.MCP_SERVER_VERSION || pkg.version,
319 |   /** Logging level. From `MCP_LOG_LEVEL` env var. Default: "debug". */
320 |   logLevel: env.MCP_LOG_LEVEL,
321 |   /** Absolute path to the logs directory. From `LOGS_DIR` env var. */
322 |   logsPath: validatedLogsPath,
323 |   /** Runtime environment. From `NODE_ENV` env var. Default: "development". */
324 |   environment: env.NODE_ENV,
325 |   /** MCP transport type ('stdio' or 'http'). From `MCP_TRANSPORT_TYPE` env var. Default: "stdio". */
326 |   mcpTransportType: env.MCP_TRANSPORT_TYPE,
327 |   /** HTTP server port (if http transport). From `MCP_HTTP_PORT` env var. Default: 3010. */
328 |   mcpHttpPort: env.MCP_HTTP_PORT,
329 |   /** HTTP server host (if http transport). From `MCP_HTTP_HOST` env var. Default: "127.0.0.1". */
330 |   mcpHttpHost: env.MCP_HTTP_HOST,
331 |   /** Array of allowed CORS origins (http transport). From `MCP_ALLOWED_ORIGINS` (comma-separated). */
332 |   mcpAllowedOrigins: env.MCP_ALLOWED_ORIGINS?.split(",")
333 |     .map((origin) => origin.trim())
334 |     .filter(Boolean),
335 |   /** Auth secret key (JWTs, http transport). From `MCP_AUTH_SECRET_KEY`. CRITICAL. */
336 |   mcpAuthSecretKey: env.MCP_AUTH_SECRET_KEY,
337 | 
338 |   /** OpenRouter App URL. From `OPENROUTER_APP_URL`. Default: "http://localhost:3000". */
339 |   openrouterAppUrl: env.OPENROUTER_APP_URL || "http://localhost:3000",
340 |   /** OpenRouter App Name. From `OPENROUTER_APP_NAME`. Defaults to `mcpServerName`. */
341 |   openrouterAppName: env.OPENROUTER_APP_NAME || pkg.name || "MCP TS App",
342 |   /** OpenRouter API Key. From `OPENROUTER_API_KEY`. */
343 |   openrouterApiKey: env.OPENROUTER_API_KEY,
344 |   /** Default LLM model. From `LLM_DEFAULT_MODEL`. */
345 |   llmDefaultModel: env.LLM_DEFAULT_MODEL,
346 |   /** Default LLM temperature. From `LLM_DEFAULT_TEMPERATURE`. */
347 |   llmDefaultTemperature: env.LLM_DEFAULT_TEMPERATURE,
348 |   /** Default LLM top_p. From `LLM_DEFAULT_TOP_P`. */
349 |   llmDefaultTopP: env.LLM_DEFAULT_TOP_P,
350 |   /** Default LLM max tokens. From `LLM_DEFAULT_MAX_TOKENS`. */
351 |   llmDefaultMaxTokens: env.LLM_DEFAULT_MAX_TOKENS,
352 |   /** Default LLM top_k. From `LLM_DEFAULT_TOP_K`. */
353 |   llmDefaultTopK: env.LLM_DEFAULT_TOP_K,
354 |   /** Default LLM min_p. From `LLM_DEFAULT_MIN_P`. */
355 |   llmDefaultMinP: env.LLM_DEFAULT_MIN_P,
356 |   /** Gemini API Key. From `GEMINI_API_KEY`. */
357 |   geminiApiKey: env.GEMINI_API_KEY,
358 | 
359 |   /** OAuth Proxy configurations. Undefined if no related env vars are set. */
360 |   oauthProxy:
361 |     env.OAUTH_PROXY_AUTHORIZATION_URL ||
362 |     env.OAUTH_PROXY_TOKEN_URL ||
363 |     env.OAUTH_PROXY_REVOCATION_URL ||
364 |     env.OAUTH_PROXY_ISSUER_URL ||
365 |     env.OAUTH_PROXY_SERVICE_DOCUMENTATION_URL ||
366 |     env.OAUTH_PROXY_DEFAULT_CLIENT_REDIRECT_URIS
367 |       ? {
368 |           authorizationUrl: env.OAUTH_PROXY_AUTHORIZATION_URL,
369 |           tokenUrl: env.OAUTH_PROXY_TOKEN_URL,
370 |           revocationUrl: env.OAUTH_PROXY_REVOCATION_URL,
371 |           issuerUrl: env.OAUTH_PROXY_ISSUER_URL,
372 |           serviceDocumentationUrl: env.OAUTH_PROXY_SERVICE_DOCUMENTATION_URL,
373 |           defaultClientRedirectUris:
374 |             env.OAUTH_PROXY_DEFAULT_CLIENT_REDIRECT_URIS?.split(",")
375 |               .map((uri) => uri.trim())
376 |               .filter(Boolean),
377 |         }
378 |       : undefined,
379 |   /** Base directory for filesystem operations. From `FS_BASE_DIRECTORY`. If set, operations are restricted to this path. Will be an absolute path. */
380 |   fsBaseDirectory: resolvedFsBaseDirectory,
381 | };
382 | 
383 | /**
384 |  * Configured logging level for the application.
385 |  * Exported for convenience.
386 |  */
387 | export const logLevel: string = config.logLevel;
388 | 
389 | /**
390 |  * Configured runtime environment ("development", "production", etc.).
391 |  * Exported for convenience.
392 |  */
393 | export const environment: string = config.environment;
394 | 
```

--------------------------------------------------------------------------------
/src/utils/internal/errorHandler.ts:
--------------------------------------------------------------------------------

```typescript
  1 | /**
  2 |  * @fileoverview This module provides utilities for robust error handling.
  3 |  * It defines structures for error context, options for handling errors,
  4 |  * and mappings for classifying errors. The main `ErrorHandler` class
  5 |  * offers static methods for consistent error processing, logging, and transformation.
  6 |  * @module src/utils/internal/errorHandler
  7 |  */
  8 | import { BaseErrorCode, McpError } from "../../types-global/errors.js";
  9 | import { generateUUID, sanitizeInputForLogging } from "../index.js";
 10 | import { logger } from "./logger.js";
 11 | import { RequestContext } from "./requestContext.js";
 12 | 
 13 | /**
 14 |  * Defines a generic structure for providing context with errors.
 15 |  * This context can include identifiers like `requestId` or any other relevant
 16 |  * key-value pairs that aid in debugging or understanding the error's circumstances.
 17 |  */
 18 | export interface ErrorContext {
 19 |   /**
 20 |    * A unique identifier for the request or operation during which the error occurred.
 21 |    * Useful for tracing errors through logs and distributed systems.
 22 |    */
 23 |   requestId?: string;
 24 | 
 25 |   /**
 26 |    * Allows for arbitrary additional context information.
 27 |    * Keys are strings, and values can be of any type.
 28 |    */
 29 |   [key: string]: unknown;
 30 | }
 31 | 
 32 | /**
 33 |  * Configuration options for the `ErrorHandler.handleError` method.
 34 |  * These options control how an error is processed, logged, and whether it's rethrown.
 35 |  */
 36 | export interface ErrorHandlerOptions {
 37 |   /**
 38 |    * The context of the operation that caused the error.
 39 |    * This can include `requestId` and other relevant debugging information.
 40 |    */
 41 |   context?: ErrorContext;
 42 | 
 43 |   /**
 44 |    * A descriptive name of the operation being performed when the error occurred.
 45 |    * This helps in identifying the source or nature of the error in logs.
 46 |    * Example: "UserLogin", "ProcessPayment", "FetchUserProfile".
 47 |    */
 48 |   operation: string;
 49 | 
 50 |   /**
 51 |    * The input data or parameters that were being processed when the error occurred.
 52 |    * This input will be sanitized before logging to prevent sensitive data exposure.
 53 |    */
 54 |   input?: unknown;
 55 | 
 56 |   /**
 57 |    * If true, the (potentially transformed) error will be rethrown after handling.
 58 |    * Defaults to `false`.
 59 |    */
 60 |   rethrow?: boolean;
 61 | 
 62 |   /**
 63 |    * A specific `BaseErrorCode` to assign to the error, overriding any
 64 |    * automatically determined error code.
 65 |    */
 66 |   errorCode?: BaseErrorCode;
 67 | 
 68 |   /**
 69 |    * A custom function to map or transform the original error into a new `Error` instance.
 70 |    * If provided, this function is used instead of the default `McpError` creation.
 71 |    * @param error - The original error that occurred.
 72 |    * @returns The transformed error.
 73 |    */
 74 |   errorMapper?: (error: unknown) => Error;
 75 | 
 76 |   /**
 77 |    * If true, stack traces will be included in the logs.
 78 |    * Defaults to `true`.
 79 |    */
 80 |   includeStack?: boolean;
 81 | 
 82 |   /**
 83 |    * If true, indicates that the error is critical and might require immediate attention
 84 |    * or could lead to system instability. This is primarily for logging and alerting.
 85 |    * Defaults to `false`.
 86 |    */
 87 |   critical?: boolean;
 88 | }
 89 | 
 90 | /**
 91 |  * Defines a basic rule for mapping errors based on patterns.
 92 |  * Used internally by `COMMON_ERROR_PATTERNS` and as a base for `ErrorMapping`.
 93 |  */
 94 | export interface BaseErrorMapping {
 95 |   /**
 96 |    * A string or regular expression to match against the error message.
 97 |    * If a string is provided, it's typically used for substring matching (case-insensitive).
 98 |    */
 99 |   pattern: string | RegExp;
100 | 
101 |   /**
102 |    * The `BaseErrorCode` to assign if the pattern matches.
103 |    */
104 |   errorCode: BaseErrorCode;
105 | 
106 |   /**
107 |    * An optional custom message template for the mapped error.
108 |    * (Note: This property is defined but not directly used by `ErrorHandler.determineErrorCode`
109 |    * which focuses on `errorCode`. It's more relevant for custom mapping logic.)
110 |    */
111 |   messageTemplate?: string;
112 | }
113 | 
114 | /**
115 |  * Extends `BaseErrorMapping` to include a factory function for creating
116 |  * specific error instances and additional context for the mapping.
117 |  * Used by `ErrorHandler.mapError`.
118 |  * @template T The type of `Error` this mapping will produce, defaults to `Error`.
119 |  */
120 | export interface ErrorMapping<T extends Error = Error>
121 |   extends BaseErrorMapping {
122 |   /**
123 |    * A factory function that creates and returns an instance of the mapped error type `T`.
124 |    * @param error - The original error that occurred.
125 |    * @param context - Optional additional context provided in the mapping rule.
126 |    * @returns The newly created error instance.
127 |    */
128 |   factory: (error: unknown, context?: Record<string, unknown>) => T;
129 | 
130 |   /**
131 |    * Additional static context to be merged or passed to the `factory` function
132 |    * when this mapping rule is applied.
133 |    */
134 |   additionalContext?: Record<string, unknown>;
135 | }
136 | 
137 | /**
138 |  * Maps standard JavaScript error constructor names to `BaseErrorCode` values.
139 |  * @private
140 |  */
141 | const ERROR_TYPE_MAPPINGS: Readonly<Record<string, BaseErrorCode>> = {
142 |   SyntaxError: BaseErrorCode.VALIDATION_ERROR,
143 |   TypeError: BaseErrorCode.VALIDATION_ERROR,
144 |   ReferenceError: BaseErrorCode.INTERNAL_ERROR,
145 |   RangeError: BaseErrorCode.VALIDATION_ERROR,
146 |   URIError: BaseErrorCode.VALIDATION_ERROR,
147 |   EvalError: BaseErrorCode.INTERNAL_ERROR,
148 | };
149 | 
150 | /**
151 |  * Array of `BaseErrorMapping` rules to classify errors by message/name patterns.
152 |  * Order matters: more specific patterns should precede generic ones.
153 |  * @private
154 |  */
155 | const COMMON_ERROR_PATTERNS: ReadonlyArray<Readonly<BaseErrorMapping>> = [
156 |   {
157 |     pattern:
158 |       /auth|unauthorized|unauthenticated|not.*logged.*in|invalid.*token|expired.*token/i,
159 |     errorCode: BaseErrorCode.UNAUTHORIZED,
160 |   },
161 |   {
162 |     pattern: /permission|forbidden|access.*denied|not.*allowed/i,
163 |     errorCode: BaseErrorCode.FORBIDDEN,
164 |   },
165 |   {
166 |     pattern: /not found|missing|no such|doesn't exist|couldn't find/i,
167 |     errorCode: BaseErrorCode.NOT_FOUND,
168 |   },
169 |   {
170 |     pattern:
171 |       /invalid|validation|malformed|bad request|wrong format|missing required/i,
172 |     errorCode: BaseErrorCode.VALIDATION_ERROR,
173 |   },
174 |   {
175 |     pattern: /conflict|already exists|duplicate|unique constraint/i,
176 |     errorCode: BaseErrorCode.CONFLICT,
177 |   },
178 |   {
179 |     pattern: /rate limit|too many requests|throttled/i,
180 |     errorCode: BaseErrorCode.RATE_LIMITED,
181 |   },
182 |   {
183 |     pattern: /timeout|timed out|deadline exceeded/i,
184 |     errorCode: BaseErrorCode.TIMEOUT,
185 |   },
186 |   {
187 |     pattern: /service unavailable|bad gateway|gateway timeout|upstream error/i,
188 |     errorCode: BaseErrorCode.SERVICE_UNAVAILABLE,
189 |   },
190 | ];
191 | 
192 | /**
193 |  * Creates a "safe" RegExp for testing error messages.
194 |  * Ensures case-insensitivity and removes the global flag.
195 |  * @param pattern - The string or RegExp pattern.
196 |  * @returns A new RegExp instance.
197 |  * @private
198 |  */
199 | function createSafeRegex(pattern: string | RegExp): RegExp {
200 |   if (pattern instanceof RegExp) {
201 |     let flags = pattern.flags.replace("g", "");
202 |     if (!flags.includes("i")) {
203 |       flags += "i";
204 |     }
205 |     return new RegExp(pattern.source, flags);
206 |   }
207 |   return new RegExp(pattern, "i");
208 | }
209 | 
210 | /**
211 |  * Retrieves a descriptive name for an error object or value.
212 |  * @param error - The error object or value.
213 |  * @returns A string representing the error's name or type.
214 |  * @private
215 |  */
216 | function getErrorName(error: unknown): string {
217 |   if (error instanceof Error) {
218 |     return error.name || "Error";
219 |   }
220 |   if (error === null) {
221 |     return "NullValueEncountered";
222 |   }
223 |   if (error === undefined) {
224 |     return "UndefinedValueEncountered";
225 |   }
226 |   if (
227 |     typeof error === "object" &&
228 |     error !== null &&
229 |     error.constructor &&
230 |     typeof error.constructor.name === "string" &&
231 |     error.constructor.name !== "Object"
232 |   ) {
233 |     return `${error.constructor.name}Encountered`;
234 |   }
235 |   return `${typeof error}Encountered`;
236 | }
237 | 
238 | /**
239 |  * Extracts a message string from an error object or value.
240 |  * @param error - The error object or value.
241 |  * @returns The error message string.
242 |  * @private
243 |  */
244 | function getErrorMessage(error: unknown): string {
245 |   if (error instanceof Error) {
246 |     return error.message;
247 |   }
248 |   if (error === null) {
249 |     return "Null value encountered as error";
250 |   }
251 |   if (error === undefined) {
252 |     return "Undefined value encountered as error";
253 |   }
254 |   if (typeof error === "string") {
255 |     return error;
256 |   }
257 |   try {
258 |     const str = String(error);
259 |     if (str === "[object Object]" && error !== null) {
260 |       try {
261 |         return `Non-Error object encountered: ${JSON.stringify(error)}`;
262 |       } catch (stringifyError) {
263 |         return `Unstringifyable non-Error object encountered (constructor: ${error.constructor?.name || "Unknown"})`;
264 |       }
265 |     }
266 |     return str;
267 |   } catch (e) {
268 |     return `Error converting error to string: ${e instanceof Error ? e.message : "Unknown conversion error"}`;
269 |   }
270 | }
271 | 
272 | /**
273 |  * A utility class providing static methods for comprehensive error handling.
274 |  */
275 | export class ErrorHandler {
276 |   /**
277 |    * Determines an appropriate `BaseErrorCode` for a given error.
278 |    * Checks `McpError` instances, `ERROR_TYPE_MAPPINGS`, and `COMMON_ERROR_PATTERNS`.
279 |    * Defaults to `BaseErrorCode.INTERNAL_ERROR`.
280 |    * @param error - The error instance or value to classify.
281 |    * @returns The determined error code.
282 |    */
283 |   public static determineErrorCode(error: unknown): BaseErrorCode {
284 |     if (error instanceof McpError) {
285 |       return error.code;
286 |     }
287 | 
288 |     const errorName = getErrorName(error);
289 |     const errorMessage = getErrorMessage(error);
290 | 
291 |     if (errorName in ERROR_TYPE_MAPPINGS) {
292 |       return ERROR_TYPE_MAPPINGS[errorName as keyof typeof ERROR_TYPE_MAPPINGS];
293 |     }
294 | 
295 |     for (const mapping of COMMON_ERROR_PATTERNS) {
296 |       const regex = createSafeRegex(mapping.pattern);
297 |       if (regex.test(errorMessage) || regex.test(errorName)) {
298 |         return mapping.errorCode;
299 |       }
300 |     }
301 |     return BaseErrorCode.INTERNAL_ERROR;
302 |   }
303 | 
304 |   /**
305 |    * Handles an error with consistent logging and optional transformation.
306 |    * Sanitizes input, determines error code, logs details, and can rethrow.
307 |    * @param error - The error instance or value that occurred.
308 |    * @param options - Configuration for handling the error.
309 |    * @returns The handled (and potentially transformed) error instance.
310 |    */
311 |   public static handleError(
312 |     error: unknown,
313 |     options: ErrorHandlerOptions,
314 |   ): Error {
315 |     const {
316 |       context = {},
317 |       operation,
318 |       input,
319 |       rethrow = false,
320 |       errorCode: explicitErrorCode,
321 |       includeStack = true,
322 |       critical = false,
323 |       errorMapper,
324 |     } = options;
325 | 
326 |     const sanitizedInput =
327 |       input !== undefined ? sanitizeInputForLogging(input) : undefined;
328 |     const originalErrorName = getErrorName(error);
329 |     const originalErrorMessage = getErrorMessage(error);
330 |     const originalStack = error instanceof Error ? error.stack : undefined;
331 | 
332 |     let finalError: Error;
333 |     let loggedErrorCode: BaseErrorCode;
334 | 
335 |     const errorDetailsSeed =
336 |       error instanceof McpError &&
337 |       typeof error.details === "object" &&
338 |       error.details !== null
339 |         ? { ...error.details }
340 |         : {};
341 | 
342 |     const consolidatedDetails: Record<string, unknown> = {
343 |       ...errorDetailsSeed,
344 |       ...context,
345 |       originalErrorName,
346 |       originalMessage: originalErrorMessage,
347 |     };
348 |     if (
349 |       originalStack &&
350 |       !(error instanceof McpError && error.details?.originalStack)
351 |     ) {
352 |       consolidatedDetails.originalStack = originalStack;
353 |     }
354 | 
355 |     if (error instanceof McpError) {
356 |       loggedErrorCode = error.code;
357 |       finalError = errorMapper
358 |         ? errorMapper(error)
359 |         : new McpError(error.code, error.message, consolidatedDetails);
360 |     } else {
361 |       loggedErrorCode =
362 |         explicitErrorCode || ErrorHandler.determineErrorCode(error);
363 |       const message = `Error in ${operation}: ${originalErrorMessage}`;
364 |       finalError = errorMapper
365 |         ? errorMapper(error)
366 |         : new McpError(loggedErrorCode, message, consolidatedDetails);
367 |     }
368 | 
369 |     if (
370 |       finalError !== error &&
371 |       error instanceof Error &&
372 |       finalError instanceof Error &&
373 |       !finalError.stack &&
374 |       error.stack
375 |     ) {
376 |       finalError.stack = error.stack;
377 |     }
378 | 
379 |     const logRequestId =
380 |       typeof context.requestId === "string" && context.requestId
381 |         ? context.requestId
382 |         : generateUUID();
383 | 
384 |     const logTimestamp =
385 |       typeof context.timestamp === "string" && context.timestamp
386 |         ? context.timestamp
387 |         : new Date().toISOString();
388 | 
389 |     const logPayload: Record<string, unknown> = {
390 |       requestId: logRequestId,
391 |       timestamp: logTimestamp,
392 |       operation,
393 |       input: sanitizedInput,
394 |       critical,
395 |       errorCode: loggedErrorCode,
396 |       originalErrorType: originalErrorName,
397 |       finalErrorType: getErrorName(finalError),
398 |       ...Object.fromEntries(
399 |         Object.entries(context).filter(
400 |           ([key]) => key !== "requestId" && key !== "timestamp",
401 |         ),
402 |       ),
403 |     };
404 | 
405 |     if (finalError instanceof McpError && finalError.details) {
406 |       logPayload.errorDetails = finalError.details;
407 |     } else {
408 |       logPayload.errorDetails = consolidatedDetails;
409 |     }
410 | 
411 |     if (includeStack) {
412 |       const stack =
413 |         finalError instanceof Error ? finalError.stack : originalStack;
414 |       if (stack) {
415 |         logPayload.stack = stack;
416 |       }
417 |     }
418 | 
419 |     logger.error(
420 |       `Error in ${operation}: ${finalError.message || originalErrorMessage}`,
421 |       logPayload as unknown as RequestContext, // Cast to RequestContext for logger compatibility
422 |     );
423 | 
424 |     if (rethrow) {
425 |       throw finalError;
426 |     }
427 |     return finalError;
428 |   }
429 | 
430 |   /**
431 |    * Maps an error to a specific error type `T` based on `ErrorMapping` rules.
432 |    * Returns original/default error if no mapping matches.
433 |    * @template T The target error type, extending `Error`.
434 |    * @param error - The error instance or value to map.
435 |    * @param mappings - An array of mapping rules to apply.
436 |    * @param defaultFactory - Optional factory for a default error if no mapping matches.
437 |    * @returns The mapped error of type `T`, or the original/defaulted error.
438 |    */
439 |   public static mapError<T extends Error>(
440 |     error: unknown,
441 |     mappings: ReadonlyArray<ErrorMapping<T>>,
442 |     defaultFactory?: (error: unknown, context?: Record<string, unknown>) => T,
443 |   ): T | Error {
444 |     const errorMessage = getErrorMessage(error);
445 |     const errorName = getErrorName(error);
446 | 
447 |     for (const mapping of mappings) {
448 |       const regex = createSafeRegex(mapping.pattern);
449 |       if (regex.test(errorMessage) || regex.test(errorName)) {
450 |         return mapping.factory(error, mapping.additionalContext);
451 |       }
452 |     }
453 | 
454 |     if (defaultFactory) {
455 |       return defaultFactory(error);
456 |     }
457 |     return error instanceof Error ? error : new Error(String(error));
458 |   }
459 | 
460 |   /**
461 |    * Formats an error into a consistent object structure for API responses or structured logging.
462 |    * @param error - The error instance or value to format.
463 |    * @returns A structured representation of the error.
464 |    */
465 |   public static formatError(error: unknown): Record<string, unknown> {
466 |     if (error instanceof McpError) {
467 |       return {
468 |         code: error.code,
469 |         message: error.message,
470 |         details:
471 |           typeof error.details === "object" && error.details !== null
472 |             ? error.details
473 |             : {},
474 |       };
475 |     }
476 | 
477 |     if (error instanceof Error) {
478 |       return {
479 |         code: ErrorHandler.determineErrorCode(error),
480 |         message: error.message,
481 |         details: { errorType: error.name || "Error" },
482 |       };
483 |     }
484 | 
485 |     return {
486 |       code: BaseErrorCode.UNKNOWN_ERROR,
487 |       message: getErrorMessage(error),
488 |       details: { errorType: getErrorName(error) },
489 |     };
490 |   }
491 | 
492 |   /**
493 |    * Safely executes a function (sync or async) and handles errors using `ErrorHandler.handleError`.
494 |    * The error is always rethrown.
495 |    * @template T The expected return type of the function `fn`.
496 |    * @param fn - The function to execute.
497 |    * @param options - Error handling options (excluding `rethrow`).
498 |    * @returns A promise resolving with the result of `fn` if successful.
499 |    * @throws {McpError | Error} The error processed by `ErrorHandler.handleError`.
500 |    * @example
501 |    * ```typescript
502 |    * async function fetchData(userId: string, context: RequestContext) {
503 |    *   return ErrorHandler.tryCatch(
504 |    *     async () => {
505 |    *       const response = await fetch(`/api/users/${userId}`);
506 |    *       if (!response.ok) throw new Error(`Failed to fetch user: ${response.status}`);
507 |    *       return response.json();
508 |    *     },
509 |    *     { operation: 'fetchUserData', context, input: { userId } }
510 |    *   );
511 |    * }
512 |    * ```
513 |    */
514 |   public static async tryCatch<T>(
515 |     fn: () => Promise<T> | T,
516 |     options: Omit<ErrorHandlerOptions, "rethrow">,
517 |   ): Promise<T> {
518 |     try {
519 |       return await Promise.resolve(fn());
520 |     } catch (error) {
521 |       // ErrorHandler.handleError will return the error to be thrown.
522 |       throw ErrorHandler.handleError(error, { ...options, rethrow: true });
523 |     }
524 |   }
525 | }
526 | 
```

--------------------------------------------------------------------------------
/scripts/tree.ts:
--------------------------------------------------------------------------------

```typescript
  1 | #!/usr/bin/env node
  2 | 
  3 | /**
  4 |  * Directory Tree Generation Operation
  5 |  * ==================================
  6 |  * 
  7 |  * A utility for generating visual tree representations of the project's directory
  8 |  * structure with configurable depth control and gitignore integration.
  9 |  *
 10 |  * This operation creates a formatted markdown file containing a hierarchical
 11 |  * representation of directories and files, respecting ignore patterns and
 12 |  * applying configurable filtering.
 13 |  * 
 14 |  * Features:
 15 |  * - Respects .gitignore patterns and common exclusions
 16 |  * - Configurable maximum depth traversal
 17 |  * - Customizable output location
 18 |  * - Sorting with directories first
 19 |  * - Cross-platform compatibility
 20 |  * 
 21 |  * @module utilities/generate.directory.tree.operation
 22 |  * 
 23 |  * Usage examples:
 24 |  * - Add to package.json: "tree": "ts-node scripts/tree.ts"
 25 |  * - Run directly: npm run tree
 26 |  * - Custom output: ts-node scripts/tree.ts ./documentation/structure.md
 27 |  * - Limit depth: ts-node scripts/tree.ts --depth=3
 28 |  * - Show help: ts-node scripts/tree.ts --help
 29 |  */
 30 | 
 31 | import fs from 'fs/promises';
 32 | import path from 'path';
 33 | 
 34 | // -----------------------------------
 35 | // Type Definitions
 36 | // -----------------------------------
 37 | 
 38 | /**
 39 |  * Standardized error category classification (using type alias instead of enum)
 40 |  */
 41 | type ErrorCategoryType = 
 42 |   | 'VALIDATION'
 43 |   | 'FILESYSTEM'
 44 |   | 'SYSTEM'
 45 |   | 'UNKNOWN';
 46 | 
 47 | const ErrorCategory = {
 48 |   VALIDATION: 'VALIDATION' as ErrorCategoryType,
 49 |   FILESYSTEM: 'FILESYSTEM' as ErrorCategoryType,
 50 |   SYSTEM: 'SYSTEM' as ErrorCategoryType,
 51 |   UNKNOWN: 'UNKNOWN' as ErrorCategoryType,
 52 | };
 53 | 
 54 | /**
 55 |  * Error severity classification (using type alias instead of enum)
 56 |  */
 57 | type ErrorSeverityLevel = 0 | 1 | 2 | 3 | 4;
 58 | 
 59 | const ErrorSeverity = {
 60 |   DEBUG: 0 as ErrorSeverityLevel,
 61 |   INFO: 1 as ErrorSeverityLevel,
 62 |   WARN: 2 as ErrorSeverityLevel,
 63 |   ERROR: 3 as ErrorSeverityLevel,
 64 |   FATAL: 4 as ErrorSeverityLevel,
 65 | };
 66 | 
 67 | /**
 68 |  * Standardized error structure for consistent error handling
 69 |  */
 70 | interface StandardizedApplicationErrorObject {
 71 |   errorMessage: string;                      // Human-readable description
 72 |   errorCode: string;                         // Machine-readable identifier
 73 |   errorCategory: ErrorCategoryType;          // System area affected (using type alias)
 74 |   errorSeverity: ErrorSeverityLevel;         // How critical the error is (using type alias)
 75 |   errorTimestamp: string;                    // When the error occurred
 76 |   errorContext: Record<string, unknown>;     // Additional relevant data
 77 |   errorStack?: string;                       // Stack trace if available
 78 | }
 79 | 
 80 | /**
 81 |  * Successful result from an operation
 82 |  */
 83 | interface OperationResultSuccess<DataType> {
 84 |   resultSuccessful: true;
 85 |   resultData: DataType;
 86 | }
 87 | 
 88 | /**
 89 |  * Failed result from an operation
 90 |  */
 91 | interface OperationResultFailure<ErrorType> {
 92 |   resultSuccessful: false;
 93 |   resultError: ErrorType;
 94 | }
 95 | 
 96 | /**
 97 |  * Combined result type for operations
 98 |  */
 99 | type OperationResult<DataType, ErrorType = StandardizedApplicationErrorObject> = 
100 |   | OperationResultSuccess<DataType>
101 |   | OperationResultFailure<ErrorType>;
102 | 
103 | /**
104 |  * Configuration options for the tree generation operation
105 |  */
106 | interface TreeGenerationConfiguration {
107 |   treeOutputFilePath: string;
108 |   maximumDirectoryDepth: number;
109 |   showHelpText: boolean;
110 | }
111 | 
112 | /**
113 |  * Definition of a gitignore pattern with parsing metadata
114 |  */
115 | interface GitignorePatternDefinition {
116 |   patternText: string;
117 |   isNegatedPattern: boolean;
118 |   regexPattern: string;
119 | }
120 | 
121 | /**
122 |  * Result from the tree generation operation
123 |  */
124 | interface TreeGenerationResult {
125 |   projectName: string;
126 |   treeOutputFilePath: string;
127 |   treeContentLength: number;
128 |   maximumDepthApplied: number;
129 |   generationTimestamp: string;
130 | }
131 | 
132 | // -----------------------------------
133 | // Constants
134 | // -----------------------------------
135 | 
136 | /**
137 |  * Default patterns to always ignore regardless of gitignore contents
138 |  */
139 | const DEFAULT_IGNORE_PATTERNS: string[] = [
140 |   '.git', 
141 |   'node_modules', 
142 |   '.DS_Store', 
143 |   'dist', 
144 |   'build'
145 | ];
146 | 
147 | /**
148 |  * Default output path for the generated tree
149 |  */
150 | const DEFAULT_OUTPUT_PATH = 'docs/tree.md';
151 | 
152 | /**
153 |  * Help text displayed when requested
154 |  */
155 | const HELP_TEXT = `
156 | Directory Tree Generator - Project structure visualization tool
157 | 
158 | Usage:
159 |   node dist/utilities/generate.directory.tree.operation.js [output-path] [--depth=<number>] [--help]
160 | 
161 | Options:
162 |   output-path      Custom file path for the tree output (default: docs/tree.md)
163 |   --depth=<number> Maximum directory depth to display (default: unlimited)
164 |   --help           Show this help message
165 | `;
166 | 
167 | // -----------------------------------
168 | // Utility Functions
169 | // -----------------------------------
170 | 
171 | /**
172 |  * Creates a standardized success result
173 |  * 
174 |  * @param data - The data to include in the success result
175 |  * @returns A standardized success result object
176 |  */
177 | function createSuccessResult<DataType>(data: DataType): OperationResultSuccess<DataType> {
178 |   return { resultSuccessful: true, resultData: data };
179 | }
180 | 
181 | /**
182 |  * Creates a standardized failure result
183 |  * 
184 |  * @param error - The error to include in the failure result
185 |  * @returns A standardized failure result object
186 |  */
187 | function createFailureResult<ErrorType>(error: ErrorType): OperationResultFailure<ErrorType> {
188 |   return { resultSuccessful: false, resultError: error };
189 | }
190 | 
191 | /**
192 |  * Creates a standardized error object
193 |  * 
194 |  * @param message - Human-readable error message
195 |  * @param code - Machine-readable error code
196 |  * @param category - Error category classification
197 |  * @param severity - Error severity level
198 |  * @param context - Additional context data
199 |  * @returns A standardized error object
200 |  */
201 | function createStandardizedError(
202 |   message: string,
203 |   code: string,
204 |   category: ErrorCategoryType, // Use the type alias
205 |   severity: ErrorSeverityLevel, // Use the type alias
206 |   context: Record<string, unknown> = {}
207 | ): StandardizedApplicationErrorObject {
208 |   return {
209 |     errorMessage: message,
210 |     errorCode: code,
211 |     errorCategory: category,
212 |     errorSeverity: severity,
213 |     errorTimestamp: new Date().toISOString(),
214 |     errorContext: context
215 |   };
216 | }
217 | 
218 | /**
219 |  * Converts an exception to a standardized error object
220 |  * 
221 |  * @param exception - The caught exception
222 |  * @param defaultMessage - Fallback message if exception is not an Error object
223 |  * @returns A standardized error object
224 |  */
225 | function wrapExceptionAsStandardizedError(
226 |   exception: unknown,
227 |   defaultMessage: string
228 | ): StandardizedApplicationErrorObject {
229 |   const errorMessage = exception instanceof Error ? exception.message : defaultMessage;
230 |   const errorStack = exception instanceof Error ? exception.stack : undefined;
231 |   
232 |   return {
233 |     errorMessage,
234 |     errorCode: 'UNEXPECTED_ERROR',
235 |     errorCategory: ErrorCategory.UNKNOWN, // Use the constant object
236 |     errorSeverity: ErrorSeverity.ERROR, // Use the constant object
237 |     errorTimestamp: new Date().toISOString(),
238 |     errorContext: { originalException: exception },
239 |     errorStack
240 |   };
241 | }
242 | 
243 | // -----------------------------------
244 | // Implementation Functions
245 | // -----------------------------------
246 | 
247 | /**
248 |  * Parses command line arguments to extract configuration options
249 |  * 
250 |  * @param commandLineArguments - Array of arguments from process.argv
251 |  * @returns Configuration object for tree generation
252 |  */
253 | function parseCommandLineArguments(
254 |   commandLineArguments: string[]
255 | ): TreeGenerationConfiguration {
256 |   let treeOutputFilePath = DEFAULT_OUTPUT_PATH;
257 |   let maximumDirectoryDepth = Infinity;
258 |   let showHelpText = false;
259 | 
260 |   for (const argumentValue of commandLineArguments) {
261 |     if (argumentValue === '--help') {
262 |       showHelpText = true;
263 |     } else if (argumentValue.startsWith('--depth=')) {
264 |       const depthValue = argumentValue.split('=')[1];
265 |       const parsedDepth = parseInt(depthValue, 10);
266 |       
267 |       if (isNaN(parsedDepth) || parsedDepth < 1) {
268 |         console.error('Invalid depth value. Using unlimited depth.');
269 |         maximumDirectoryDepth = Infinity;
270 |       } else {
271 |         maximumDirectoryDepth = parsedDepth;
272 |       }
273 |     } else if (!argumentValue.startsWith('--')) {
274 |       // If it's not an option flag, assume it's the output path
275 |       treeOutputFilePath = argumentValue;
276 |     }
277 |   }
278 | 
279 |   return {
280 |     treeOutputFilePath,
281 |     maximumDirectoryDepth,
282 |     showHelpText
283 |   };
284 | }
285 | 
286 | /**
287 |  * Loads and parses patterns from the .gitignore file
288 |  * 
289 |  * @returns Promise resolving to an array of parsed gitignore patterns
290 |  */
291 | async function loadGitignorePatternDefinitions(): Promise<OperationResult<GitignorePatternDefinition[]>> {
292 |   try {
293 |     const gitignoreContent = await fs.readFile('.gitignore', 'utf-8');
294 |     
295 |     const patternDefinitions = gitignoreContent
296 |       .split('\n')
297 |       .map(line => line.trim())
298 |       // Remove comments, empty lines, and lines with just whitespace
299 |       .filter(line => line && !line.startsWith('#') && line.trim() !== '')
300 |       // Process each pattern
301 |       .map(pattern => ({
302 |         patternText: pattern.startsWith('!') ? pattern.slice(1) : pattern,
303 |         isNegatedPattern: pattern.startsWith('!'),
304 |         // Convert glob patterns to regex-compatible strings (simplified approach)
305 |         regexPattern: pattern
306 |           .replace(/\./g, '\\.') // Escape dots first
307 |           .replace(/\*/g, '.*')  // Convert * to .*
308 |           .replace(/\?/g, '.')   // Convert ? to .
309 |           .replace(/\/$/, '(/.*)?') // Handle directory indicators
310 |       }));
311 |     
312 |     return createSuccessResult(patternDefinitions);
313 |   } catch (exceptionObject) {
314 |     console.warn('No .gitignore file found, using default patterns only');
315 |     return createSuccessResult([]);
316 |   }
317 | }
318 | 
319 | /**
320 |  * Checks if a given file path should be ignored based on patterns
321 |  * 
322 |  * @param entryPath - The relative path to check
323 |  * @param ignorePatternDefinitions - Array of parsed gitignore patterns
324 |  * @returns Boolean indicating if the path should be ignored
325 |  */
326 | function checkPathShouldBeIgnored(
327 |   entryPath: string, 
328 |   ignorePatternDefinitions: GitignorePatternDefinition[]
329 | ): boolean {
330 |   // Always check default patterns first
331 |   if (DEFAULT_IGNORE_PATTERNS.some(pattern => entryPath.includes(pattern))) {
332 |     return true;
333 |   }
334 | 
335 |   let shouldBeIgnored = false;
336 |   
337 |   for (const { patternText, isNegatedPattern, regexPattern } of ignorePatternDefinitions) {
338 |     // Convert the pattern to a proper regex
339 |     const compiledRegexPattern = new RegExp(`^${regexPattern}$|/${regexPattern}$|/${regexPattern}/`);
340 |     
341 |     if (compiledRegexPattern.test(entryPath)) {
342 |       // If it's a negation pattern (!pattern), this file should NOT be ignored
343 |       // Otherwise, it should be ignored
344 |       shouldBeIgnored = !isNegatedPattern;
345 |     }
346 |   }
347 |   
348 |   return shouldBeIgnored;
349 | }
350 | 
351 | /**
352 |  * Recursively generates a tree representation of the directory structure
353 |  * 
354 |  * @param directoryPath - Path to the directory to process
355 |  * @param ignorePatternDefinitions - Array of gitignore pattern definitions
356 |  * @param prefixString - Prefix string for the current level (used for indentation)
357 |  * @param isLastEntry - Whether this is the last entry at the current level
358 |  * @param relativePathString - Relative path from the root directory
359 |  * @param currentDepthLevel - Current depth level in the traversal
360 |  * @returns Promise resolving to the string representation of the tree
361 |  */
362 | async function generateDirectoryTreeRepresentation(
363 |   directoryPath: string, 
364 |   ignorePatternDefinitions: GitignorePatternDefinition[], 
365 |   prefixString = '', 
366 |   isLastEntry = true, 
367 |   relativePathString = '', 
368 |   currentDepthLevel = 0,
369 |   maximumDepthLevel = Infinity
370 | ): Promise<OperationResult<string>> {
371 |   try {
372 |     const directoryEntries = await fs.readdir(directoryPath, { withFileTypes: true });
373 |     let treeOutputContent = '';
374 | 
375 |     // Filter and sort entries
376 |     const filteredEntries = directoryEntries
377 |       .filter(entry => {
378 |         const entryPath = path.join(relativePathString, entry.name);
379 |         return !checkPathShouldBeIgnored(entryPath, ignorePatternDefinitions);
380 |       })
381 |       .sort((a, b) => {
382 |         // Directories first, then files
383 |         if (a.isDirectory() && !b.isDirectory()) return -1;
384 |         if (!a.isDirectory() && b.isDirectory()) return 1;
385 |         return a.name.localeCompare(b.name);
386 |       });
387 | 
388 |     for (let entryIndex = 0; entryIndex < filteredEntries.length; entryIndex++) {
389 |       const entryItem = filteredEntries[entryIndex];
390 |       const isLastItem = entryIndex === filteredEntries.length - 1;
391 |       const newPrefixString = prefixString + (isLastEntry ? '    ' : '│   ');
392 |       const newRelativePath = path.join(relativePathString, entryItem.name);
393 |       
394 |       treeOutputContent += prefixString + (isLastItem ? '└── ' : '├── ') + entryItem.name + '\n';
395 | 
396 |       // Only traverse deeper if we haven't reached maximumDepthLevel
397 |       if (entryItem.isDirectory() && currentDepthLevel < maximumDepthLevel) {
398 |         const subTreeResult = await generateDirectoryTreeRepresentation(
399 |           path.join(directoryPath, entryItem.name),
400 |           ignorePatternDefinitions,
401 |           newPrefixString,
402 |           isLastItem,
403 |           newRelativePath,
404 |           currentDepthLevel + 1,
405 |           maximumDepthLevel
406 |         );
407 |         
408 |         if (subTreeResult.resultSuccessful) {
409 |           treeOutputContent += subTreeResult.resultData;
410 |         } else {
411 |           return subTreeResult; // Propagate error
412 |         }
413 |       }
414 |     }
415 | 
416 |     return createSuccessResult(treeOutputContent);
417 |   } catch (exceptionObject) {
418 |     return createFailureResult(
419 |       wrapExceptionAsStandardizedError(
420 |         exceptionObject,
421 |         `Failed to generate tree for directory: ${directoryPath}`
422 |       )
423 |     );
424 |   }
425 | }
426 | 
427 | /**
428 |  * Ensures the directory for the output file exists, creating it if needed
429 |  * 
430 |  * @param directoryPath - Path to the directory to check/create
431 |  * @returns Promise resolving to operation result
432 |  */
433 | async function ensureDirectoryExists(
434 |   directoryPath: string
435 | ): Promise<OperationResult<boolean>> {
436 |   try {
437 |     await fs.access(directoryPath);
438 |     return createSuccessResult(true);
439 |   } catch {
440 |     try {
441 |       await fs.mkdir(directoryPath, { recursive: true });
442 |       console.log(`Creating directory: ${directoryPath}`);
443 |       return createSuccessResult(true);
444 |     } catch (exceptionObject) {
445 |       return createFailureResult(
446 |         wrapExceptionAsStandardizedError(
447 |           exceptionObject,
448 |           `Failed to create directory: ${directoryPath}`
449 |         )
450 |       );
451 |     }
452 |   }
453 | }
454 | 
455 | /**
456 |  * Writes the generated tree content to a markdown file
457 |  * 
458 |  * @param projectName - Name of the project
459 |  * @param treeContent - Generated tree content
460 |  * @param outputFilePath - Path where the output file should be written
461 |  * @param maximumDepthValue - Maximum depth value that was applied
462 |  * @returns Promise resolving to operation result
463 |  */
464 | async function writeTreeContentToFile(
465 |   projectName: string,
466 |   treeContent: string,
467 |   outputFilePath: string,
468 |   maximumDepthValue: number
469 | ): Promise<OperationResult<TreeGenerationResult>> {
470 |   try {
471 |     const rootDirectoryPath = process.cwd();
472 |     const outputDirectoryPath = path.dirname(path.resolve(rootDirectoryPath, outputFilePath));
473 |     
474 |     // Ensure output directory exists
475 |     const directoryResult = await ensureDirectoryExists(outputDirectoryPath);
476 |     if (!directoryResult.resultSuccessful) {
477 |       return directoryResult;
478 |     }
479 | 
480 |     // Format the timestamp
481 |     const timestamp = new Date().toISOString().replace(/T/, ' ').replace(/\..+/, '');
482 |     
483 |     // Format the markdown content
484 |     const markdownContent = `# ${projectName} - Directory Structure
485 | 
486 | Generated on: ${timestamp}
487 | 
488 | ${maximumDepthValue !== Infinity ? `_Depth limited to ${maximumDepthValue} levels_\n\n` : ''}
489 | \`\`\`
490 | ${projectName}
491 | ${treeContent}
492 | \`\`\`
493 | 
494 | _Note: This tree excludes files and directories matched by .gitignore and common patterns like node_modules._
495 | `;
496 | 
497 |     // Write the content to the file
498 |     await fs.writeFile(
499 |       path.resolve(rootDirectoryPath, outputFilePath),
500 |       markdownContent
501 |     );
502 |     
503 |     return createSuccessResult({
504 |       projectName,
505 |       treeOutputFilePath: outputFilePath,
506 |       treeContentLength: treeContent.length,
507 |       maximumDepthApplied: maximumDepthValue,
508 |       generationTimestamp: timestamp
509 |     });
510 |   } catch (exceptionObject) {
511 |     return createFailureResult(
512 |       wrapExceptionAsStandardizedError(
513 |         exceptionObject,
514 |         `Failed to write tree to file: ${outputFilePath}`
515 |       )
516 |     );
517 |   }
518 | }
519 | 
520 | /**
521 |  * Main operation function that orchestrates the tree generation process
522 |  * 
523 |  * @returns Promise that resolves when the operation completes
524 |  */
525 | async function generateProjectDirectoryTree(): Promise<void> {
526 |   try {
527 |     // Parse command line arguments
528 |     const commandLineArguments = process.argv.slice(2);
529 |     const configurationSettings = parseCommandLineArguments(commandLineArguments);
530 |     
531 |     // Display help if requested
532 |     if (configurationSettings.showHelpText) {
533 |       console.log(HELP_TEXT);
534 |       process.exit(0);
535 |     }
536 |     
537 |     const rootDirectoryPath = process.cwd();
538 |     const projectName = path.basename(rootDirectoryPath);
539 |     
540 |     // Load gitignore patterns
541 |     const ignorePatternResult = await loadGitignorePatternDefinitions();
542 |     if (!ignorePatternResult.resultSuccessful) {
543 |       throw new Error(`Failed to load gitignore patterns: ${ignorePatternResult.resultError.errorMessage}`);
544 |     }
545 |     
546 |     const ignorePatternDefinitions = ignorePatternResult.resultData;
547 |     
548 |     console.log(`Generating directory tree for: ${projectName}`);
549 |     console.log(`Output path: ${configurationSettings.treeOutputFilePath}`);
550 |     
551 |     if (configurationSettings.maximumDirectoryDepth !== Infinity) {
552 |       console.log(`Maximum depth: ${configurationSettings.maximumDirectoryDepth}`);
553 |     }
554 |     
555 |     // Generate the tree structure
556 |     const treeGenerationResult = await generateDirectoryTreeRepresentation(
557 |       rootDirectoryPath, 
558 |       ignorePatternDefinitions, 
559 |       '', 
560 |       true, 
561 |       '', 
562 |       0,
563 |       configurationSettings.maximumDirectoryDepth
564 |     );
565 |     
566 |     if (!treeGenerationResult.resultSuccessful) {
567 |       throw new Error(`Failed to generate tree: ${treeGenerationResult.resultError.errorMessage}`);
568 |     }
569 |     
570 |     // Write the tree to a file
571 |     const writeResult = await writeTreeContentToFile(
572 |       projectName,
573 |       treeGenerationResult.resultData,
574 |       configurationSettings.treeOutputFilePath,
575 |       configurationSettings.maximumDirectoryDepth
576 |     );
577 |     
578 |     if (!writeResult.resultSuccessful) {
579 |       throw new Error(`Failed to write tree: ${writeResult.resultError.errorMessage}`);
580 |     }
581 |     
582 |     console.log(`✓ Successfully generated tree structure in ${configurationSettings.treeOutputFilePath}`);
583 |   } catch (exceptionObject) {
584 |     const standardizedError = wrapExceptionAsStandardizedError(
585 |       exceptionObject,
586 |       'Unhandled error during tree generation'
587 |     );
588 |     
589 |     console.error(`× Error generating tree: ${standardizedError.errorMessage}`);
590 |     process.exit(1);
591 |   }
592 | }
593 | 
594 | // -----------------------------------
595 | // Script Execution
596 | // -----------------------------------
597 | 
598 | // Execute the main operation function
599 | generateProjectDirectoryTree();
600 | 
```

--------------------------------------------------------------------------------
/src/utils/internal/logger.ts:
--------------------------------------------------------------------------------

```typescript
  1 | /**
  2 |  * @fileoverview Provides a singleton Logger class that wraps Winston for file logging
  3 |  * and supports sending MCP (Model Context Protocol) `notifications/message`.
  4 |  * It handles different log levels compliant with RFC 5424 and MCP specifications.
  5 |  * @module src/utils/internal/logger
  6 |  */
  7 | import fs from "fs";
  8 | import path from "path";
  9 | import winston from "winston";
 10 | import TransportStream from "winston-transport";
 11 | import { config } from "../../config/index.js";
 12 | import { RequestContext } from "./requestContext.js";
 13 | 
 14 | /**
 15 |  * Defines the supported logging levels based on RFC 5424 Syslog severity levels,
 16 |  * as used by the Model Context Protocol (MCP).
 17 |  * Levels are: 'debug'(7), 'info'(6), 'notice'(5), 'warning'(4), 'error'(3), 'crit'(2), 'alert'(1), 'emerg'(0).
 18 |  * Lower numeric values indicate higher severity.
 19 |  */
 20 | export type McpLogLevel =
 21 |   | "debug"
 22 |   | "info"
 23 |   | "notice"
 24 |   | "warning"
 25 |   | "error"
 26 |   | "crit"
 27 |   | "alert"
 28 |   | "emerg";
 29 | 
 30 | /**
 31 |  * Numeric severity mapping for MCP log levels (lower is more severe).
 32 |  * @private
 33 |  */
 34 | const mcpLevelSeverity: Record<McpLogLevel, number> = {
 35 |   emerg: 0,
 36 |   alert: 1,
 37 |   crit: 2,
 38 |   error: 3,
 39 |   warning: 4,
 40 |   notice: 5,
 41 |   info: 6,
 42 |   debug: 7,
 43 | };
 44 | 
 45 | /**
 46 |  * Maps MCP log levels to Winston's core levels for file logging.
 47 |  * @private
 48 |  */
 49 | const mcpToWinstonLevel: Record<
 50 |   McpLogLevel,
 51 |   "debug" | "info" | "warn" | "error"
 52 | > = {
 53 |   debug: "debug",
 54 |   info: "info",
 55 |   notice: "info",
 56 |   warning: "warn",
 57 |   error: "error",
 58 |   crit: "error",
 59 |   alert: "error",
 60 |   emerg: "error",
 61 | };
 62 | 
 63 | /**
 64 |  * Interface for a more structured error object, primarily for formatting console logs.
 65 |  * @private
 66 |  */
 67 | interface ErrorWithMessageAndStack {
 68 |   message?: string;
 69 |   stack?: string;
 70 |   [key: string]: any;
 71 | }
 72 | 
 73 | /**
 74 |  * Interface for the payload of an MCP log notification.
 75 |  * This structure is used when sending log data via MCP `notifications/message`.
 76 |  */
 77 | export interface McpLogPayload {
 78 |   message: string;
 79 |   context?: RequestContext;
 80 |   error?: {
 81 |     message: string;
 82 |     stack?: string;
 83 |   };
 84 |   [key: string]: any;
 85 | }
 86 | 
 87 | /**
 88 |  * Type for the `data` parameter of the `McpNotificationSender` function.
 89 |  */
 90 | export type McpNotificationData = McpLogPayload | Record<string, unknown>;
 91 | 
 92 | /**
 93 |  * Defines the signature for a function that can send MCP log notifications.
 94 |  * This function is typically provided by the MCP server instance.
 95 |  * @param level - The severity level of the log message.
 96 |  * @param data - The payload of the log notification.
 97 |  * @param loggerName - An optional name or identifier for the logger/server.
 98 |  */
 99 | export type McpNotificationSender = (
100 |   level: McpLogLevel,
101 |   data: McpNotificationData,
102 |   loggerName?: string,
103 | ) => void;
104 | 
105 | // The logsPath from config is already resolved and validated by src/config/index.ts
106 | const resolvedLogsDir = config.logsPath;
107 | const isLogsDirSafe = !!resolvedLogsDir; // If logsPath is set, it's considered safe by config logic.
108 | 
109 | /**
110 |  * Creates the Winston console log format.
111 |  * @returns The Winston log format for console output.
112 |  * @private
113 |  */
114 | function createWinstonConsoleFormat(): winston.Logform.Format {
115 |   return winston.format.combine(
116 |     winston.format.colorize(),
117 |     winston.format.timestamp({ format: "YYYY-MM-DD HH:mm:ss" }),
118 |     winston.format.printf(({ timestamp, level, message, ...meta }) => {
119 |       let metaString = "";
120 |       const metaCopy = { ...meta };
121 |       if (metaCopy.error && typeof metaCopy.error === "object") {
122 |         const errorObj = metaCopy.error as ErrorWithMessageAndStack;
123 |         if (errorObj.message) metaString += `\n  Error: ${errorObj.message}`;
124 |         if (errorObj.stack)
125 |           metaString += `\n  Stack: ${String(errorObj.stack)
126 |             .split("\n")
127 |             .map((l: string) => `    ${l}`)
128 |             .join("\n")}`;
129 |         delete metaCopy.error;
130 |       }
131 |       if (Object.keys(metaCopy).length > 0) {
132 |         try {
133 |           const remainingMetaJson = JSON.stringify(metaCopy, null, 2);
134 |           if (remainingMetaJson !== "{}")
135 |             metaString += `\n  Meta: ${remainingMetaJson}`;
136 |         } catch (stringifyError: unknown) {
137 |           const errorMessage =
138 |             stringifyError instanceof Error
139 |               ? stringifyError.message
140 |               : String(stringifyError);
141 |           metaString += `\n  Meta: [Error stringifying metadata: ${errorMessage}]`;
142 |         }
143 |       }
144 |       return `${timestamp} ${level}: ${message}${metaString}`;
145 |     }),
146 |   );
147 | }
148 | 
149 | /**
150 |  * Singleton Logger class that wraps Winston for robust logging.
151 |  * Supports file logging, conditional console logging, and MCP notifications.
152 |  */
153 | export class Logger {
154 |   private static instance: Logger;
155 |   private winstonLogger?: winston.Logger;
156 |   private initialized = false;
157 |   private mcpNotificationSender?: McpNotificationSender;
158 |   private currentMcpLevel: McpLogLevel = "info";
159 |   private currentWinstonLevel: "debug" | "info" | "warn" | "error" = "info";
160 | 
161 |   private readonly MCP_NOTIFICATION_STACK_TRACE_MAX_LENGTH = 1024;
162 |   private readonly LOG_FILE_MAX_SIZE = 5 * 1024 * 1024; // 5MB
163 |   private readonly LOG_MAX_FILES = 5;
164 | 
165 |   /** @private */
166 |   private constructor() {}
167 | 
168 |   /**
169 |    * Initializes the Winston logger instance.
170 |    * Should be called once at application startup.
171 |    * @param level - The initial minimum MCP log level.
172 |    */
173 |   public async initialize(level: McpLogLevel = "info"): Promise<void> {
174 |     if (this.initialized) {
175 |       this.warning("Logger already initialized.", {
176 |         loggerSetup: true,
177 |         requestId: "logger-init",
178 |         timestamp: new Date().toISOString(),
179 |       });
180 |       return;
181 |     }
182 |     this.currentMcpLevel = level;
183 |     this.currentWinstonLevel = mcpToWinstonLevel[level];
184 | 
185 |     let logsDirCreatedMessage: string | null = null; // This message is now informational as creation is handled by config
186 | 
187 |     if (isLogsDirSafe) {
188 |       // Directory creation is handled by config/index.ts ensureDirectory.
189 |       // We can log if it was newly created by checking if it existed before config ran,
190 |       // but that's complex. For now, we assume config handled it.
191 |       // If resolvedLogsDir is set, config ensures it exists.
192 |       if (!fs.existsSync(resolvedLogsDir)) {
193 |         // This case should ideally not be hit if config.logsPath is correctly set up and validated.
194 |         // However, if it somehow occurs (e.g. dir deleted after config init but before logger init),
195 |         // we attempt to create it.
196 |         try {
197 |           await fs.promises.mkdir(resolvedLogsDir, { recursive: true });
198 |           logsDirCreatedMessage = `Re-created logs directory (should have been created by config): ${resolvedLogsDir}`;
199 |         } catch (err: unknown) {
200 |           if (process.stdout.isTTY) {
201 |             const errorMessage =
202 |               err instanceof Error ? err.message : String(err);
203 |             console.error(
204 |               `Error creating logs directory at ${resolvedLogsDir}: ${errorMessage}. File logging disabled.`,
205 |             );
206 |           }
207 |           throw err; // Critical if logs dir cannot be ensured
208 |         }
209 |       }
210 |     }
211 | 
212 |     const fileFormat = winston.format.combine(
213 |       winston.format.timestamp(),
214 |       winston.format.errors({ stack: true }),
215 |       winston.format.json(),
216 |     );
217 | 
218 |     const transports: TransportStream[] = [];
219 |     const fileTransportOptions = {
220 |       format: fileFormat,
221 |       maxsize: this.LOG_FILE_MAX_SIZE,
222 |       maxFiles: this.LOG_MAX_FILES,
223 |       tailable: true,
224 |     };
225 | 
226 |     if (isLogsDirSafe) {
227 |       transports.push(
228 |         new winston.transports.File({
229 |           filename: path.join(resolvedLogsDir, "error.log"),
230 |           level: "error",
231 |           ...fileTransportOptions,
232 |         }),
233 |         new winston.transports.File({
234 |           filename: path.join(resolvedLogsDir, "warn.log"),
235 |           level: "warn",
236 |           ...fileTransportOptions,
237 |         }),
238 |         new winston.transports.File({
239 |           filename: path.join(resolvedLogsDir, "info.log"),
240 |           level: "info",
241 |           ...fileTransportOptions,
242 |         }),
243 |         new winston.transports.File({
244 |           filename: path.join(resolvedLogsDir, "debug.log"),
245 |           level: "debug",
246 |           ...fileTransportOptions,
247 |         }),
248 |         new winston.transports.File({
249 |           filename: path.join(resolvedLogsDir, "combined.log"),
250 |           ...fileTransportOptions,
251 |         }),
252 |       );
253 |     } else {
254 |       if (process.stdout.isTTY) {
255 |         console.warn(
256 |           "File logging disabled as logsPath is not configured or invalid.",
257 |         );
258 |       }
259 |     }
260 | 
261 |     this.winstonLogger = winston.createLogger({
262 |       level: this.currentWinstonLevel,
263 |       transports,
264 |       exitOnError: false,
265 |     });
266 | 
267 |     // Configure console transport after Winston logger is created
268 |     const consoleStatus = this._configureConsoleTransport();
269 | 
270 |     const initialContext: RequestContext = {
271 |       loggerSetup: true,
272 |       requestId: "logger-init-deferred",
273 |       timestamp: new Date().toISOString(),
274 |     };
275 |     if (logsDirCreatedMessage) {
276 |       // Log if we had to re-create it
277 |       this.info(logsDirCreatedMessage, initialContext);
278 |     }
279 |     if (consoleStatus.message) {
280 |       this.info(consoleStatus.message, initialContext);
281 |     }
282 | 
283 |     this.initialized = true;
284 |     this.info(
285 |       `Logger initialized. File logging level: ${this.currentWinstonLevel}. MCP logging level: ${this.currentMcpLevel}. Console logging: ${consoleStatus.enabled ? "enabled" : "disabled"}`,
286 |       {
287 |         loggerSetup: true,
288 |         requestId: "logger-post-init",
289 |         timestamp: new Date().toISOString(),
290 |         logsPathUsed: resolvedLogsDir,
291 |       },
292 |     );
293 |   }
294 | 
295 |   /**
296 |    * Sets the function used to send MCP 'notifications/message'.
297 |    * @param sender - The function to call for sending notifications, or undefined to disable.
298 |    */
299 |   public setMcpNotificationSender(
300 |     sender: McpNotificationSender | undefined,
301 |   ): void {
302 |     this.mcpNotificationSender = sender;
303 |     const status = sender ? "enabled" : "disabled";
304 |     this.info(`MCP notification sending ${status}.`, {
305 |       loggerSetup: true,
306 |       requestId: "logger-set-sender",
307 |       timestamp: new Date().toISOString(),
308 |     });
309 |   }
310 | 
311 |   /**
312 |    * Dynamically sets the minimum logging level.
313 |    * @param newLevel - The new minimum MCP log level to set.
314 |    */
315 |   public setLevel(newLevel: McpLogLevel): void {
316 |     const setLevelContext: RequestContext = {
317 |       loggerSetup: true,
318 |       requestId: "logger-set-level",
319 |       timestamp: new Date().toISOString(),
320 |     };
321 |     if (!this.ensureInitialized()) {
322 |       if (process.stdout.isTTY) {
323 |         console.error("Cannot set level: Logger not initialized.");
324 |       }
325 |       return;
326 |     }
327 |     if (!(newLevel in mcpLevelSeverity)) {
328 |       this.warning(
329 |         `Invalid MCP log level provided: ${newLevel}. Level not changed.`,
330 |         setLevelContext,
331 |       );
332 |       return;
333 |     }
334 | 
335 |     const oldLevel = this.currentMcpLevel;
336 |     this.currentMcpLevel = newLevel;
337 |     this.currentWinstonLevel = mcpToWinstonLevel[newLevel];
338 |     if (this.winstonLogger) {
339 |       // Ensure winstonLogger is defined
340 |       this.winstonLogger.level = this.currentWinstonLevel;
341 |     }
342 | 
343 |     const consoleStatus = this._configureConsoleTransport();
344 | 
345 |     if (oldLevel !== newLevel) {
346 |       this.info(
347 |         `Log level changed. File logging level: ${this.currentWinstonLevel}. MCP logging level: ${this.currentMcpLevel}. Console logging: ${consoleStatus.enabled ? "enabled" : "disabled"}`,
348 |         setLevelContext,
349 |       );
350 |       if (
351 |         consoleStatus.message &&
352 |         consoleStatus.message !== "Console logging status unchanged."
353 |       ) {
354 |         this.info(consoleStatus.message, setLevelContext);
355 |       }
356 |     }
357 |   }
358 | 
359 |   /**
360 |    * Configures the console transport based on the current log level and TTY status.
361 |    * Adds or removes the console transport as needed.
362 |    * @returns {{ enabled: boolean, message: string | null }} Status of console logging.
363 |    * @private
364 |    */
365 |   private _configureConsoleTransport(): {
366 |     enabled: boolean;
367 |     message: string | null;
368 |   } {
369 |     if (!this.winstonLogger) {
370 |       return {
371 |         enabled: false,
372 |         message: "Cannot configure console: Winston logger not initialized.",
373 |       };
374 |     }
375 | 
376 |     const consoleTransport = this.winstonLogger.transports.find(
377 |       (t) => t instanceof winston.transports.Console,
378 |     );
379 |     const shouldHaveConsole =
380 |       this.currentMcpLevel === "debug" && process.stdout.isTTY;
381 |     let message: string | null = null;
382 | 
383 |     if (shouldHaveConsole && !consoleTransport) {
384 |       const consoleFormat = createWinstonConsoleFormat();
385 |       this.winstonLogger.add(
386 |         new winston.transports.Console({
387 |           level: "debug", // Console always logs debug if enabled
388 |           format: consoleFormat,
389 |         }),
390 |       );
391 |       message = "Console logging enabled (level: debug, stdout is TTY).";
392 |     } else if (!shouldHaveConsole && consoleTransport) {
393 |       this.winstonLogger.remove(consoleTransport);
394 |       message = "Console logging disabled (level not debug or stdout not TTY).";
395 |     } else {
396 |       message = "Console logging status unchanged.";
397 |     }
398 |     return { enabled: shouldHaveConsole, message };
399 |   }
400 | 
401 |   /**
402 |    * Gets the singleton instance of the Logger.
403 |    * @returns The singleton Logger instance.
404 |    */
405 |   public static getInstance(): Logger {
406 |     if (!Logger.instance) {
407 |       Logger.instance = new Logger();
408 |     }
409 |     return Logger.instance;
410 |   }
411 | 
412 |   /**
413 |    * Ensures the logger has been initialized.
414 |    * @returns True if initialized, false otherwise.
415 |    * @private
416 |    */
417 |   private ensureInitialized(): boolean {
418 |     if (!this.initialized || !this.winstonLogger) {
419 |       if (process.stdout.isTTY) {
420 |         console.warn("Logger not initialized; message dropped.");
421 |       }
422 |       return false;
423 |     }
424 |     return true;
425 |   }
426 | 
427 |   /**
428 |    * Centralized log processing method.
429 |    * @param level - The MCP severity level of the message.
430 |    * @param msg - The main log message.
431 |    * @param context - Optional request context for the log.
432 |    * @param error - Optional error object associated with the log.
433 |    * @private
434 |    */
435 |   private log(
436 |     level: McpLogLevel,
437 |     msg: string,
438 |     context?: RequestContext,
439 |     error?: Error,
440 |   ): void {
441 |     if (!this.ensureInitialized()) return;
442 |     if (mcpLevelSeverity[level] > mcpLevelSeverity[this.currentMcpLevel]) {
443 |       return; // Do not log if message level is less severe than currentMcpLevel
444 |     }
445 | 
446 |     const logData: Record<string, unknown> = { ...context };
447 |     const winstonLevel = mcpToWinstonLevel[level];
448 | 
449 |     if (error) {
450 |       this.winstonLogger!.log(winstonLevel, msg, { ...logData, error });
451 |     } else {
452 |       this.winstonLogger!.log(winstonLevel, msg, logData);
453 |     }
454 | 
455 |     if (this.mcpNotificationSender) {
456 |       const mcpDataPayload: McpLogPayload = { message: msg };
457 |       if (context && Object.keys(context).length > 0)
458 |         mcpDataPayload.context = context;
459 |       if (error) {
460 |         mcpDataPayload.error = { message: error.message };
461 |         // Include stack trace in debug mode for MCP notifications, truncated for brevity
462 |         if (this.currentMcpLevel === "debug" && error.stack) {
463 |           mcpDataPayload.error.stack = error.stack.substring(
464 |             0,
465 |             this.MCP_NOTIFICATION_STACK_TRACE_MAX_LENGTH,
466 |           );
467 |         }
468 |       }
469 |       try {
470 |         const serverName =
471 |           config?.mcpServerName ?? "MCP_SERVER_NAME_NOT_CONFIGURED";
472 |         this.mcpNotificationSender(level, mcpDataPayload, serverName);
473 |       } catch (sendError: unknown) {
474 |         const errorMessage =
475 |           sendError instanceof Error ? sendError.message : String(sendError);
476 |         const internalErrorContext: RequestContext = {
477 |           requestId: context?.requestId || "logger-internal-error",
478 |           timestamp: new Date().toISOString(),
479 |           originalLevel: level,
480 |           originalMessage: msg,
481 |           sendError: errorMessage,
482 |           mcpPayload: JSON.stringify(mcpDataPayload).substring(0, 500), // Log a preview
483 |         };
484 |         this.winstonLogger!.error(
485 |           "Failed to send MCP log notification",
486 |           internalErrorContext,
487 |         );
488 |       }
489 |     }
490 |   }
491 | 
492 |   /** Logs a message at the 'debug' level. */
493 |   public debug(msg: string, context?: RequestContext): void {
494 |     this.log("debug", msg, context);
495 |   }
496 | 
497 |   /** Logs a message at the 'info' level. */
498 |   public info(msg: string, context?: RequestContext): void {
499 |     this.log("info", msg, context);
500 |   }
501 | 
502 |   /** Logs a message at the 'notice' level. */
503 |   public notice(msg: string, context?: RequestContext): void {
504 |     this.log("notice", msg, context);
505 |   }
506 | 
507 |   /** Logs a message at the 'warning' level. */
508 |   public warning(msg: string, context?: RequestContext): void {
509 |     this.log("warning", msg, context);
510 |   }
511 | 
512 |   /**
513 |    * Logs a message at the 'error' level.
514 |    * @param msg - The main log message.
515 |    * @param err - Optional. Error object or RequestContext.
516 |    * @param context - Optional. RequestContext if `err` is an Error.
517 |    */
518 |   public error(
519 |     msg: string,
520 |     err?: Error | RequestContext,
521 |     context?: RequestContext,
522 |   ): void {
523 |     const errorObj = err instanceof Error ? err : undefined;
524 |     const actualContext = err instanceof Error ? context : err;
525 |     this.log("error", msg, actualContext, errorObj);
526 |   }
527 | 
528 |   /**
529 |    * Logs a message at the 'crit' (critical) level.
530 |    * @param msg - The main log message.
531 |    * @param err - Optional. Error object or RequestContext.
532 |    * @param context - Optional. RequestContext if `err` is an Error.
533 |    */
534 |   public crit(
535 |     msg: string,
536 |     err?: Error | RequestContext,
537 |     context?: RequestContext,
538 |   ): void {
539 |     const errorObj = err instanceof Error ? err : undefined;
540 |     const actualContext = err instanceof Error ? context : err;
541 |     this.log("crit", msg, actualContext, errorObj);
542 |   }
543 | 
544 |   /**
545 |    * Logs a message at the 'alert' level.
546 |    * @param msg - The main log message.
547 |    * @param err - Optional. Error object or RequestContext.
548 |    * @param context - Optional. RequestContext if `err` is an Error.
549 |    */
550 |   public alert(
551 |     msg: string,
552 |     err?: Error | RequestContext,
553 |     context?: RequestContext,
554 |   ): void {
555 |     const errorObj = err instanceof Error ? err : undefined;
556 |     const actualContext = err instanceof Error ? context : err;
557 |     this.log("alert", msg, actualContext, errorObj);
558 |   }
559 | 
560 |   /**
561 |    * Logs a message at the 'emerg' (emergency) level.
562 |    * @param msg - The main log message.
563 |    * @param err - Optional. Error object or RequestContext.
564 |    * @param context - Optional. RequestContext if `err` is an Error.
565 |    */
566 |   public emerg(
567 |     msg: string,
568 |     err?: Error | RequestContext,
569 |     context?: RequestContext,
570 |   ): void {
571 |     const errorObj = err instanceof Error ? err : undefined;
572 |     const actualContext = err instanceof Error ? context : err;
573 |     this.log("emerg", msg, actualContext, errorObj);
574 |   }
575 | 
576 |   /**
577 |    * Logs a message at the 'emerg' (emergency) level, typically for fatal errors.
578 |    * @param msg - The main log message.
579 |    * @param err - Optional. Error object or RequestContext.
580 |    * @param context - Optional. RequestContext if `err` is an Error.
581 |    */
582 |   public fatal(
583 |     msg: string,
584 |     err?: Error | RequestContext,
585 |     context?: RequestContext,
586 |   ): void {
587 |     const errorObj = err instanceof Error ? err : undefined;
588 |     const actualContext = err instanceof Error ? context : err;
589 |     this.log("emerg", msg, actualContext, errorObj);
590 |   }
591 | }
592 | 
593 | /**
594 |  * The singleton instance of the Logger.
595 |  * Use this instance for all logging operations.
596 |  */
597 | export const logger = Logger.getInstance();
598 | 
```

--------------------------------------------------------------------------------
/src/utils/security/sanitization.ts:
--------------------------------------------------------------------------------

```typescript
  1 | /**
  2 |  * @fileoverview Provides a comprehensive `Sanitization` class for various input cleaning and validation tasks.
  3 |  * This module includes utilities for sanitizing HTML, strings, URLs, file paths, JSON, numbers,
  4 |  * and for redacting sensitive information from data intended for logging.
  5 |  * @module src/utils/security/sanitization
  6 |  */
  7 | import path from "path";
  8 | import sanitizeHtml from "sanitize-html";
  9 | import validator from "validator";
 10 | import { BaseErrorCode, McpError } from "../../types-global/errors.js";
 11 | import { logger, requestContextService } from "../index.js";
 12 | 
 13 | /**
 14 |  * Defines options for path sanitization to control how file paths are processed and validated.
 15 |  */
 16 | export interface PathSanitizeOptions {
 17 |   /** If provided, restricts sanitized paths to be relative to this directory. */
 18 |   rootDir?: string;
 19 |   /** If true, normalizes Windows backslashes to POSIX forward slashes. */
 20 |   toPosix?: boolean;
 21 |   /** If true, absolute paths are permitted (subject to `rootDir`). Default: false. */
 22 |   allowAbsolute?: boolean;
 23 | }
 24 | 
 25 | /**
 26 |  * Contains information about a path sanitization operation.
 27 |  */
 28 | export interface SanitizedPathInfo {
 29 |   /** The final sanitized and normalized path string. */
 30 |   sanitizedPath: string;
 31 |   /** The original path string before any processing. */
 32 |   originalInput: string;
 33 |   /** True if the input path was absolute after initial normalization. */
 34 |   wasAbsolute: boolean;
 35 |   /** True if an absolute path was converted to relative due to `allowAbsolute: false`. */
 36 |   convertedToRelative: boolean;
 37 |   /** The effective options used for sanitization, including defaults. */
 38 |   optionsUsed: PathSanitizeOptions;
 39 | }
 40 | 
 41 | /**
 42 |  * Defines options for context-specific string sanitization.
 43 |  */
 44 | export interface SanitizeStringOptions {
 45 |   /** The context in which the string will be used. 'javascript' is disallowed. */
 46 |   context?: "text" | "html" | "attribute" | "url" | "javascript";
 47 |   /** Custom allowed HTML tags if `context` is 'html'. */
 48 |   allowedTags?: string[];
 49 |   /** Custom allowed HTML attributes if `context` is 'html'. */
 50 |   allowedAttributes?: Record<string, string[]>;
 51 | }
 52 | 
 53 | /**
 54 |  * Configuration options for HTML sanitization, mirroring `sanitize-html` library options.
 55 |  */
 56 | export interface HtmlSanitizeConfig {
 57 |   /** An array of allowed HTML tag names. */
 58 |   allowedTags?: string[];
 59 |   /** Specifies allowed attributes, either globally or per tag. */
 60 |   allowedAttributes?: sanitizeHtml.IOptions["allowedAttributes"];
 61 |   /** If true, HTML comments are preserved. */
 62 |   preserveComments?: boolean;
 63 |   /** Custom functions to transform tags during sanitization. */
 64 |   transformTags?: sanitizeHtml.IOptions["transformTags"];
 65 | }
 66 | 
 67 | /**
 68 |  * A singleton class providing various methods for input sanitization.
 69 |  * Aims to protect against common vulnerabilities like XSS and path traversal.
 70 |  */
 71 | export class Sanitization {
 72 |   /** @private */
 73 |   private static instance: Sanitization;
 74 | 
 75 |   /**
 76 |    * Default list of field names considered sensitive for log redaction.
 77 |    * Case-insensitive matching is applied.
 78 |    * @private
 79 |    */
 80 |   private sensitiveFields: string[] = [
 81 |     "password",
 82 |     "token",
 83 |     "secret",
 84 |     "key",
 85 |     "apiKey",
 86 |     "auth",
 87 |     "credential",
 88 |     "jwt",
 89 |     "ssn",
 90 |     "credit",
 91 |     "card",
 92 |     "cvv",
 93 |     "authorization",
 94 |   ];
 95 | 
 96 |   /**
 97 |    * Default configuration for HTML sanitization.
 98 |    * @private
 99 |    */
100 |   private defaultHtmlSanitizeConfig: HtmlSanitizeConfig = {
101 |     allowedTags: [
102 |       "h1",
103 |       "h2",
104 |       "h3",
105 |       "h4",
106 |       "h5",
107 |       "h6",
108 |       "p",
109 |       "a",
110 |       "ul",
111 |       "ol",
112 |       "li",
113 |       "b",
114 |       "i",
115 |       "strong",
116 |       "em",
117 |       "strike",
118 |       "code",
119 |       "hr",
120 |       "br",
121 |       "div",
122 |       "table",
123 |       "thead",
124 |       "tbody",
125 |       "tr",
126 |       "th",
127 |       "td",
128 |       "pre",
129 |     ],
130 |     allowedAttributes: {
131 |       a: ["href", "name", "target"],
132 |       img: ["src", "alt", "title", "width", "height"],
133 |       "*": ["class", "id", "style"],
134 |     },
135 |     preserveComments: false,
136 |   };
137 | 
138 |   /** @private */
139 |   private constructor() {}
140 | 
141 |   /**
142 |    * Retrieves the singleton instance of the `Sanitization` class.
143 |    * @returns The singleton `Sanitization` instance.
144 |    */
145 |   public static getInstance(): Sanitization {
146 |     if (!Sanitization.instance) {
147 |       Sanitization.instance = new Sanitization();
148 |     }
149 |     return Sanitization.instance;
150 |   }
151 | 
152 |   /**
153 |    * Sets or extends the list of sensitive field names for log sanitization.
154 |    * @param fields - An array of field names to add to the sensitive list.
155 |    */
156 |   public setSensitiveFields(fields: string[]): void {
157 |     this.sensitiveFields = [
158 |       ...new Set([
159 |         ...this.sensitiveFields,
160 |         ...fields.map((f) => f.toLowerCase()),
161 |       ]),
162 |     ];
163 |     const logContext = requestContextService.createRequestContext({
164 |       operation: "Sanitization.setSensitiveFields",
165 |       newSensitiveFieldCount: this.sensitiveFields.length,
166 |     });
167 |     logger.debug(
168 |       "Updated sensitive fields list for log sanitization",
169 |       logContext,
170 |     );
171 |   }
172 | 
173 |   /**
174 |    * Gets a copy of the current list of sensitive field names.
175 |    * @returns An array of sensitive field names.
176 |    */
177 |   public getSensitiveFields(): string[] {
178 |     return [...this.sensitiveFields];
179 |   }
180 | 
181 |   /**
182 |    * Sanitizes an HTML string by removing potentially malicious tags and attributes.
183 |    * @param input - The HTML string to sanitize.
184 |    * @param config - Optional custom configuration for `sanitize-html`.
185 |    * @returns The sanitized HTML string. Returns an empty string if input is falsy.
186 |    */
187 |   public sanitizeHtml(input: string, config?: HtmlSanitizeConfig): string {
188 |     if (!input) return "";
189 |     const effectiveConfig = { ...this.defaultHtmlSanitizeConfig, ...config };
190 |     const options: sanitizeHtml.IOptions = {
191 |       allowedTags: effectiveConfig.allowedTags,
192 |       allowedAttributes: effectiveConfig.allowedAttributes,
193 |       transformTags: effectiveConfig.transformTags,
194 |     };
195 |     if (effectiveConfig.preserveComments) {
196 |       options.allowedTags = [...(options.allowedTags || []), "!--"];
197 |     }
198 |     return sanitizeHtml(input, options);
199 |   }
200 | 
201 |   /**
202 |    * Sanitizes a string based on its intended context (e.g., HTML, URL, text).
203 |    * **Important:** `context: 'javascript'` is disallowed due to security risks.
204 |    *
205 |    * @param input - The string to sanitize.
206 |    * @param options - Options specifying the sanitization context.
207 |    * @returns The sanitized string. Returns an empty string if input is falsy.
208 |    * @throws {McpError} If `options.context` is 'javascript', or URL validation fails.
209 |    */
210 |   public sanitizeString(
211 |     input: string,
212 |     options: SanitizeStringOptions = {},
213 |   ): string {
214 |     if (!input) return "";
215 | 
216 |     switch (options.context) {
217 |       case "html":
218 |         return this.sanitizeHtml(input, {
219 |           allowedTags: options.allowedTags,
220 |           allowedAttributes: options.allowedAttributes
221 |             ? this.convertAttributesFormat(options.allowedAttributes)
222 |             : undefined,
223 |         });
224 |       case "attribute":
225 |         return sanitizeHtml(input, { allowedTags: [], allowedAttributes: {} });
226 |       case "url":
227 |         if (
228 |           !validator.isURL(input, {
229 |             protocols: ["http", "https"],
230 |             require_protocol: true,
231 |             require_host: true,
232 |           })
233 |         ) {
234 |           logger.warning(
235 |             "Potentially invalid URL detected during string sanitization (context: url)",
236 |             requestContextService.createRequestContext({
237 |               operation: "Sanitization.sanitizeString.urlWarning",
238 |               invalidUrlAttempt: input,
239 |             }),
240 |           );
241 |           return "";
242 |         }
243 |         return validator.trim(input);
244 |       case "javascript":
245 |         logger.error(
246 |           "Attempted JavaScript sanitization via sanitizeString, which is disallowed.",
247 |           requestContextService.createRequestContext({
248 |             operation: "Sanitization.sanitizeString.jsAttempt",
249 |             inputSnippet: input.substring(0, 50),
250 |           }),
251 |         );
252 |         throw new McpError(
253 |           BaseErrorCode.VALIDATION_ERROR,
254 |           "JavaScript sanitization is not supported through sanitizeString due to security risks.",
255 |         );
256 |       case "text":
257 |       default:
258 |         return sanitizeHtml(input, { allowedTags: [], allowedAttributes: {} });
259 |     }
260 |   }
261 | 
262 |   /**
263 |    * Converts attribute format for `sanitizeHtml`.
264 |    * @param attrs - Attributes in `{ tagName: ['attr1'] }` format.
265 |    * @returns Attributes in `sanitize-html` expected format.
266 |    * @private
267 |    */
268 |   private convertAttributesFormat(
269 |     attrs: Record<string, string[]>,
270 |   ): sanitizeHtml.IOptions["allowedAttributes"] {
271 |     return attrs;
272 |   }
273 | 
274 |   /**
275 |    * Sanitizes a URL string by validating its format and protocol.
276 |    * @param input - The URL string to sanitize.
277 |    * @param allowedProtocols - Array of allowed URL protocols. Default: `['http', 'https']`.
278 |    * @returns The sanitized and trimmed URL string.
279 |    * @throws {McpError} If the URL is invalid or uses a disallowed protocol.
280 |    */
281 |   public sanitizeUrl(
282 |     input: string,
283 |     allowedProtocols: string[] = ["http", "https"],
284 |   ): string {
285 |     try {
286 |       const trimmedInput = input.trim();
287 |       if (
288 |         !validator.isURL(trimmedInput, {
289 |           protocols: allowedProtocols,
290 |           require_protocol: true,
291 |           require_host: true,
292 |         })
293 |       ) {
294 |         throw new Error("Invalid URL format or protocol not in allowed list.");
295 |       }
296 |       if (trimmedInput.toLowerCase().startsWith("javascript:")) {
297 |         throw new Error("JavaScript pseudo-protocol is not allowed in URLs.");
298 |       }
299 |       return trimmedInput;
300 |     } catch (error) {
301 |       throw new McpError(
302 |         BaseErrorCode.VALIDATION_ERROR,
303 |         error instanceof Error
304 |           ? error.message
305 |           : "Invalid or unsafe URL provided.",
306 |         { input },
307 |       );
308 |     }
309 |   }
310 | 
311 |   /**
312 |    * Sanitizes a file path to prevent path traversal and normalize format.
313 |    * @param input - The file path string to sanitize.
314 |    * @param options - Options to control sanitization behavior.
315 |    * @returns An object with the sanitized path and sanitization metadata.
316 |    * @throws {McpError} If the path is invalid or unsafe.
317 |    */
318 |   public sanitizePath(
319 |     input: string,
320 |     options: PathSanitizeOptions = {},
321 |   ): SanitizedPathInfo {
322 |     const originalInput = input;
323 |     const effectiveOptions: PathSanitizeOptions = {
324 |       toPosix: options.toPosix ?? false,
325 |       allowAbsolute: options.allowAbsolute ?? false,
326 |       rootDir: options.rootDir ? path.resolve(options.rootDir) : undefined,
327 |     };
328 | 
329 |     let wasAbsoluteInitially = false;
330 |     let convertedToRelative = false;
331 | 
332 |     try {
333 |       if (!input || typeof input !== "string")
334 |         throw new Error("Invalid path input: must be a non-empty string.");
335 |       if (input.includes("\0"))
336 |         throw new Error("Path contains null byte, which is disallowed.");
337 | 
338 |       let normalized = path.normalize(input);
339 |       wasAbsoluteInitially = path.isAbsolute(normalized);
340 | 
341 |       if (effectiveOptions.toPosix) {
342 |         normalized = normalized.replace(/\\/g, "/");
343 |       }
344 | 
345 |       let finalSanitizedPath: string;
346 | 
347 |       if (effectiveOptions.rootDir) {
348 |         const fullPath = path.resolve(effectiveOptions.rootDir, normalized);
349 |         if (
350 |           !fullPath.startsWith(effectiveOptions.rootDir + path.sep) &&
351 |           fullPath !== effectiveOptions.rootDir
352 |         ) {
353 |           throw new Error(
354 |             "Path traversal detected: attempts to escape the defined root directory.",
355 |           );
356 |         }
357 |         finalSanitizedPath = path.relative(effectiveOptions.rootDir, fullPath);
358 |         finalSanitizedPath =
359 |           finalSanitizedPath === "" ? "." : finalSanitizedPath;
360 |         if (
361 |           path.isAbsolute(finalSanitizedPath) &&
362 |           !effectiveOptions.allowAbsolute
363 |         ) {
364 |           throw new Error(
365 |             "Path resolved to absolute outside root when absolute paths are disallowed.",
366 |           );
367 |         }
368 |       } else {
369 |         if (path.isAbsolute(normalized)) {
370 |           if (!effectiveOptions.allowAbsolute) {
371 |             finalSanitizedPath = normalized.replace(
372 |               /^(?:[A-Za-z]:)?[/\\]+/,
373 |               "",
374 |             );
375 |             convertedToRelative = true;
376 |           } else {
377 |             finalSanitizedPath = normalized;
378 |           }
379 |         } else {
380 |           const resolvedAgainstCwd = path.resolve(normalized);
381 |           const currentWorkingDir = path.resolve(".");
382 |           if (
383 |             !resolvedAgainstCwd.startsWith(currentWorkingDir + path.sep) &&
384 |             resolvedAgainstCwd !== currentWorkingDir
385 |           ) {
386 |             throw new Error(
387 |               "Relative path traversal detected (escapes current working directory context).",
388 |             );
389 |           }
390 |           finalSanitizedPath = normalized;
391 |         }
392 |       }
393 | 
394 |       return {
395 |         sanitizedPath: finalSanitizedPath,
396 |         originalInput,
397 |         wasAbsolute: wasAbsoluteInitially,
398 |         convertedToRelative:
399 |           wasAbsoluteInitially &&
400 |           !path.isAbsolute(finalSanitizedPath) &&
401 |           !effectiveOptions.allowAbsolute,
402 |         optionsUsed: effectiveOptions,
403 |       };
404 |     } catch (error) {
405 |       logger.warning(
406 |         "Path sanitization error",
407 |         requestContextService.createRequestContext({
408 |           operation: "Sanitization.sanitizePath.error",
409 |           originalPathInput: originalInput,
410 |           pathOptionsUsed: effectiveOptions,
411 |           errorMessage: error instanceof Error ? error.message : String(error),
412 |         }),
413 |       );
414 |       throw new McpError(
415 |         BaseErrorCode.VALIDATION_ERROR,
416 |         error instanceof Error
417 |           ? error.message
418 |           : "Invalid or unsafe path provided.",
419 |         { input: originalInput },
420 |       );
421 |     }
422 |   }
423 | 
424 |   /**
425 |    * Sanitizes a JSON string by parsing it to validate its format.
426 |    * Optionally checks if the JSON string exceeds a maximum allowed size.
427 |    * @template T The expected type of the parsed JSON object. Defaults to `unknown`.
428 |    * @param input - The JSON string to sanitize/validate.
429 |    * @param maxSize - Optional maximum allowed size of the JSON string in bytes.
430 |    * @returns The parsed JavaScript object.
431 |    * @throws {McpError} If input is not a string, too large, or invalid JSON.
432 |    */
433 |   public sanitizeJson<T = unknown>(input: string, maxSize?: number): T {
434 |     try {
435 |       if (typeof input !== "string")
436 |         throw new Error("Invalid input: expected a JSON string.");
437 |       if (maxSize !== undefined && Buffer.byteLength(input, "utf8") > maxSize) {
438 |         throw new McpError(
439 |           BaseErrorCode.VALIDATION_ERROR,
440 |           `JSON string exceeds maximum allowed size of ${maxSize} bytes.`,
441 |           { actualSize: Buffer.byteLength(input, "utf8"), maxSize },
442 |         );
443 |       }
444 |       return JSON.parse(input) as T;
445 |     } catch (error) {
446 |       if (error instanceof McpError) throw error;
447 |       throw new McpError(
448 |         BaseErrorCode.VALIDATION_ERROR,
449 |         error instanceof Error ? error.message : "Invalid JSON format.",
450 |         {
451 |           inputPreview:
452 |             input.length > 100 ? `${input.substring(0, 100)}...` : input,
453 |         },
454 |       );
455 |     }
456 |   }
457 | 
458 |   /**
459 |    * Validates and sanitizes a numeric input, converting strings to numbers.
460 |    * Clamps the number to `min`/`max` if provided.
461 |    * @param input - The number or string to validate and sanitize.
462 |    * @param min - Minimum allowed value (inclusive).
463 |    * @param max - Maximum allowed value (inclusive).
464 |    * @returns The sanitized (and potentially clamped) number.
465 |    * @throws {McpError} If input is not a valid number, NaN, or Infinity.
466 |    */
467 |   public sanitizeNumber(
468 |     input: number | string,
469 |     min?: number,
470 |     max?: number,
471 |   ): number {
472 |     let value: number;
473 |     if (typeof input === "string") {
474 |       const trimmedInput = input.trim();
475 |       if (trimmedInput === "" || !validator.isNumeric(trimmedInput)) {
476 |         throw new McpError(
477 |           BaseErrorCode.VALIDATION_ERROR,
478 |           "Invalid number format: input is empty or not numeric.",
479 |           { input },
480 |         );
481 |       }
482 |       value = parseFloat(trimmedInput);
483 |     } else if (typeof input === "number") {
484 |       value = input;
485 |     } else {
486 |       throw new McpError(
487 |         BaseErrorCode.VALIDATION_ERROR,
488 |         "Invalid input type: expected number or string.",
489 |         { input: String(input) },
490 |       );
491 |     }
492 | 
493 |     if (isNaN(value) || !isFinite(value)) {
494 |       throw new McpError(
495 |         BaseErrorCode.VALIDATION_ERROR,
496 |         "Invalid number value (NaN or Infinity).",
497 |         { input },
498 |       );
499 |     }
500 | 
501 |     let clamped = false;
502 |     let originalValueForLog = value;
503 |     if (min !== undefined && value < min) {
504 |       value = min;
505 |       clamped = true;
506 |     }
507 |     if (max !== undefined && value > max) {
508 |       value = max;
509 |       clamped = true;
510 |     }
511 |     if (clamped) {
512 |       logger.debug(
513 |         "Number clamped to range.",
514 |         requestContextService.createRequestContext({
515 |           operation: "Sanitization.sanitizeNumber.clamped",
516 |           originalInput: String(input),
517 |           parsedValue: originalValueForLog,
518 |           minValue: min,
519 |           maxValue: max,
520 |           clampedValue: value,
521 |         }),
522 |       );
523 |     }
524 |     return value;
525 |   }
526 | 
527 |   /**
528 |    * Sanitizes input for logging by redacting sensitive fields.
529 |    * Creates a deep clone and replaces values of fields matching `this.sensitiveFields`
530 |    * (case-insensitive substring match) with "[REDACTED]".
531 |    * @param input - The input data to sanitize for logging.
532 |    * @returns A sanitized (deep cloned) version of the input, safe for logging.
533 |    *   Returns original input if not object/array, or "[Log Sanitization Failed]" on error.
534 |    */
535 |   public sanitizeForLogging(input: unknown): unknown {
536 |     try {
537 |       if (!input || typeof input !== "object") return input;
538 | 
539 |       const clonedInput =
540 |         typeof structuredClone === "function"
541 |           ? structuredClone(input)
542 |           : JSON.parse(JSON.stringify(input));
543 |       this.redactSensitiveFields(clonedInput);
544 |       return clonedInput;
545 |     } catch (error) {
546 |       logger.error(
547 |         "Error during log sanitization, returning placeholder.",
548 |         requestContextService.createRequestContext({
549 |           operation: "Sanitization.sanitizeForLogging.error",
550 |           errorMessage: error instanceof Error ? error.message : String(error),
551 |         }),
552 |       );
553 |       return "[Log Sanitization Failed]";
554 |     }
555 |   }
556 | 
557 |   /**
558 |    * Recursively redacts sensitive fields in an object or array in place.
559 |    * @param obj - The object or array to redact.
560 |    * @private
561 |    */
562 |   private redactSensitiveFields(obj: unknown): void {
563 |     if (!obj || typeof obj !== "object") return;
564 | 
565 |     if (Array.isArray(obj)) {
566 |       obj.forEach((item) => this.redactSensitiveFields(item));
567 |       return;
568 |     }
569 | 
570 |     for (const key in obj) {
571 |       if (Object.prototype.hasOwnProperty.call(obj, key)) {
572 |         const value = (obj as Record<string, unknown>)[key];
573 |         const lowerKey = key.toLowerCase();
574 |         const isSensitive = this.sensitiveFields.some((field) =>
575 |           lowerKey.includes(field),
576 |         );
577 | 
578 |         if (isSensitive) {
579 |           (obj as Record<string, unknown>)[key] = "[REDACTED]";
580 |         } else if (value && typeof value === "object") {
581 |           this.redactSensitiveFields(value);
582 |         }
583 |       }
584 |     }
585 |   }
586 | }
587 | 
588 | /**
589 |  * Singleton instance of the `Sanitization` class.
590 |  * Use this for all input sanitization tasks.
591 |  */
592 | export const sanitization = Sanitization.getInstance();
593 | 
594 | /**
595 |  * Convenience function calling `sanitization.sanitizeForLogging`.
596 |  * @param input - The input data to sanitize.
597 |  * @returns A sanitized version of the input, safe for logging.
598 |  */
599 | export const sanitizeInputForLogging = (input: unknown): unknown =>
600 |   sanitization.sanitizeForLogging(input);
601 | 
```

--------------------------------------------------------------------------------
/src/mcp-server/transports/httpTransport.ts:
--------------------------------------------------------------------------------

```typescript
  1 | /**
  2 |  * @fileoverview Handles the setup and management of the Streamable HTTP MCP transport.
  3 |  * Implements the MCP Specification 2025-03-26 for Streamable HTTP.
  4 |  * This includes creating an Express server, configuring middleware (CORS, Authentication),
  5 |  * defining request routing for the single MCP endpoint (POST/GET/DELETE),
  6 |  * managing server-side sessions, handling Server-Sent Events (SSE) for streaming,
  7 |  * and binding to a network port with retry logic for port conflicts.
  8 |  *
  9 |  * Specification Reference:
 10 |  * https://github.com/modelcontextprotocol/modelcontextprotocol/blob/main/docs/specification/2025-03-26/basic/transports.mdx#streamable-http
 11 |  * @module src/mcp-server/transports/httpTransport
 12 |  */
 13 | 
 14 | import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
 15 | import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
 16 | import { isInitializeRequest } from "@modelcontextprotocol/sdk/types.js";
 17 | import express, { NextFunction, Request, Response } from "express";
 18 | import http from "http";
 19 | import { randomUUID } from "node:crypto";
 20 | import { config } from "../../config/index.js";
 21 | import {
 22 |   logger,
 23 |   requestContextService,
 24 | } from "../../utils/internal/index.js"; // Corrected path
 25 | import { RequestContext } from "../../utils/internal/requestContext.js"; // Explicit path for RequestContext
 26 | import { mcpAuthMiddleware } from "./authentication/authMiddleware.js";
 27 | 
 28 | /**
 29 |  * The port number for the HTTP transport, configured via `MCP_HTTP_PORT` environment variable.
 30 |  * Defaults to 3010 if not specified (default is managed by the config module).
 31 |  * @constant {number} HTTP_PORT
 32 |  * @private
 33 |  */
 34 | const HTTP_PORT = config.mcpHttpPort;
 35 | 
 36 | /**
 37 |  * The host address for the HTTP transport, configured via `MCP_HTTP_HOST` environment variable.
 38 |  * Defaults to '127.0.0.1' if not specified (default is managed by the config module).
 39 |  * MCP Spec Security Note: Recommends binding to localhost for local servers to minimize exposure.
 40 |  * @private
 41 |  */
 42 | const HTTP_HOST = config.mcpHttpHost;
 43 | 
 44 | /**
 45 |  * The single HTTP endpoint path for all MCP communication, as required by the MCP specification.
 46 |  * This endpoint supports POST, GET, DELETE, and OPTIONS methods.
 47 |  * @constant {string} MCP_ENDPOINT_PATH
 48 |  * @private
 49 |  */
 50 | const MCP_ENDPOINT_PATH = "/mcp";
 51 | 
 52 | /**
 53 |  * Maximum number of attempts to find an available port if the initial `HTTP_PORT` is in use.
 54 |  * The server will try ports sequentially: `HTTP_PORT`, `HTTP_PORT + 1`, ..., up to `MAX_PORT_RETRIES`.
 55 |  * @constant {number} MAX_PORT_RETRIES
 56 |  * @private
 57 |  */
 58 | const MAX_PORT_RETRIES = 15;
 59 | 
 60 | /**
 61 |  * Stores active `StreamableHTTPServerTransport` instances from the SDK, keyed by their session ID.
 62 |  * This is essential for routing subsequent HTTP requests (GET, DELETE, non-initialize POST)
 63 |  * to the correct stateful session transport instance.
 64 |  * @type {Record<string, StreamableHTTPServerTransport>}
 65 |  * @private
 66 |  */
 67 | const httpTransports: Record<string, StreamableHTTPServerTransport> = {};
 68 | 
 69 | /**
 70 |  * Checks if an incoming HTTP request's `Origin` header is permissible based on configuration.
 71 |  * MCP Spec Security: Servers MUST validate the `Origin` header for cross-origin requests.
 72 |  * This function checks the request's origin against the `config.mcpAllowedOrigins` list.
 73 |  * If the server is bound to localhost, requests from localhost or with no/null origin are also permitted.
 74 |  * Sets appropriate CORS headers (`Access-Control-Allow-Origin`, etc.) if the origin is allowed.
 75 |  *
 76 |  * @param req - The Express request object.
 77 |  * @param res - The Express response object.
 78 |  * @returns True if the origin is allowed, false otherwise.
 79 |  * @private
 80 |  */
 81 | function isOriginAllowed(req: Request, res: Response): boolean {
 82 |   const origin = req.headers.origin;
 83 |   const host = req.hostname;
 84 |   const isLocalhostBinding = ["127.0.0.1", "::1", "localhost"].includes(host);
 85 |   const allowedOrigins = config.mcpAllowedOrigins || [];
 86 |   const context = requestContextService.createRequestContext({
 87 |     operation: "isOriginAllowed",
 88 |     origin,
 89 |     host,
 90 |     isLocalhostBinding,
 91 |     allowedOrigins,
 92 |   });
 93 |   logger.debug("Checking origin allowance", context);
 94 | 
 95 |   const allowed =
 96 |     (origin && allowedOrigins.includes(origin)) ||
 97 |     (isLocalhostBinding && (!origin || origin === "null"));
 98 | 
 99 |   if (allowed && origin) {
100 |     res.setHeader("Access-Control-Allow-Origin", origin);
101 |     res.setHeader("Access-Control-Allow-Methods", "GET, POST, DELETE, OPTIONS");
102 |     res.setHeader(
103 |       "Access-Control-Allow-Headers",
104 |       "Content-Type, Mcp-Session-Id, Last-Event-ID, Authorization",
105 |     );
106 |     res.setHeader("Access-Control-Allow-Credentials", "true");
107 |   } else if (!allowed && origin) {
108 |     logger.warning(`Origin denied: ${origin}`, context);
109 |   }
110 |   logger.debug(`Origin check result: ${allowed}`, { ...context, allowed });
111 |   return allowed;
112 | }
113 | 
114 | /**
115 |  * Proactively checks if a specific network port is already in use.
116 |  * @param port - The port number to check.
117 |  * @param host - The host address to check the port on.
118 |  * @param parentContext - Logging context from the caller.
119 |  * @returns A promise that resolves to `true` if the port is in use, or `false` otherwise.
120 |  * @private
121 |  */
122 | async function isPortInUse(
123 |   port: number,
124 |   host: string,
125 |   parentContext: RequestContext,
126 | ): Promise<boolean> {
127 |   const checkContext = requestContextService.createRequestContext({
128 |     ...parentContext,
129 |     operation: "isPortInUse",
130 |     port,
131 |     host,
132 |   });
133 |   logger.debug(`Proactively checking port usability...`, checkContext);
134 |   return new Promise((resolve) => {
135 |     const tempServer = http.createServer();
136 |     tempServer
137 |       .once("error", (err: NodeJS.ErrnoException) => {
138 |         if (err.code === "EADDRINUSE") {
139 |           logger.debug(
140 |             `Proactive check: Port confirmed in use (EADDRINUSE).`,
141 |             checkContext,
142 |           );
143 |           resolve(true);
144 |         } else {
145 |           logger.debug(
146 |             `Proactive check: Non-EADDRINUSE error encountered: ${err.message}`,
147 |             { ...checkContext, errorCode: err.code },
148 |           );
149 |           resolve(false);
150 |         }
151 |       })
152 |       .once("listening", () => {
153 |         logger.debug(`Proactive check: Port is available.`, checkContext);
154 |         tempServer.close(() => resolve(false));
155 |       })
156 |       .listen(port, host);
157 |   });
158 | }
159 | 
160 | /**
161 |  * Attempts to start the HTTP server, retrying on incrementing ports if `EADDRINUSE` occurs.
162 |  *
163 |  * @param serverInstance - The Node.js HTTP server instance.
164 |  * @param initialPort - The initial port number to try.
165 |  * @param host - The host address to bind to.
166 |  * @param maxRetries - Maximum number of additional ports to attempt.
167 |  * @param parentContext - Logging context from the caller.
168 |  * @returns A promise that resolves with the port number the server successfully bound to.
169 |  * @throws {Error} If binding fails after all retries or for a non-EADDRINUSE error.
170 |  * @private
171 |  */
172 | function startHttpServerWithRetry(
173 |   serverInstance: http.Server,
174 |   initialPort: number,
175 |   host: string,
176 |   maxRetries: number,
177 |   parentContext: RequestContext,
178 | ): Promise<number> {
179 |   const startContext = requestContextService.createRequestContext({
180 |     ...parentContext,
181 |     operation: "startHttpServerWithRetry",
182 |     initialPort,
183 |     host,
184 |     maxRetries,
185 |   });
186 |   logger.debug(`Attempting to start HTTP server...`, startContext);
187 |   return new Promise(async (resolve, reject) => {
188 |     let lastError: Error | null = null;
189 |     for (let i = 0; i <= maxRetries; i++) {
190 |       const currentPort = initialPort + i;
191 |       const attemptContext = requestContextService.createRequestContext({
192 |         ...startContext,
193 |         port: currentPort,
194 |         attempt: i + 1,
195 |         maxAttempts: maxRetries + 1,
196 |       });
197 |       logger.debug(
198 |         `Attempting port ${currentPort} (${attemptContext.attempt}/${attemptContext.maxAttempts})`,
199 |         attemptContext,
200 |       );
201 | 
202 |       if (await isPortInUse(currentPort, host, attemptContext)) {
203 |         logger.warning(
204 |           `Proactive check detected port ${currentPort} is in use, retrying...`,
205 |           attemptContext,
206 |         );
207 |         lastError = new Error(
208 |           `EADDRINUSE: Port ${currentPort} detected as in use by proactive check.`,
209 |         );
210 |         await new Promise((res) => setTimeout(res, 100));
211 |         continue;
212 |       }
213 | 
214 |       try {
215 |         await new Promise<void>((listenResolve, listenReject) => {
216 |           serverInstance
217 |             .listen(currentPort, host, () => {
218 |               const serverAddress = `http://${host}:${currentPort}${MCP_ENDPOINT_PATH}`;
219 |               logger.info(
220 |                 `HTTP transport successfully listening on host ${host} at ${serverAddress}`,
221 |                 { ...attemptContext, address: serverAddress },
222 |               );
223 |               listenResolve();
224 |             })
225 |             .on("error", (err: NodeJS.ErrnoException) => {
226 |               listenReject(err);
227 |             });
228 |         });
229 |         resolve(currentPort);
230 |         return;
231 |       } catch (err: any) {
232 |         lastError = err;
233 |         logger.debug(
234 |           `Listen error on port ${currentPort}: Code=${err.code}, Message=${err.message}`,
235 |           { ...attemptContext, errorCode: err.code, errorMessage: err.message },
236 |         );
237 |         if (err.code === "EADDRINUSE") {
238 |           logger.warning(
239 |             `Port ${currentPort} already in use (EADDRINUSE), retrying...`,
240 |             attemptContext,
241 |           );
242 |           await new Promise((res) => setTimeout(res, 100));
243 |         } else {
244 |           logger.error(
245 |             `Failed to bind to port ${currentPort} due to non-EADDRINUSE error: ${err.message}`,
246 |             { ...attemptContext, error: err.message },
247 |           );
248 |           reject(err);
249 |           return;
250 |         }
251 |       }
252 |     }
253 |     logger.error(
254 |       `Failed to bind to any port after ${maxRetries + 1} attempts. Last error: ${lastError?.message}`,
255 |       { ...startContext, error: lastError?.message },
256 |     );
257 |     reject(
258 |       lastError ||
259 |         new Error("Failed to bind to any port after multiple retries."),
260 |     );
261 |   });
262 | }
263 | 
264 | /**
265 |  * Sets up and starts the Streamable HTTP transport layer for the MCP server.
266 |  *
267 |  * @param createServerInstanceFn - An asynchronous factory function that returns a new `McpServer` instance.
268 |  * @param parentContext - Logging context from the main server startup process.
269 |  * @returns A promise that resolves when the HTTP server is successfully listening.
270 |  * @throws {Error} If the server fails to start after all port retries.
271 |  */
272 | export async function startHttpTransport(
273 |   createServerInstanceFn: () => Promise<McpServer>,
274 |   parentContext: RequestContext,
275 | ): Promise<void> {
276 |   const app = express();
277 |   const transportContext = requestContextService.createRequestContext({
278 |     ...parentContext,
279 |     transportType: "HTTP",
280 |     component: "HttpTransportSetup",
281 |   });
282 |   logger.debug(
283 |     "Setting up Express app for HTTP transport...",
284 |     transportContext,
285 |   );
286 | 
287 |   app.use(express.json());
288 | 
289 |   app.options(MCP_ENDPOINT_PATH, (req, res) => {
290 |     const optionsContext = requestContextService.createRequestContext({
291 |       ...transportContext,
292 |       operation: "handleOptions",
293 |       origin: req.headers.origin,
294 |       method: req.method,
295 |       path: req.path,
296 |     });
297 |     logger.debug(
298 |       `Received OPTIONS request for ${MCP_ENDPOINT_PATH}`,
299 |       optionsContext,
300 |     );
301 |     if (isOriginAllowed(req, res)) {
302 |       logger.debug(
303 |         "OPTIONS request origin allowed, sending 204.",
304 |         optionsContext,
305 |       );
306 |       res.sendStatus(204);
307 |     } else {
308 |       logger.debug(
309 |         "OPTIONS request origin denied, sending 403.",
310 |         optionsContext,
311 |       );
312 |       res.status(403).send("Forbidden: Invalid Origin");
313 |     }
314 |   });
315 | 
316 |   app.use((req: Request, res: Response, next: NextFunction) => {
317 |     const securityContext = requestContextService.createRequestContext({
318 |       ...transportContext,
319 |       operation: "securityMiddleware",
320 |       path: req.path,
321 |       method: req.method,
322 |       origin: req.headers.origin,
323 |     });
324 |     logger.debug(`Applying security middleware...`, securityContext);
325 |     if (!isOriginAllowed(req, res)) {
326 |       logger.debug("Origin check failed, sending 403.", securityContext);
327 |       res.status(403).send("Forbidden: Invalid Origin");
328 |       return;
329 |     }
330 |     res.setHeader("X-Content-Type-Options", "nosniff");
331 |     res.setHeader("Referrer-Policy", "strict-origin-when-cross-origin");
332 |     res.setHeader(
333 |       "Content-Security-Policy",
334 |       "default-src 'self'; script-src 'self'; object-src 'none'; style-src 'self'; img-src 'self'; media-src 'self'; frame-src 'none'; font-src 'self'; connect-src 'self'",
335 |     );
336 |     logger.debug("Security middleware passed.", securityContext);
337 |     next();
338 |   });
339 | 
340 |   app.use(mcpAuthMiddleware);
341 | 
342 |   app.post(MCP_ENDPOINT_PATH, async (req, res) => {
343 |     const basePostContext = requestContextService.createRequestContext({
344 |       ...transportContext,
345 |       operation: "handlePost",
346 |       method: "POST",
347 |       path: req.path,
348 |       origin: req.headers.origin,
349 |     });
350 |     logger.debug(`Received POST request on ${MCP_ENDPOINT_PATH}`, {
351 |       ...basePostContext,
352 |       headers: req.headers,
353 |       bodyPreview: JSON.stringify(req.body).substring(0, 100),
354 |     });
355 | 
356 |     const sessionId = req.headers["mcp-session-id"] as string | undefined;
357 |     logger.debug(`Extracted session ID: ${sessionId}`, {
358 |       ...basePostContext,
359 |       sessionId,
360 |     });
361 | 
362 |     let transport = sessionId ? httpTransports[sessionId] : undefined;
363 |     logger.debug(`Found existing transport for session ID: ${!!transport}`, {
364 |       ...basePostContext,
365 |       sessionId,
366 |     });
367 | 
368 |     const isInitReq = isInitializeRequest(req.body);
369 |     logger.debug(`Is InitializeRequest: ${isInitReq}`, {
370 |       ...basePostContext,
371 |       sessionId,
372 |     });
373 |     const requestId = (req.body as any)?.id || null;
374 | 
375 |     try {
376 |       if (isInitReq) {
377 |         if (transport) {
378 |           logger.warning(
379 |             "Received InitializeRequest on an existing session ID. Closing old session and creating new.",
380 |             { ...basePostContext, sessionId },
381 |           );
382 |           await transport.close();
383 |           delete httpTransports[sessionId!];
384 |         }
385 |         logger.info("Handling Initialize Request: Creating new session...", {
386 |           ...basePostContext,
387 |           sessionId,
388 |         });
389 | 
390 |         transport = new StreamableHTTPServerTransport({
391 |           sessionIdGenerator: () => {
392 |             const newId = randomUUID();
393 |             logger.debug(`Generated new session ID: ${newId}`, basePostContext);
394 |             return newId;
395 |           },
396 |           onsessioninitialized: (newId) => {
397 |             logger.debug(
398 |               `Session initialized callback triggered for ID: ${newId}`,
399 |               { ...basePostContext, newSessionId: newId },
400 |             );
401 |             httpTransports[newId] = transport!;
402 |             logger.info(`HTTP Session created: ${newId}`, {
403 |               ...basePostContext,
404 |               newSessionId: newId,
405 |             });
406 |           },
407 |         });
408 | 
409 |         transport.onclose = () => {
410 |           const closedSessionId = transport!.sessionId;
411 |           if (closedSessionId) {
412 |             logger.debug(
413 |               `onclose handler triggered for session ID: ${closedSessionId}`,
414 |               { ...basePostContext, closedSessionId },
415 |             );
416 |             delete httpTransports[closedSessionId];
417 |             logger.info(`HTTP Session closed: ${closedSessionId}`, {
418 |               ...basePostContext,
419 |               closedSessionId,
420 |             });
421 |           } else {
422 |             logger.debug(
423 |               "onclose handler triggered for transport without session ID (likely init failure).",
424 |               basePostContext,
425 |             );
426 |           }
427 |         };
428 | 
429 |         logger.debug(
430 |           "Creating McpServer instance for new session...",
431 |           basePostContext,
432 |         );
433 |         const server = await createServerInstanceFn();
434 |         logger.debug(
435 |           "Connecting McpServer to new transport...",
436 |           basePostContext,
437 |         );
438 |         await server.connect(transport);
439 |         logger.debug("McpServer connected to transport.", basePostContext);
440 |       } else if (!transport) {
441 |         logger.warning(
442 |           "Invalid or missing session ID for non-initialize POST request.",
443 |           { ...basePostContext, sessionId },
444 |         );
445 |         res.status(404).json({
446 |           jsonrpc: "2.0",
447 |           error: { code: -32004, message: "Invalid or expired session ID" },
448 |           id: requestId,
449 |         });
450 |         return;
451 |       }
452 | 
453 |       const currentSessionId = transport.sessionId;
454 |       logger.debug(
455 |         `Processing POST request content for session ${currentSessionId}...`,
456 |         { ...basePostContext, sessionId: currentSessionId, isInitReq },
457 |       );
458 |       await transport.handleRequest(req, res, req.body);
459 |       logger.debug(
460 |         `Finished processing POST request content for session ${currentSessionId}.`,
461 |         { ...basePostContext, sessionId: currentSessionId },
462 |       );
463 |     } catch (err) {
464 |       const errorSessionId = transport?.sessionId || sessionId;
465 |       logger.error("Error handling POST request", {
466 |         ...basePostContext,
467 |         sessionId: errorSessionId,
468 |         isInitReq,
469 |         error: err instanceof Error ? err.message : String(err),
470 |         stack: err instanceof Error ? err.stack : undefined,
471 |       });
472 |       if (!res.headersSent) {
473 |         res.status(500).json({
474 |           jsonrpc: "2.0",
475 |           error: {
476 |             code: -32603,
477 |             message: "Internal server error during POST handling",
478 |           },
479 |           id: requestId,
480 |         });
481 |       }
482 |       if (isInitReq && transport && !transport.sessionId) {
483 |         logger.debug("Cleaning up transport after initialization failure.", {
484 |           ...basePostContext,
485 |           sessionId: errorSessionId,
486 |         });
487 |         await transport.close().catch((closeErr) =>
488 |           logger.error("Error closing transport after init failure", {
489 |             ...basePostContext,
490 |             sessionId: errorSessionId,
491 |             closeError: closeErr,
492 |           }),
493 |         );
494 |       }
495 |     }
496 |   });
497 | 
498 |   const handleSessionReq = async (req: Request, res: Response) => {
499 |     const method = req.method;
500 |     const baseSessionReqContext = requestContextService.createRequestContext({
501 |       ...transportContext,
502 |       operation: `handle${method}`,
503 |       method,
504 |       path: req.path,
505 |       origin: req.headers.origin,
506 |     });
507 |     logger.debug(`Received ${method} request on ${MCP_ENDPOINT_PATH}`, {
508 |       ...baseSessionReqContext,
509 |       headers: req.headers,
510 |     });
511 | 
512 |     const sessionId = req.headers["mcp-session-id"] as string | undefined;
513 |     logger.debug(`Extracted session ID: ${sessionId}`, {
514 |       ...baseSessionReqContext,
515 |       sessionId,
516 |     });
517 | 
518 |     const transport = sessionId ? httpTransports[sessionId] : undefined;
519 |     logger.debug(`Found existing transport for session ID: ${!!transport}`, {
520 |       ...baseSessionReqContext,
521 |       sessionId,
522 |     });
523 | 
524 |     if (!transport) {
525 |       logger.warning(`Session not found for ${method} request`, {
526 |         ...baseSessionReqContext,
527 |         sessionId,
528 |       });
529 |       res.status(404).json({
530 |         jsonrpc: "2.0",
531 |         error: { code: -32004, message: "Session not found or expired" },
532 |         id: null, // Or a relevant request identifier if available from context
533 |       });
534 |       return;
535 |     }
536 | 
537 |     try {
538 |       logger.debug(
539 |         `Delegating ${method} request to transport for session ${sessionId}...`,
540 |         { ...baseSessionReqContext, sessionId },
541 |       );
542 |       await transport.handleRequest(req, res);
543 |       logger.info(
544 |         `Successfully handled ${method} request for session ${sessionId}`,
545 |         { ...baseSessionReqContext, sessionId },
546 |       );
547 |     } catch (err) {
548 |       logger.error(
549 |         `Error handling ${method} request for session ${sessionId}`,
550 |         {
551 |           ...baseSessionReqContext,
552 |           sessionId,
553 |           error: err instanceof Error ? err.message : String(err),
554 |           stack: err instanceof Error ? err.stack : undefined,
555 |         },
556 |       );
557 |       if (!res.headersSent) {
558 |         res.status(500).json({
559 |           jsonrpc: "2.0",
560 |           error: { code: -32603, message: "Internal Server Error" },
561 |           id: null, // Or a relevant request identifier
562 |         });
563 |       }
564 |     }
565 |   };
566 |   app.get(MCP_ENDPOINT_PATH, handleSessionReq);
567 |   app.delete(MCP_ENDPOINT_PATH, handleSessionReq);
568 | 
569 |   logger.debug("Creating HTTP server instance...", transportContext);
570 |   const serverInstance = http.createServer(app);
571 |   try {
572 |     logger.debug(
573 |       "Attempting to start HTTP server with retry logic...",
574 |       transportContext,
575 |     );
576 |     const actualPort = await startHttpServerWithRetry(
577 |       serverInstance,
578 |       config.mcpHttpPort,
579 |       config.mcpHttpHost,
580 |       MAX_PORT_RETRIES,
581 |       transportContext,
582 |     );
583 | 
584 |     let serverAddressLog = `http://${config.mcpHttpHost}:${actualPort}${MCP_ENDPOINT_PATH}`;
585 |     let productionNote = "";
586 |     if (config.environment === "production") {
587 |       // The server itself runs HTTP, but it's expected to be behind an HTTPS proxy in production.
588 |       // The log reflects the effective public-facing URL.
589 |       serverAddressLog = `https://${config.mcpHttpHost}:${actualPort}${MCP_ENDPOINT_PATH}`;
590 |       productionNote = ` (via HTTPS, ensure reverse proxy is configured)`;
591 |     }
592 | 
593 |     if (process.stdout.isTTY) {
594 |       console.log(
595 |         `\n🚀 MCP Server running in HTTP mode at: ${serverAddressLog}${productionNote}\n   (MCP Spec: 2025-03-26 Streamable HTTP Transport)\n`,
596 |       );
597 |     }
598 |   } catch (err) {
599 |     logger.fatal("HTTP server failed to start after multiple port retries.", {
600 |       ...transportContext,
601 |       error: err instanceof Error ? err.message : String(err),
602 |     });
603 |     throw err;
604 |   }
605 | }
606 | 
```
Page 2/2FirstPrevNextLast