#
tokens: 26730/50000 2/82 files (page 5/5)
lines: on (toggle) GitHub
raw markdown copy reset
This is page 5 of 5. Use http://codebase.md/modelcontextprotocol/servers?lines=true&page={x} to view the full context.

# Directory Structure

```
├── .gitattributes
├── .github
│   ├── pull_request_template.md
│   └── workflows
│       ├── claude.yml
│       ├── python.yml
│       ├── release.yml
│       └── typescript.yml
├── .gitignore
├── .mcp.json
├── .npmrc
├── CODE_OF_CONDUCT.md
├── CONTRIBUTING.md
├── LICENSE
├── package-lock.json
├── package.json
├── README.md
├── scripts
│   └── release.py
├── SECURITY.md
├── src
│   ├── everything
│   │   ├── CLAUDE.md
│   │   ├── Dockerfile
│   │   ├── everything.ts
│   │   ├── index.ts
│   │   ├── instructions.md
│   │   ├── package.json
│   │   ├── README.md
│   │   ├── sse.ts
│   │   ├── stdio.ts
│   │   ├── streamableHttp.ts
│   │   └── tsconfig.json
│   ├── fetch
│   │   ├── .python-version
│   │   ├── Dockerfile
│   │   ├── LICENSE
│   │   ├── pyproject.toml
│   │   ├── README.md
│   │   ├── src
│   │   │   └── mcp_server_fetch
│   │   │       ├── __init__.py
│   │   │       ├── __main__.py
│   │   │       └── server.py
│   │   └── uv.lock
│   ├── filesystem
│   │   ├── __tests__
│   │   │   ├── directory-tree.test.ts
│   │   │   ├── lib.test.ts
│   │   │   ├── path-utils.test.ts
│   │   │   ├── path-validation.test.ts
│   │   │   └── roots-utils.test.ts
│   │   ├── Dockerfile
│   │   ├── index.ts
│   │   ├── lib.ts
│   │   ├── package.json
│   │   ├── path-utils.ts
│   │   ├── path-validation.ts
│   │   ├── README.md
│   │   ├── roots-utils.ts
│   │   ├── tsconfig.json
│   │   └── vitest.config.ts
│   ├── git
│   │   ├── .gitignore
│   │   ├── .python-version
│   │   ├── Dockerfile
│   │   ├── LICENSE
│   │   ├── pyproject.toml
│   │   ├── README.md
│   │   ├── src
│   │   │   └── mcp_server_git
│   │   │       ├── __init__.py
│   │   │       ├── __main__.py
│   │   │       ├── py.typed
│   │   │       └── server.py
│   │   ├── tests
│   │   │   └── test_server.py
│   │   └── uv.lock
│   ├── memory
│   │   ├── __tests__
│   │   │   ├── file-path.test.ts
│   │   │   └── knowledge-graph.test.ts
│   │   ├── Dockerfile
│   │   ├── index.ts
│   │   ├── package.json
│   │   ├── README.md
│   │   ├── tsconfig.json
│   │   └── vitest.config.ts
│   ├── sequentialthinking
│   │   ├── __tests__
│   │   │   └── lib.test.ts
│   │   ├── Dockerfile
│   │   ├── index.ts
│   │   ├── lib.ts
│   │   ├── package.json
│   │   ├── README.md
│   │   ├── tsconfig.json
│   │   └── vitest.config.ts
│   └── time
│       ├── .python-version
│       ├── Dockerfile
│       ├── pyproject.toml
│       ├── README.md
│       ├── src
│       │   └── mcp_server_time
│       │       ├── __init__.py
│       │       ├── __main__.py
│       │       └── server.py
│       ├── test
│       │   └── time_server_test.py
│       └── uv.lock
└── tsconfig.json
```

# Files

--------------------------------------------------------------------------------
/src/everything/everything.ts:
--------------------------------------------------------------------------------

```typescript
   1 | import { Server } from "@modelcontextprotocol/sdk/server/index.js";
   2 | import type { RequestHandlerExtra } from "@modelcontextprotocol/sdk/shared/protocol.js";
   3 | import {
   4 |   CallToolRequestSchema,
   5 |   ClientCapabilities,
   6 |   CompleteRequestSchema,
   7 |   CreateMessageRequest,
   8 |   CreateMessageResultSchema,
   9 |   ElicitResultSchema,
  10 |   GetPromptRequestSchema,
  11 |   ListPromptsRequestSchema,
  12 |   ListResourcesRequestSchema,
  13 |   ListResourceTemplatesRequestSchema,
  14 |   ListToolsRequestSchema,
  15 |   LoggingLevel,
  16 |   ReadResourceRequestSchema,
  17 |   Resource,
  18 |   RootsListChangedNotificationSchema,
  19 |   ServerNotification,
  20 |   ServerRequest,
  21 |   SubscribeRequestSchema,
  22 |   Tool,
  23 |   UnsubscribeRequestSchema,
  24 |   type Root
  25 | } from "@modelcontextprotocol/sdk/types.js";
  26 | import { z } from "zod";
  27 | import { zodToJsonSchema } from "zod-to-json-schema";
  28 | import { readFileSync } from "fs";
  29 | import { fileURLToPath } from "url";
  30 | import { dirname, join } from "path";
  31 | import JSZip from "jszip";
  32 | 
  33 | const __filename = fileURLToPath(import.meta.url);
  34 | const __dirname = dirname(__filename);
  35 | const instructions = readFileSync(join(__dirname, "instructions.md"), "utf-8");
  36 | 
  37 | type ToolInput = Tool["inputSchema"];
  38 | type ToolOutput = Tool["outputSchema"];
  39 | 
  40 | type SendRequest = RequestHandlerExtra<ServerRequest, ServerNotification>["sendRequest"];
  41 | 
  42 | /* Input schemas for tools implemented in this server */
  43 | const EchoSchema = z.object({
  44 |   message: z.string().describe("Message to echo"),
  45 | });
  46 | 
  47 | const AddSchema = z.object({
  48 |   a: z.number().describe("First number"),
  49 |   b: z.number().describe("Second number"),
  50 | });
  51 | 
  52 | const LongRunningOperationSchema = z.object({
  53 |   duration: z
  54 |     .number()
  55 |     .default(10)
  56 |     .describe("Duration of the operation in seconds"),
  57 |   steps: z
  58 |     .number()
  59 |     .default(5)
  60 |     .describe("Number of steps in the operation"),
  61 | });
  62 | 
  63 | const PrintEnvSchema = z.object({});
  64 | 
  65 | const SampleLLMSchema = z.object({
  66 |   prompt: z.string().describe("The prompt to send to the LLM"),
  67 |   maxTokens: z
  68 |     .number()
  69 |     .default(100)
  70 |     .describe("Maximum number of tokens to generate"),
  71 | });
  72 | 
  73 | const GetTinyImageSchema = z.object({});
  74 | 
  75 | const AnnotatedMessageSchema = z.object({
  76 |   messageType: z
  77 |     .enum(["error", "success", "debug"])
  78 |     .describe("Type of message to demonstrate different annotation patterns"),
  79 |   includeImage: z
  80 |     .boolean()
  81 |     .default(false)
  82 |     .describe("Whether to include an example image"),
  83 | });
  84 | 
  85 | const GetResourceReferenceSchema = z.object({
  86 |   resourceId: z
  87 |     .number()
  88 |     .min(1)
  89 |     .max(100)
  90 |     .describe("ID of the resource to reference (1-100)"),
  91 | });
  92 | 
  93 | const ElicitationSchema = z.object({});
  94 | 
  95 | const GetResourceLinksSchema = z.object({
  96 |   count: z
  97 |     .number()
  98 |     .min(1)
  99 |     .max(10)
 100 |     .default(3)
 101 |     .describe("Number of resource links to return (1-10)"),
 102 | });
 103 | 
 104 | const ListRootsSchema = z.object({});
 105 | 
 106 | const StructuredContentSchema = {
 107 |   input: z.object({
 108 |     location: z
 109 |       .string()
 110 |       .trim()
 111 |       .min(1)
 112 |       .describe("City name or zip code"),
 113 |   }),
 114 | 
 115 |   output: z.object({
 116 |     temperature: z
 117 |       .number()
 118 |       .describe("Temperature in celsius"),
 119 |     conditions: z
 120 |       .string()
 121 |       .describe("Weather conditions description"),
 122 |     humidity: z
 123 |       .number()
 124 |       .describe("Humidity percentage"),
 125 |   })
 126 | };
 127 | 
 128 | const ZipResourcesInputSchema = z.object({
 129 |   files: z.record(z.string().url().describe("URL of the file to include in the zip")).describe("Mapping of file names to URLs to include in the zip"),
 130 | });
 131 | 
 132 | enum ToolName {
 133 |   ECHO = "echo",
 134 |   ADD = "add",
 135 |   LONG_RUNNING_OPERATION = "longRunningOperation",
 136 |   PRINT_ENV = "printEnv",
 137 |   SAMPLE_LLM = "sampleLLM",
 138 |   GET_TINY_IMAGE = "getTinyImage",
 139 |   ANNOTATED_MESSAGE = "annotatedMessage",
 140 |   GET_RESOURCE_REFERENCE = "getResourceReference",
 141 |   ELICITATION = "startElicitation",
 142 |   GET_RESOURCE_LINKS = "getResourceLinks",
 143 |   STRUCTURED_CONTENT = "structuredContent",
 144 |   ZIP_RESOURCES = "zip",
 145 |   LIST_ROOTS = "listRoots"
 146 | }
 147 | 
 148 | enum PromptName {
 149 |   SIMPLE = "simple_prompt",
 150 |   COMPLEX = "complex_prompt",
 151 |   RESOURCE = "resource_prompt",
 152 | }
 153 | 
 154 | // Example completion values
 155 | const EXAMPLE_COMPLETIONS = {
 156 |   style: ["casual", "formal", "technical", "friendly"],
 157 |   temperature: ["0", "0.5", "0.7", "1.0"],
 158 |   resourceId: ["1", "2", "3", "4", "5"],
 159 | };
 160 | 
 161 | export const createServer = () => {
 162 |   const server = new Server(
 163 |     {
 164 |       name: "example-servers/everything",
 165 |       title: "Everything Example Server",
 166 |       version: "1.0.0",
 167 |     },
 168 |     {
 169 |       capabilities: {
 170 |         prompts: {},
 171 |         resources: { subscribe: true },
 172 |         tools: {},
 173 |         logging: {},
 174 |         completions: {}
 175 |       },
 176 |       instructions
 177 |     }
 178 |   );
 179 | 
 180 |   let subscriptions: Set<string> = new Set();
 181 |   let subsUpdateInterval: NodeJS.Timeout | undefined;
 182 |   let stdErrUpdateInterval: NodeJS.Timeout | undefined;
 183 | 
 184 |   let logsUpdateInterval: NodeJS.Timeout | undefined;
 185 |   // Store client capabilities
 186 |   let clientCapabilities: ClientCapabilities | undefined;
 187 | 
 188 |   // Roots state management
 189 |   let currentRoots: Root[] = [];
 190 |   let clientSupportsRoots = false;
 191 |   let sessionId: string | undefined;
 192 | 
 193 |     // Function to start notification intervals when a client connects
 194 |   const startNotificationIntervals = (sid?: string|undefined) => {
 195 |       sessionId = sid;
 196 |       if (!subsUpdateInterval) {
 197 |         subsUpdateInterval = setInterval(() => {
 198 |           for (const uri of subscriptions) {
 199 |             server.notification({
 200 |               method: "notifications/resources/updated",
 201 |               params: { uri },
 202 |             });
 203 |           }
 204 |         }, 10000);
 205 |       }
 206 | 
 207 |       const maybeAppendSessionId = sessionId ? ` - SessionId ${sessionId}`: "";
 208 |       const messages: { level: LoggingLevel; data: string }[] = [
 209 |           { level: "debug", data: `Debug-level message${maybeAppendSessionId}` },
 210 |           { level: "info", data: `Info-level message${maybeAppendSessionId}` },
 211 |           { level: "notice", data: `Notice-level message${maybeAppendSessionId}` },
 212 |           { level: "warning", data: `Warning-level message${maybeAppendSessionId}` },
 213 |           { level: "error", data: `Error-level message${maybeAppendSessionId}` },
 214 |           { level: "critical", data: `Critical-level message${maybeAppendSessionId}` },
 215 |           { level: "alert", data: `Alert level-message${maybeAppendSessionId}` },
 216 |           { level: "emergency", data: `Emergency-level message${maybeAppendSessionId}` },
 217 |       ];
 218 | 
 219 |       if (!logsUpdateInterval) {
 220 |           console.error("Starting logs update interval");
 221 |           logsUpdateInterval = setInterval(async () => {
 222 |           await server.sendLoggingMessage( messages[Math.floor(Math.random() * messages.length)], sessionId);
 223 |       }, 15000);
 224 |     }
 225 |   };
 226 | 
 227 |   // Helper method to request sampling from client
 228 |   const requestSampling = async (
 229 |     context: string,
 230 |     uri: string,
 231 |     maxTokens: number = 100,
 232 |     sendRequest: SendRequest
 233 |   ) => {
 234 |     const request: CreateMessageRequest = {
 235 |       method: "sampling/createMessage",
 236 |       params: {
 237 |         messages: [
 238 |           {
 239 |             role: "user",
 240 |             content: {
 241 |               type: "text",
 242 |               text: `Resource ${uri} context: ${context}`,
 243 |             },
 244 |           },
 245 |         ],
 246 |         systemPrompt: "You are a helpful test server.",
 247 |         maxTokens,
 248 |         temperature: 0.7,
 249 |         includeContext: "thisServer",
 250 |       },
 251 |     };
 252 | 
 253 |     return await sendRequest(request, CreateMessageResultSchema);
 254 | 
 255 |   };
 256 | 
 257 |   const ALL_RESOURCES: Resource[] = Array.from({ length: 100 }, (_, i) => {
 258 |     const uri = `test://static/resource/${i + 1}`;
 259 |     if (i % 2 === 0) {
 260 |       return {
 261 |         uri,
 262 |         name: `Resource ${i + 1}`,
 263 |         mimeType: "text/plain",
 264 |         text: `Resource ${i + 1}: This is a plaintext resource`,
 265 |       };
 266 |     } else {
 267 |       const buffer = Buffer.from(`Resource ${i + 1}: This is a base64 blob`);
 268 |       return {
 269 |         uri,
 270 |         name: `Resource ${i + 1}`,
 271 |         mimeType: "application/octet-stream",
 272 |         blob: buffer.toString("base64"),
 273 |       };
 274 |     }
 275 |   });
 276 | 
 277 |   const PAGE_SIZE = 10;
 278 | 
 279 |   server.setRequestHandler(ListResourcesRequestSchema, async (request) => {
 280 |     const cursor = request.params?.cursor;
 281 |     let startIndex = 0;
 282 | 
 283 |     if (cursor) {
 284 |       const decodedCursor = parseInt(atob(cursor), 10);
 285 |       if (!isNaN(decodedCursor)) {
 286 |         startIndex = decodedCursor;
 287 |       }
 288 |     }
 289 | 
 290 |     const endIndex = Math.min(startIndex + PAGE_SIZE, ALL_RESOURCES.length);
 291 |     const resources = ALL_RESOURCES.slice(startIndex, endIndex);
 292 | 
 293 |     let nextCursor: string | undefined;
 294 |     if (endIndex < ALL_RESOURCES.length) {
 295 |       nextCursor = btoa(endIndex.toString());
 296 |     }
 297 | 
 298 |     return {
 299 |       resources,
 300 |       nextCursor,
 301 |     };
 302 |   });
 303 | 
 304 |   server.setRequestHandler(ListResourceTemplatesRequestSchema, async () => {
 305 |     return {
 306 |       resourceTemplates: [
 307 |         {
 308 |           uriTemplate: "test://static/resource/{id}",
 309 |           name: "Static Resource",
 310 |           description: "A static resource with a numeric ID",
 311 |         },
 312 |       ],
 313 |     };
 314 |   });
 315 | 
 316 |   server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
 317 |     const uri = request.params.uri;
 318 | 
 319 |     if (uri.startsWith("test://static/resource/")) {
 320 |       const index = parseInt(uri.split("/").pop() ?? "", 10) - 1;
 321 |       if (index >= 0 && index < ALL_RESOURCES.length) {
 322 |         const resource = ALL_RESOURCES[index];
 323 |         return {
 324 |           contents: [resource],
 325 |         };
 326 |       }
 327 |     }
 328 | 
 329 |     throw new Error(`Unknown resource: ${uri}`);
 330 |   });
 331 | 
 332 |   server.setRequestHandler(SubscribeRequestSchema, async (request, extra) => {
 333 |     const { uri } = request.params;
 334 |     subscriptions.add(uri);
 335 |     return {};
 336 |   });
 337 | 
 338 |   server.setRequestHandler(UnsubscribeRequestSchema, async (request) => {
 339 |     subscriptions.delete(request.params.uri);
 340 |     return {};
 341 |   });
 342 | 
 343 |   server.setRequestHandler(ListPromptsRequestSchema, async () => {
 344 |     return {
 345 |       prompts: [
 346 |         {
 347 |           name: PromptName.SIMPLE,
 348 |           description: "A prompt without arguments",
 349 |         },
 350 |         {
 351 |           name: PromptName.COMPLEX,
 352 |           description: "A prompt with arguments",
 353 |           arguments: [
 354 |             {
 355 |               name: "temperature",
 356 |               description: "Temperature setting",
 357 |               required: true,
 358 |             },
 359 |             {
 360 |               name: "style",
 361 |               description: "Output style",
 362 |               required: false,
 363 |             },
 364 |           ],
 365 |         },
 366 |         {
 367 |           name: PromptName.RESOURCE,
 368 |           description: "A prompt that includes an embedded resource reference",
 369 |           arguments: [
 370 |             {
 371 |               name: "resourceId",
 372 |               description: "Resource ID to include (1-100)",
 373 |               required: true,
 374 |             },
 375 |           ],
 376 |         },
 377 |       ],
 378 |     };
 379 |   });
 380 | 
 381 |   server.setRequestHandler(GetPromptRequestSchema, async (request) => {
 382 |     const { name, arguments: args } = request.params;
 383 | 
 384 |     if (name === PromptName.SIMPLE) {
 385 |       return {
 386 |         messages: [
 387 |           {
 388 |             role: "user",
 389 |             content: {
 390 |               type: "text",
 391 |               text: "This is a simple prompt without arguments.",
 392 |             },
 393 |           },
 394 |         ],
 395 |       };
 396 |     }
 397 | 
 398 |     if (name === PromptName.COMPLEX) {
 399 |       return {
 400 |         messages: [
 401 |           {
 402 |             role: "user",
 403 |             content: {
 404 |               type: "text",
 405 |               text: `This is a complex prompt with arguments: temperature=${args?.temperature}, style=${args?.style}`,
 406 |             },
 407 |           },
 408 |           {
 409 |             role: "assistant",
 410 |             content: {
 411 |               type: "text",
 412 |               text: "I understand. You've provided a complex prompt with temperature and style arguments. How would you like me to proceed?",
 413 |             },
 414 |           },
 415 |           {
 416 |             role: "user",
 417 |             content: {
 418 |               type: "image",
 419 |               data: MCP_TINY_IMAGE,
 420 |               mimeType: "image/png",
 421 |             },
 422 |           },
 423 |         ],
 424 |       };
 425 |     }
 426 | 
 427 |     if (name === PromptName.RESOURCE) {
 428 |       const resourceId = parseInt(args?.resourceId as string, 10);
 429 |       if (isNaN(resourceId) || resourceId < 1 || resourceId > 100) {
 430 |         throw new Error(
 431 |           `Invalid resourceId: ${args?.resourceId}. Must be a number between 1 and 100.`
 432 |         );
 433 |       }
 434 | 
 435 |       const resourceIndex = resourceId - 1;
 436 |       const resource = ALL_RESOURCES[resourceIndex];
 437 | 
 438 |       return {
 439 |         messages: [
 440 |           {
 441 |             role: "user",
 442 |             content: {
 443 |               type: "text",
 444 |               text: `This prompt includes Resource ${resourceId}. Please analyze the following resource:`,
 445 |             },
 446 |           },
 447 |           {
 448 |             role: "user",
 449 |             content: {
 450 |               type: "resource",
 451 |               resource: resource,
 452 |             },
 453 |           },
 454 |         ],
 455 |       };
 456 |     }
 457 | 
 458 |     throw new Error(`Unknown prompt: ${name}`);
 459 |   });
 460 | 
 461 |   server.setRequestHandler(ListToolsRequestSchema, async () => {
 462 |     const tools: Tool[] = [
 463 |       {
 464 |         name: ToolName.ECHO,
 465 |         description: "Echoes back the input",
 466 |         inputSchema: zodToJsonSchema(EchoSchema) as ToolInput,
 467 |       },
 468 |       {
 469 |         name: ToolName.ADD,
 470 |         description: "Adds two numbers",
 471 |         inputSchema: zodToJsonSchema(AddSchema) as ToolInput,
 472 |       },
 473 |       {
 474 |         name: ToolName.LONG_RUNNING_OPERATION,
 475 |         description:
 476 |           "Demonstrates a long running operation with progress updates",
 477 |         inputSchema: zodToJsonSchema(LongRunningOperationSchema) as ToolInput,
 478 |       },
 479 |       {
 480 |         name: ToolName.PRINT_ENV,
 481 |         description:
 482 |           "Prints all environment variables, helpful for debugging MCP server configuration",
 483 |         inputSchema: zodToJsonSchema(PrintEnvSchema) as ToolInput,
 484 |       },
 485 |       {
 486 |         name: ToolName.SAMPLE_LLM,
 487 |         description: "Samples from an LLM using MCP's sampling feature",
 488 |         inputSchema: zodToJsonSchema(SampleLLMSchema) as ToolInput,
 489 |       },
 490 |       {
 491 |         name: ToolName.GET_TINY_IMAGE,
 492 |         description: "Returns the MCP_TINY_IMAGE",
 493 |         inputSchema: zodToJsonSchema(GetTinyImageSchema) as ToolInput,
 494 |       },
 495 |       {
 496 |         name: ToolName.ANNOTATED_MESSAGE,
 497 |         description:
 498 |           "Demonstrates how annotations can be used to provide metadata about content",
 499 |         inputSchema: zodToJsonSchema(AnnotatedMessageSchema) as ToolInput,
 500 |       },
 501 |       {
 502 |         name: ToolName.GET_RESOURCE_REFERENCE,
 503 |         description:
 504 |           "Returns a resource reference that can be used by MCP clients",
 505 |         inputSchema: zodToJsonSchema(GetResourceReferenceSchema) as ToolInput,
 506 |       },
 507 |       {
 508 |         name: ToolName.GET_RESOURCE_LINKS,
 509 |         description:
 510 |           "Returns multiple resource links that reference different types of resources",
 511 |         inputSchema: zodToJsonSchema(GetResourceLinksSchema) as ToolInput,
 512 |       },
 513 |       {
 514 |         name: ToolName.STRUCTURED_CONTENT,
 515 |         description:
 516 |           "Returns structured content along with an output schema for client data validation",
 517 |         inputSchema: zodToJsonSchema(StructuredContentSchema.input) as ToolInput,
 518 |         outputSchema: zodToJsonSchema(StructuredContentSchema.output) as ToolOutput,
 519 |       },
 520 |       {
 521 |         name: ToolName.ZIP_RESOURCES,
 522 |         description: "Compresses the provided resource files (mapping of name to URI, which can be a data URI) to a zip file, which it returns as a data URI resource link.",
 523 |         inputSchema: zodToJsonSchema(ZipResourcesInputSchema) as ToolInput,
 524 |       }
 525 |     ];
 526 |     if (clientCapabilities!.roots) tools.push ({
 527 |         name: ToolName.LIST_ROOTS,
 528 |         description:
 529 |             "Lists the current MCP roots provided by the client. Demonstrates the roots protocol capability even though this server doesn't access files.",
 530 |         inputSchema: zodToJsonSchema(ListRootsSchema) as ToolInput,
 531 |     });
 532 |     if (clientCapabilities!.elicitation) tools.push ({
 533 |         name: ToolName.ELICITATION,
 534 |         description: "Elicitation test tool that demonstrates how to request user input with various field types (string, boolean, email, uri, date, integer, number, enum)",
 535 |         inputSchema: zodToJsonSchema(ElicitationSchema) as ToolInput,
 536 |     });
 537 | 
 538 |     return { tools };
 539 |   });
 540 | 
 541 |   server.setRequestHandler(CallToolRequestSchema, async (request,extra) => {
 542 |     const { name, arguments: args } = request.params;
 543 | 
 544 |     if (name === ToolName.ECHO) {
 545 |       const validatedArgs = EchoSchema.parse(args);
 546 |       return {
 547 |         content: [{ type: "text", text: `Echo: ${validatedArgs.message}` }],
 548 |       };
 549 |     }
 550 | 
 551 |     if (name === ToolName.ADD) {
 552 |       const validatedArgs = AddSchema.parse(args);
 553 |       const sum = validatedArgs.a + validatedArgs.b;
 554 |       return {
 555 |         content: [
 556 |           {
 557 |             type: "text",
 558 |             text: `The sum of ${validatedArgs.a} and ${validatedArgs.b} is ${sum}.`,
 559 |           },
 560 |         ],
 561 |       };
 562 |     }
 563 | 
 564 |     if (name === ToolName.LONG_RUNNING_OPERATION) {
 565 |       const validatedArgs = LongRunningOperationSchema.parse(args);
 566 |       const { duration, steps } = validatedArgs;
 567 |       const stepDuration = duration / steps;
 568 |       const progressToken = request.params._meta?.progressToken;
 569 | 
 570 |       for (let i = 1; i < steps + 1; i++) {
 571 |         await new Promise((resolve) =>
 572 |           setTimeout(resolve, stepDuration * 1000)
 573 |         );
 574 | 
 575 |         if (progressToken !== undefined) {
 576 |           await server.notification({
 577 |             method: "notifications/progress",
 578 |             params: {
 579 |               progress: i,
 580 |               total: steps,
 581 |               progressToken,
 582 |             },
 583 |           },{relatedRequestId: extra.requestId});
 584 |         }
 585 |       }
 586 | 
 587 |       return {
 588 |         content: [
 589 |           {
 590 |             type: "text",
 591 |             text: `Long running operation completed. Duration: ${duration} seconds, Steps: ${steps}.`,
 592 |           },
 593 |         ],
 594 |       };
 595 |     }
 596 | 
 597 |     if (name === ToolName.PRINT_ENV) {
 598 |       return {
 599 |         content: [
 600 |           {
 601 |             type: "text",
 602 |             text: JSON.stringify(process.env, null, 2),
 603 |           },
 604 |         ],
 605 |       };
 606 |     }
 607 | 
 608 |     if (name === ToolName.SAMPLE_LLM) {
 609 |       const validatedArgs = SampleLLMSchema.parse(args);
 610 |       const { prompt, maxTokens } = validatedArgs;
 611 | 
 612 |       const result = await requestSampling(
 613 |         prompt,
 614 |         ToolName.SAMPLE_LLM,
 615 |         maxTokens,
 616 |         extra.sendRequest
 617 |       );
 618 |       return {
 619 |         content: [
 620 |           { type: "text", text: `LLM sampling result: ${Array.isArray(result.content) ? result.content.map(c => c.type === "text" ? c.text : JSON.stringify(c)).join("") : (result.content.type === "text" ? result.content.text : JSON.stringify(result.content))}` },
 621 |         ],
 622 |       };
 623 |     }
 624 | 
 625 |     if (name === ToolName.GET_TINY_IMAGE) {
 626 |       GetTinyImageSchema.parse(args);
 627 |       return {
 628 |         content: [
 629 |           {
 630 |             type: "text",
 631 |             text: "This is a tiny image:",
 632 |           },
 633 |           {
 634 |             type: "image",
 635 |             data: MCP_TINY_IMAGE,
 636 |             mimeType: "image/png",
 637 |           },
 638 |           {
 639 |             type: "text",
 640 |             text: "The image above is the MCP tiny image.",
 641 |           },
 642 |         ],
 643 |       };
 644 |     }
 645 | 
 646 |     if (name === ToolName.ANNOTATED_MESSAGE) {
 647 |       const { messageType, includeImage } = AnnotatedMessageSchema.parse(args);
 648 | 
 649 |       const content = [];
 650 | 
 651 |       // Main message with different priorities/audiences based on type
 652 |       if (messageType === "error") {
 653 |         content.push({
 654 |           type: "text",
 655 |           text: "Error: Operation failed",
 656 |           annotations: {
 657 |             priority: 1.0, // Errors are highest priority
 658 |             audience: ["user", "assistant"], // Both need to know about errors
 659 |           },
 660 |         });
 661 |       } else if (messageType === "success") {
 662 |         content.push({
 663 |           type: "text",
 664 |           text: "Operation completed successfully",
 665 |           annotations: {
 666 |             priority: 0.7, // Success messages are important but not critical
 667 |             audience: ["user"], // Success mainly for user consumption
 668 |           },
 669 |         });
 670 |       } else if (messageType === "debug") {
 671 |         content.push({
 672 |           type: "text",
 673 |           text: "Debug: Cache hit ratio 0.95, latency 150ms",
 674 |           annotations: {
 675 |             priority: 0.3, // Debug info is low priority
 676 |             audience: ["assistant"], // Technical details for assistant
 677 |           },
 678 |         });
 679 |       }
 680 | 
 681 |       // Optional image with its own annotations
 682 |       if (includeImage) {
 683 |         content.push({
 684 |           type: "image",
 685 |           data: MCP_TINY_IMAGE,
 686 |           mimeType: "image/png",
 687 |           annotations: {
 688 |             priority: 0.5,
 689 |             audience: ["user"], // Images primarily for user visualization
 690 |           },
 691 |         });
 692 |       }
 693 | 
 694 |       return { content };
 695 |     }
 696 | 
 697 |     if (name === ToolName.GET_RESOURCE_REFERENCE) {
 698 |       const validatedArgs = GetResourceReferenceSchema.parse(args);
 699 |       const resourceId = validatedArgs.resourceId;
 700 | 
 701 |       const resourceIndex = resourceId - 1;
 702 |       if (resourceIndex < 0 || resourceIndex >= ALL_RESOURCES.length) {
 703 |         throw new Error(`Resource with ID ${resourceId} does not exist`);
 704 |       }
 705 | 
 706 |       const resource = ALL_RESOURCES[resourceIndex];
 707 | 
 708 |       return {
 709 |         content: [
 710 |           {
 711 |             type: "text",
 712 |             text: `Returning resource reference for Resource ${resourceId}:`,
 713 |           },
 714 |           {
 715 |             type: "resource",
 716 |             resource: resource,
 717 |           },
 718 |           {
 719 |             type: "text",
 720 |             text: `You can access this resource using the URI: ${resource.uri}`,
 721 |           },
 722 |         ],
 723 |       };
 724 |     }
 725 | 
 726 |     if (name === ToolName.ELICITATION) {
 727 |       ElicitationSchema.parse(args);
 728 | 
 729 |       const elicitationResult = await extra.sendRequest({
 730 |         method: 'elicitation/create',
 731 |         params: {
 732 |           message: 'Please provide inputs for the following fields:',
 733 |           requestedSchema: {
 734 |             type: 'object',
 735 |             properties: {
 736 |               name: {
 737 |                 title: 'Full Name',
 738 |                 type: 'string',
 739 |                 description: 'Your full, legal name',
 740 |               },
 741 |               check: {
 742 |                 title: 'Agree to terms',
 743 |                 type: 'boolean',
 744 |                 description: 'A boolean check',
 745 |               },
 746 |               color: {
 747 |                 title: 'Favorite Color',
 748 |                 type: 'string',
 749 |                 description: 'Favorite color (open text)',
 750 |                 default: 'blue',
 751 |               },
 752 |               email: {
 753 |                 title: 'Email Address',
 754 |                 type: 'string',
 755 |                 format: 'email',
 756 |                 description: 'Your email address (will be verified, and never shared with anyone else)',
 757 |               },
 758 |               homepage: {
 759 |                 type: 'string',
 760 |                 format: 'uri',
 761 |                 description: 'Homepage / personal site',
 762 |               },
 763 |               birthdate: {
 764 |                 title: 'Birthdate',
 765 |                 type: 'string',
 766 |                 format: 'date',
 767 |                 description: 'Your date of birth (will never be shared with anyone else)',
 768 |               },
 769 |               integer: {
 770 |                 title: 'Favorite Integer',
 771 |                 type: 'integer',
 772 |                 description: 'Your favorite integer (do not give us your phone number, pin, or other sensitive info)',
 773 |                 minimum: 1,
 774 |                 maximum: 100,
 775 |                 default: 42,
 776 |               },
 777 |               number: {
 778 |                 title: 'Favorite Number',
 779 |                 type: 'number',
 780 |                 description: 'Favorite number (there are no wrong answers)',
 781 |                 minimum: 0,
 782 |                 maximum: 1000,
 783 |                 default: 3.14,
 784 |               },
 785 |               petType: {
 786 |                 title: 'Pet type',
 787 |                 type: 'string',
 788 |                 enum: ['cats', 'dogs', 'birds', 'fish', 'reptiles'],
 789 |                 enumNames: ['Cats', 'Dogs', 'Birds', 'Fish', 'Reptiles'],
 790 |                 default: 'dogs',
 791 |                 description: 'Your favorite pet type',
 792 |               },
 793 |             },
 794 |             required: ['name'],
 795 |           },
 796 |         },
 797 |       }, ElicitResultSchema, { timeout: 10 * 60 * 1000 /* 10 minutes */ });
 798 | 
 799 |       // Handle different response actions
 800 |       const content = [];
 801 | 
 802 |       if (elicitationResult.action === 'accept' && elicitationResult.content) {
 803 |         content.push({
 804 |           type: "text",
 805 |           text: `✅ User provided the requested information!`,
 806 |         });
 807 | 
 808 |         // Only access elicitationResult.content when action is accept
 809 |         const userData = elicitationResult.content;
 810 |         const lines = [];
 811 |         if (userData.name) lines.push(`- Name: ${userData.name}`);
 812 |         if (userData.check !== undefined) lines.push(`- Agreed to terms: ${userData.check}`);
 813 |         if (userData.color) lines.push(`- Favorite Color: ${userData.color}`);
 814 |         if (userData.email) lines.push(`- Email: ${userData.email}`);
 815 |         if (userData.homepage) lines.push(`- Homepage: ${userData.homepage}`);
 816 |         if (userData.birthdate) lines.push(`- Birthdate: ${userData.birthdate}`);
 817 |         if (userData.integer !== undefined) lines.push(`- Favorite Integer: ${userData.integer}`);
 818 |         if (userData.number !== undefined) lines.push(`- Favorite Number: ${userData.number}`);
 819 |         if (userData.petType) lines.push(`- Pet Type: ${userData.petType}`);
 820 | 
 821 |         content.push({
 822 |           type: "text",
 823 |           text: `User inputs:\n${lines.join('\n')}`,
 824 |         });
 825 |       } else if (elicitationResult.action === 'decline') {
 826 |         content.push({
 827 |           type: "text",
 828 |           text: `❌ User declined to provide the requested information.`,
 829 |         });
 830 |       } else if (elicitationResult.action === 'cancel') {
 831 |         content.push({
 832 |           type: "text",
 833 |           text: `⚠️ User cancelled the elicitation dialog.`,
 834 |         });
 835 |       }
 836 | 
 837 |       // Include raw result for debugging
 838 |       content.push({
 839 |         type: "text",
 840 |         text: `\nRaw result: ${JSON.stringify(elicitationResult, null, 2)}`,
 841 |       });
 842 | 
 843 |       return { content };
 844 |     }
 845 | 
 846 |     if (name === ToolName.GET_RESOURCE_LINKS) {
 847 |       const { count } = GetResourceLinksSchema.parse(args);
 848 |       const content = [];
 849 | 
 850 |       // Add intro text
 851 |       content.push({
 852 |         type: "text",
 853 |         text: `Here are ${count} resource links to resources available in this server (see full output in tool response if your client does not support resource_link yet):`,
 854 |       });
 855 | 
 856 |       // Return resource links to actual resources from ALL_RESOURCES
 857 |       const actualCount = Math.min(count, ALL_RESOURCES.length);
 858 |       for (let i = 0; i < actualCount; i++) {
 859 |         const resource = ALL_RESOURCES[i];
 860 |         content.push({
 861 |           type: "resource_link",
 862 |           uri: resource.uri,
 863 |           name: resource.name,
 864 |           description: `Resource ${i + 1}: ${resource.mimeType === "text/plain"
 865 |             ? "plaintext resource"
 866 |             : "binary blob resource"
 867 |             }`,
 868 |           mimeType: resource.mimeType,
 869 |         });
 870 |       }
 871 | 
 872 |       return { content };
 873 |     }
 874 | 
 875 |     if (name === ToolName.STRUCTURED_CONTENT) {
 876 |       // The same response is returned for every input.
 877 |       const validatedArgs = StructuredContentSchema.input.parse(args);
 878 | 
 879 |       const weather = {
 880 |         temperature: 22.5,
 881 |         conditions: "Partly cloudy",
 882 |         humidity: 65
 883 |       }
 884 | 
 885 |       const backwardCompatiblecontent = {
 886 |         type: "text",
 887 |         text: JSON.stringify(weather)
 888 |       }
 889 | 
 890 |       return {
 891 |         content: [backwardCompatiblecontent],
 892 |         structuredContent: weather
 893 |       };
 894 |     }
 895 | 
 896 |     if (name === ToolName.ZIP_RESOURCES) {
 897 |       const { files } = ZipResourcesInputSchema.parse(args);
 898 | 
 899 |       const zip = new JSZip();
 900 | 
 901 |       for (const [fileName, fileUrl] of Object.entries(files)) {
 902 |         try {
 903 |           const response = await fetch(fileUrl);
 904 |           if (!response.ok) {
 905 |             throw new Error(`Failed to fetch ${fileUrl}: ${response.statusText}`);
 906 |           }
 907 |           const arrayBuffer = await response.arrayBuffer();
 908 |           zip.file(fileName, arrayBuffer);
 909 |         } catch (error) {
 910 |           throw new Error(`Error fetching file ${fileUrl}: ${error instanceof Error ? error.message : String(error)}`);
 911 |         }
 912 |       }
 913 | 
 914 |       const uri = `data:application/zip;base64,${await zip.generateAsync({ type: "base64" })}`;
 915 | 
 916 |       return {
 917 |         content: [
 918 |           {
 919 |             type: "resource_link",
 920 |             mimeType: "application/zip",
 921 |             uri,
 922 |           },
 923 |         ],
 924 |       };
 925 |     }
 926 | 
 927 |     if (name === ToolName.LIST_ROOTS) {
 928 |       ListRootsSchema.parse(args);
 929 | 
 930 |       if (!clientSupportsRoots) {
 931 |         return {
 932 |           content: [
 933 |             {
 934 |               type: "text",
 935 |               text: "The MCP client does not support the roots protocol.\n\n" +
 936 |                 "This means the server cannot access information about the client's workspace directories or file system roots."
 937 |             }
 938 |           ]
 939 |         };
 940 |       }
 941 | 
 942 |       if (currentRoots.length === 0) {
 943 |         return {
 944 |           content: [
 945 |             {
 946 |               type: "text",
 947 |               text: "The client supports roots but no roots are currently configured.\n\n" +
 948 |                 "This could mean:\n" +
 949 |                 "1. The client hasn't provided any roots yet\n" +
 950 |                 "2. The client provided an empty roots list\n" +
 951 |                 "3. The roots configuration is still being loaded"
 952 |             }
 953 |           ]
 954 |         };
 955 |       }
 956 | 
 957 |       const rootsList = currentRoots.map((root, index) => {
 958 |         return `${index + 1}. ${root.name || 'Unnamed Root'}\n   URI: ${root.uri}`;
 959 |       }).join('\n\n');
 960 | 
 961 |       return {
 962 |         content: [
 963 |           {
 964 |             type: "text",
 965 |             text: `Current MCP Roots (${currentRoots.length} total):\n\n${rootsList}\n\n` +
 966 |               "Note: This server demonstrates the roots protocol capability but doesn't actually access files. " +
 967 |               "The roots are provided by the MCP client and can be used by servers that need file system access."
 968 |           }
 969 |         ]
 970 |       };
 971 |     }
 972 | 
 973 |     throw new Error(`Unknown tool: ${name}`);
 974 |   });
 975 | 
 976 |   server.setRequestHandler(CompleteRequestSchema, async (request) => {
 977 |     const { ref, argument } = request.params;
 978 | 
 979 |     if (ref.type === "ref/resource") {
 980 |       const resourceId = ref.uri.split("/").pop();
 981 |       if (!resourceId) return { completion: { values: [] } };
 982 | 
 983 |       // Filter resource IDs that start with the input value
 984 |       const values = EXAMPLE_COMPLETIONS.resourceId.filter((id) =>
 985 |         id.startsWith(argument.value)
 986 |       );
 987 |       return { completion: { values, hasMore: false, total: values.length } };
 988 |     }
 989 | 
 990 |     if (ref.type === "ref/prompt") {
 991 |       // Handle completion for prompt arguments
 992 |       const completions =
 993 |         EXAMPLE_COMPLETIONS[argument.name as keyof typeof EXAMPLE_COMPLETIONS];
 994 |       if (!completions) return { completion: { values: [] } };
 995 | 
 996 |       const values = completions.filter((value) =>
 997 |         value.startsWith(argument.value)
 998 |       );
 999 |       return { completion: { values, hasMore: false, total: values.length } };
1000 |     }
1001 | 
1002 |     throw new Error(`Unknown reference type`);
1003 |   });
1004 | 
1005 |   // Roots protocol handlers
1006 |   server.setNotificationHandler(RootsListChangedNotificationSchema, async () => {
1007 |     try {
1008 |       // Request the updated roots list from the client
1009 |       const response = await server.listRoots();
1010 |       if (response && 'roots' in response) {
1011 |         currentRoots = response.roots;
1012 | 
1013 |         // Log the roots update for demonstration
1014 |         await server.sendLoggingMessage({
1015 |             level: "info",
1016 |             logger: "everything-server",
1017 |             data: `Roots updated: ${currentRoots.length} root(s) received from client`,
1018 |         }, sessionId);
1019 |       }
1020 |     } catch (error) {
1021 |       await server.sendLoggingMessage({
1022 |           level: "error",
1023 |           logger: "everything-server",
1024 |           data: `Failed to request roots from client: ${error instanceof Error ? error.message : String(error)}`,
1025 |       }, sessionId);
1026 |     }
1027 |   });
1028 | 
1029 |   // Handle post-initialization setup for roots
1030 |   server.oninitialized = async () => {
1031 |    clientCapabilities = server.getClientCapabilities();
1032 | 
1033 |     if (clientCapabilities?.roots) {
1034 |       clientSupportsRoots = true;
1035 |       try {
1036 |         const response = await server.listRoots();
1037 |         if (response && 'roots' in response) {
1038 |           currentRoots = response.roots;
1039 | 
1040 |           await server.sendLoggingMessage({
1041 |               level: "info",
1042 |               logger: "everything-server",
1043 |               data: `Initial roots received: ${currentRoots.length} root(s) from client`,
1044 |           }, sessionId);
1045 |         } else {
1046 |           await server.sendLoggingMessage({
1047 |               level: "warning",
1048 |               logger: "everything-server",
1049 |               data: "Client returned no roots set",
1050 |           }, sessionId);
1051 |         }
1052 |       } catch (error) {
1053 |         await server.sendLoggingMessage({
1054 |             level: "error",
1055 |             logger: "everything-server",
1056 |             data: `Failed to request initial roots from client: ${error instanceof Error ? error.message : String(error)}`,
1057 |         }, sessionId);
1058 |       }
1059 |     } else {
1060 |       await server.sendLoggingMessage({
1061 |           level: "info",
1062 |           logger: "everything-server",
1063 |           data: "Client does not support MCP roots protocol",
1064 |       }, sessionId);
1065 |     }
1066 |   };
1067 | 
1068 |   const cleanup = async () => {
1069 |     if (subsUpdateInterval) clearInterval(subsUpdateInterval);
1070 |     if (logsUpdateInterval) clearInterval(logsUpdateInterval);
1071 |     if (stdErrUpdateInterval) clearInterval(stdErrUpdateInterval);
1072 |   };
1073 | 
1074 |   return { server, cleanup, startNotificationIntervals };
1075 | };
1076 | 
1077 | const MCP_TINY_IMAGE =
1078 |   "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";
1079 | 
```

--------------------------------------------------------------------------------
/src/filesystem/__tests__/path-validation.test.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { describe, it, expect, beforeEach, afterEach } from 'vitest';
  2 | import * as path from 'path';
  3 | import * as fs from 'fs/promises';
  4 | import * as os from 'os';
  5 | import { isPathWithinAllowedDirectories } from '../path-validation.js';
  6 | 
  7 | /**
  8 |  * Check if the current environment supports symlink creation
  9 |  */
 10 | async function checkSymlinkSupport(): Promise<boolean> {
 11 |   const testDir = await fs.mkdtemp(path.join(os.tmpdir(), 'symlink-test-'));
 12 |   try {
 13 |     const targetFile = path.join(testDir, 'target.txt');
 14 |     const linkFile = path.join(testDir, 'link.txt');
 15 |     
 16 |     await fs.writeFile(targetFile, 'test');
 17 |     await fs.symlink(targetFile, linkFile);
 18 |     
 19 |     // If we get here, symlinks are supported
 20 |     return true;
 21 |   } catch (error) {
 22 |     // EPERM indicates no symlink permissions
 23 |     if ((error as NodeJS.ErrnoException).code === 'EPERM') {
 24 |       return false;
 25 |     }
 26 |     // Other errors might indicate a real problem
 27 |     throw error;
 28 |   } finally {
 29 |     await fs.rm(testDir, { recursive: true, force: true });
 30 |   }
 31 | }
 32 | 
 33 | // Global variable to store symlink support status
 34 | let symlinkSupported: boolean | null = null;
 35 | 
 36 | /**
 37 |  * Get cached symlink support status, checking once per test run
 38 |  */
 39 | async function getSymlinkSupport(): Promise<boolean> {
 40 |   if (symlinkSupported === null) {
 41 |     symlinkSupported = await checkSymlinkSupport();
 42 |     if (!symlinkSupported) {
 43 |       console.log('\n⚠️  Symlink tests will be skipped - symlink creation not supported in this environment');
 44 |       console.log('   On Windows, enable Developer Mode or run as Administrator to enable symlink tests');
 45 |     }
 46 |   }
 47 |   return symlinkSupported;
 48 | }
 49 | 
 50 | describe('Path Validation', () => {
 51 |   it('allows exact directory match', () => {
 52 |     const allowed = ['/home/user/project'];
 53 |     expect(isPathWithinAllowedDirectories('/home/user/project', allowed)).toBe(true);
 54 |   });
 55 | 
 56 |   it('allows subdirectories', () => {
 57 |     const allowed = ['/home/user/project'];
 58 |     expect(isPathWithinAllowedDirectories('/home/user/project/src', allowed)).toBe(true);
 59 |     expect(isPathWithinAllowedDirectories('/home/user/project/src/index.js', allowed)).toBe(true);
 60 |     expect(isPathWithinAllowedDirectories('/home/user/project/deeply/nested/file.txt', allowed)).toBe(true);
 61 |   });
 62 | 
 63 |   it('blocks similar directory names (prefix vulnerability)', () => {
 64 |     const allowed = ['/home/user/project'];
 65 |     expect(isPathWithinAllowedDirectories('/home/user/project2', allowed)).toBe(false);
 66 |     expect(isPathWithinAllowedDirectories('/home/user/project_backup', allowed)).toBe(false);
 67 |     expect(isPathWithinAllowedDirectories('/home/user/project-old', allowed)).toBe(false);
 68 |     expect(isPathWithinAllowedDirectories('/home/user/projectile', allowed)).toBe(false);
 69 |     expect(isPathWithinAllowedDirectories('/home/user/project.bak', allowed)).toBe(false);
 70 |   });
 71 | 
 72 |   it('blocks paths outside allowed directories', () => {
 73 |     const allowed = ['/home/user/project'];
 74 |     expect(isPathWithinAllowedDirectories('/home/user/other', allowed)).toBe(false);
 75 |     expect(isPathWithinAllowedDirectories('/etc/passwd', allowed)).toBe(false);
 76 |     expect(isPathWithinAllowedDirectories('/home/user', allowed)).toBe(false);
 77 |     expect(isPathWithinAllowedDirectories('/', allowed)).toBe(false);
 78 |   });
 79 | 
 80 |   it('handles multiple allowed directories', () => {
 81 |     const allowed = ['/home/user/project1', '/home/user/project2'];
 82 |     expect(isPathWithinAllowedDirectories('/home/user/project1/src', allowed)).toBe(true);
 83 |     expect(isPathWithinAllowedDirectories('/home/user/project2/src', allowed)).toBe(true);
 84 |     expect(isPathWithinAllowedDirectories('/home/user/project3', allowed)).toBe(false);
 85 |     expect(isPathWithinAllowedDirectories('/home/user/project1_backup', allowed)).toBe(false);
 86 |     expect(isPathWithinAllowedDirectories('/home/user/project2-old', allowed)).toBe(false);
 87 |   });
 88 | 
 89 |   it('blocks parent and sibling directories', () => {
 90 |     const allowed = ['/test/allowed'];
 91 | 
 92 |     // Parent directory
 93 |     expect(isPathWithinAllowedDirectories('/test', allowed)).toBe(false);
 94 |     expect(isPathWithinAllowedDirectories('/', allowed)).toBe(false);
 95 | 
 96 |     // Sibling with common prefix
 97 |     expect(isPathWithinAllowedDirectories('/test/allowed_sibling', allowed)).toBe(false);
 98 |     expect(isPathWithinAllowedDirectories('/test/allowed2', allowed)).toBe(false);
 99 |   });
100 | 
101 |   it('handles paths with special characters', () => {
102 |     const allowed = ['/home/user/my-project (v2)'];
103 | 
104 |     expect(isPathWithinAllowedDirectories('/home/user/my-project (v2)', allowed)).toBe(true);
105 |     expect(isPathWithinAllowedDirectories('/home/user/my-project (v2)/src', allowed)).toBe(true);
106 |     expect(isPathWithinAllowedDirectories('/home/user/my-project (v2)_backup', allowed)).toBe(false);
107 |     expect(isPathWithinAllowedDirectories('/home/user/my-project', allowed)).toBe(false);
108 |   });
109 | 
110 |   describe('Input validation', () => {
111 |     it('rejects empty inputs', () => {
112 |       const allowed = ['/home/user/project'];
113 | 
114 |       expect(isPathWithinAllowedDirectories('', allowed)).toBe(false);
115 |       expect(isPathWithinAllowedDirectories('/home/user/project', [])).toBe(false);
116 |     });
117 | 
118 |     it('handles trailing separators correctly', () => {
119 |       const allowed = ['/home/user/project'];
120 | 
121 |       // Path with trailing separator should still match
122 |       expect(isPathWithinAllowedDirectories('/home/user/project/', allowed)).toBe(true);
123 | 
124 |       // Allowed directory with trailing separator
125 |       const allowedWithSep = ['/home/user/project/'];
126 |       expect(isPathWithinAllowedDirectories('/home/user/project', allowedWithSep)).toBe(true);
127 |       expect(isPathWithinAllowedDirectories('/home/user/project/', allowedWithSep)).toBe(true);
128 | 
129 |       // Should still block similar names with or without trailing separators
130 |       expect(isPathWithinAllowedDirectories('/home/user/project2', allowedWithSep)).toBe(false);
131 |       expect(isPathWithinAllowedDirectories('/home/user/project2', allowed)).toBe(false);
132 |       expect(isPathWithinAllowedDirectories('/home/user/project2/', allowed)).toBe(false);
133 |     });
134 | 
135 |     it('skips empty directory entries in allowed list', () => {
136 |       const allowed = ['', '/home/user/project', ''];
137 |       expect(isPathWithinAllowedDirectories('/home/user/project', allowed)).toBe(true);
138 |       expect(isPathWithinAllowedDirectories('/home/user/project/src', allowed)).toBe(true);
139 | 
140 |       // Should still validate properly with empty entries
141 |       expect(isPathWithinAllowedDirectories('/home/user/other', allowed)).toBe(false);
142 |     });
143 | 
144 |     it('handles Windows paths with trailing separators', () => {
145 |       if (path.sep === '\\') {
146 |         const allowed = ['C:\\Users\\project'];
147 | 
148 |         // Path with trailing separator
149 |         expect(isPathWithinAllowedDirectories('C:\\Users\\project\\', allowed)).toBe(true);
150 | 
151 |         // Allowed with trailing separator
152 |         const allowedWithSep = ['C:\\Users\\project\\'];
153 |         expect(isPathWithinAllowedDirectories('C:\\Users\\project', allowedWithSep)).toBe(true);
154 |         expect(isPathWithinAllowedDirectories('C:\\Users\\project\\', allowedWithSep)).toBe(true);
155 | 
156 |         // Should still block similar names
157 |         expect(isPathWithinAllowedDirectories('C:\\Users\\project2\\', allowed)).toBe(false);
158 |       }
159 |     });
160 |   });
161 | 
162 |   describe('Error handling', () => {
163 |     it('normalizes relative paths to absolute', () => {
164 |       const allowed = [process.cwd()];
165 | 
166 |       // Relative paths get normalized to absolute paths based on cwd
167 |       expect(isPathWithinAllowedDirectories('relative/path', allowed)).toBe(true);
168 |       expect(isPathWithinAllowedDirectories('./file', allowed)).toBe(true);
169 | 
170 |       // Parent directory references that escape allowed directory
171 |       const parentAllowed = ['/home/user/project'];
172 |       expect(isPathWithinAllowedDirectories('../parent', parentAllowed)).toBe(false);
173 |     });
174 | 
175 |     it('returns false for relative paths in allowed directories', () => {
176 |       const badAllowed = ['relative/path', '/some/other/absolute/path'];
177 | 
178 |       // Relative paths in allowed dirs are normalized to absolute based on cwd
179 |       // The normalized 'relative/path' won't match our test path
180 |       expect(isPathWithinAllowedDirectories('/some/other/absolute/path/file', badAllowed)).toBe(true);
181 |       expect(isPathWithinAllowedDirectories('/absolute/path/file', badAllowed)).toBe(false);
182 |     });
183 | 
184 |     it('handles null and undefined inputs gracefully', () => {
185 |       const allowed = ['/home/user/project'];
186 | 
187 |       // Should return false, not crash
188 |       expect(isPathWithinAllowedDirectories(null as any, allowed)).toBe(false);
189 |       expect(isPathWithinAllowedDirectories(undefined as any, allowed)).toBe(false);
190 |       expect(isPathWithinAllowedDirectories('/path', null as any)).toBe(false);
191 |       expect(isPathWithinAllowedDirectories('/path', undefined as any)).toBe(false);
192 |     });
193 |   });
194 | 
195 |   describe('Unicode and special characters', () => {
196 |     it('handles unicode characters in paths', () => {
197 |       const allowed = ['/home/user/café'];
198 | 
199 |       expect(isPathWithinAllowedDirectories('/home/user/café', allowed)).toBe(true);
200 |       expect(isPathWithinAllowedDirectories('/home/user/café/file', allowed)).toBe(true);
201 | 
202 |       // Different unicode representation won't match (not normalized)
203 |       const decomposed = '/home/user/cafe\u0301'; // e + combining accent
204 |       expect(isPathWithinAllowedDirectories(decomposed, allowed)).toBe(false);
205 |     });
206 | 
207 |     it('handles paths with spaces correctly', () => {
208 |       const allowed = ['/home/user/my project'];
209 | 
210 |       expect(isPathWithinAllowedDirectories('/home/user/my project', allowed)).toBe(true);
211 |       expect(isPathWithinAllowedDirectories('/home/user/my project/file', allowed)).toBe(true);
212 | 
213 |       // Partial matches should fail
214 |       expect(isPathWithinAllowedDirectories('/home/user/my', allowed)).toBe(false);
215 |       expect(isPathWithinAllowedDirectories('/home/user/my proj', allowed)).toBe(false);
216 |     });
217 |   });
218 | 
219 |   describe('Overlapping allowed directories', () => {
220 |     it('handles nested allowed directories correctly', () => {
221 |       const allowed = ['/home', '/home/user', '/home/user/project'];
222 | 
223 |       // All paths under /home are allowed
224 |       expect(isPathWithinAllowedDirectories('/home/anything', allowed)).toBe(true);
225 |       expect(isPathWithinAllowedDirectories('/home/user/anything', allowed)).toBe(true);
226 |       expect(isPathWithinAllowedDirectories('/home/user/project/anything', allowed)).toBe(true);
227 | 
228 |       // First match wins (most permissive)
229 |       expect(isPathWithinAllowedDirectories('/home/other/deep/path', allowed)).toBe(true);
230 |     });
231 | 
232 |     it('handles root directory as allowed', () => {
233 |       const allowed = ['/'];
234 | 
235 |       // Everything is allowed under root (dangerous configuration)
236 |       expect(isPathWithinAllowedDirectories('/', allowed)).toBe(true);
237 |       expect(isPathWithinAllowedDirectories('/any/path', allowed)).toBe(true);
238 |       expect(isPathWithinAllowedDirectories('/etc/passwd', allowed)).toBe(true);
239 |       expect(isPathWithinAllowedDirectories('/home/user/secret', allowed)).toBe(true);
240 | 
241 |       // But only on the same filesystem root
242 |       if (path.sep === '\\') {
243 |         expect(isPathWithinAllowedDirectories('D:\\other', ['/'])).toBe(false);
244 |       }
245 |     });
246 |   });
247 | 
248 |   describe('Cross-platform behavior', () => {
249 |     it('handles Windows-style paths on Windows', () => {
250 |       if (path.sep === '\\') {
251 |         const allowed = ['C:\\Users\\project'];
252 |         expect(isPathWithinAllowedDirectories('C:\\Users\\project', allowed)).toBe(true);
253 |         expect(isPathWithinAllowedDirectories('C:\\Users\\project\\src', allowed)).toBe(true);
254 |         expect(isPathWithinAllowedDirectories('C:\\Users\\project2', allowed)).toBe(false);
255 |         expect(isPathWithinAllowedDirectories('C:\\Users\\project_backup', allowed)).toBe(false);
256 |       }
257 |     });
258 | 
259 |     it('handles Unix-style paths on Unix', () => {
260 |       if (path.sep === '/') {
261 |         const allowed = ['/home/user/project'];
262 |         expect(isPathWithinAllowedDirectories('/home/user/project', allowed)).toBe(true);
263 |         expect(isPathWithinAllowedDirectories('/home/user/project/src', allowed)).toBe(true);
264 |         expect(isPathWithinAllowedDirectories('/home/user/project2', allowed)).toBe(false);
265 |       }
266 |     });
267 |   });
268 | 
269 |   describe('Validation Tests - Path Traversal', () => {
270 |     it('blocks path traversal attempts', () => {
271 |       const allowed = ['/home/user/project'];
272 | 
273 |       // Basic traversal attempts
274 |       expect(isPathWithinAllowedDirectories('/home/user/project/../../../etc/passwd', allowed)).toBe(false);
275 |       expect(isPathWithinAllowedDirectories('/home/user/project/../../other', allowed)).toBe(false);
276 |       expect(isPathWithinAllowedDirectories('/home/user/project/../project2', allowed)).toBe(false);
277 | 
278 |       // Mixed traversal with valid segments
279 |       expect(isPathWithinAllowedDirectories('/home/user/project/src/../../project2', allowed)).toBe(false);
280 |       expect(isPathWithinAllowedDirectories('/home/user/project/./../../other', allowed)).toBe(false);
281 | 
282 |       // Multiple traversal sequences
283 |       expect(isPathWithinAllowedDirectories('/home/user/project/../project/../../../etc', allowed)).toBe(false);
284 |     });
285 | 
286 |     it('blocks traversal in allowed directories', () => {
287 |       const allowed = ['/home/user/project/../safe'];
288 | 
289 |       // The allowed directory itself should be normalized and safe
290 |       expect(isPathWithinAllowedDirectories('/home/user/safe/file', allowed)).toBe(true);
291 |       expect(isPathWithinAllowedDirectories('/home/user/project/file', allowed)).toBe(false);
292 |     });
293 | 
294 |     it('handles complex traversal patterns', () => {
295 |       const allowed = ['/home/user/project'];
296 | 
297 |       // Double dots in filenames (not traversal) - these normalize to paths within allowed dir
298 |       expect(isPathWithinAllowedDirectories('/home/user/project/..test', allowed)).toBe(true); // Not traversal
299 |       expect(isPathWithinAllowedDirectories('/home/user/project/test..', allowed)).toBe(true); // Not traversal
300 |       expect(isPathWithinAllowedDirectories('/home/user/project/te..st', allowed)).toBe(true); // Not traversal
301 | 
302 |       // Actual traversal
303 |       expect(isPathWithinAllowedDirectories('/home/user/project/../test', allowed)).toBe(false); // Is traversal - goes to /home/user/test
304 | 
305 |       // Edge case: /home/user/project/.. normalizes to /home/user (parent dir)
306 |       expect(isPathWithinAllowedDirectories('/home/user/project/..', allowed)).toBe(false); // Goes to parent
307 |     });
308 |   });
309 | 
310 |   describe('Validation Tests - Null Bytes', () => {
311 |     it('rejects paths with null bytes', () => {
312 |       const allowed = ['/home/user/project'];
313 | 
314 |       expect(isPathWithinAllowedDirectories('/home/user/project\x00/etc/passwd', allowed)).toBe(false);
315 |       expect(isPathWithinAllowedDirectories('/home/user/project/test\x00.txt', allowed)).toBe(false);
316 |       expect(isPathWithinAllowedDirectories('\x00/home/user/project', allowed)).toBe(false);
317 |       expect(isPathWithinAllowedDirectories('/home/user/project/\x00', allowed)).toBe(false);
318 |     });
319 | 
320 |     it('rejects allowed directories with null bytes', () => {
321 |       const allowed = ['/home/user/project\x00'];
322 | 
323 |       expect(isPathWithinAllowedDirectories('/home/user/project', allowed)).toBe(false);
324 |       expect(isPathWithinAllowedDirectories('/home/user/project/file', allowed)).toBe(false);
325 |     });
326 |   });
327 | 
328 |   describe('Validation Tests - Special Characters', () => {
329 |     it('allows percent signs in filenames', () => {
330 |       const allowed = ['/home/user/project'];
331 | 
332 |       // Percent is a valid filename character
333 |       expect(isPathWithinAllowedDirectories('/home/user/project/report_50%.pdf', allowed)).toBe(true);
334 |       expect(isPathWithinAllowedDirectories('/home/user/project/Q1_25%_growth', allowed)).toBe(true);
335 |       expect(isPathWithinAllowedDirectories('/home/user/project/%41', allowed)).toBe(true); // File named %41
336 | 
337 |       // URL encoding is NOT decoded by path.normalize, so these are just odd filenames
338 |       expect(isPathWithinAllowedDirectories('/home/user/project/%2e%2e', allowed)).toBe(true); // File named "%2e%2e"
339 |       expect(isPathWithinAllowedDirectories('/home/user/project/file%20name', allowed)).toBe(true); // File with %20 in name
340 |     });
341 | 
342 |     it('handles percent signs in allowed directories', () => {
343 |       const allowed = ['/home/user/project%20files'];
344 | 
345 |       // This is a directory literally named "project%20files"
346 |       expect(isPathWithinAllowedDirectories('/home/user/project%20files/test', allowed)).toBe(true);
347 |       expect(isPathWithinAllowedDirectories('/home/user/project files/test', allowed)).toBe(false); // Different dir
348 |     });
349 |   });
350 | 
351 |   describe('Path Normalization', () => {
352 |     it('normalizes paths before comparison', () => {
353 |       const allowed = ['/home/user/project'];
354 | 
355 |       // Trailing slashes
356 |       expect(isPathWithinAllowedDirectories('/home/user/project/', allowed)).toBe(true);
357 |       expect(isPathWithinAllowedDirectories('/home/user/project//', allowed)).toBe(true);
358 |       expect(isPathWithinAllowedDirectories('/home/user/project///', allowed)).toBe(true);
359 | 
360 |       // Current directory references
361 |       expect(isPathWithinAllowedDirectories('/home/user/project/./src', allowed)).toBe(true);
362 |       expect(isPathWithinAllowedDirectories('/home/user/./project/src', allowed)).toBe(true);
363 | 
364 |       // Multiple slashes
365 |       expect(isPathWithinAllowedDirectories('/home/user/project//src//file', allowed)).toBe(true);
366 |       expect(isPathWithinAllowedDirectories('/home//user//project//src', allowed)).toBe(true);
367 | 
368 |       // Should still block outside paths
369 |       expect(isPathWithinAllowedDirectories('/home/user//project2', allowed)).toBe(false);
370 |     });
371 | 
372 |     it('handles mixed separators correctly', () => {
373 |       if (path.sep === '\\') {
374 |         const allowed = ['C:\\Users\\project'];
375 | 
376 |         // Mixed separators should be normalized
377 |         expect(isPathWithinAllowedDirectories('C:/Users/project', allowed)).toBe(true);
378 |         expect(isPathWithinAllowedDirectories('C:\\Users/project\\src', allowed)).toBe(true);
379 |         expect(isPathWithinAllowedDirectories('C:/Users\\project/src', allowed)).toBe(true);
380 |       }
381 |     });
382 |   });
383 | 
384 |   describe('Edge Cases', () => {
385 |     it('rejects non-string inputs safely', () => {
386 |       const allowed = ['/home/user/project'];
387 | 
388 |       expect(isPathWithinAllowedDirectories(123 as any, allowed)).toBe(false);
389 |       expect(isPathWithinAllowedDirectories({} as any, allowed)).toBe(false);
390 |       expect(isPathWithinAllowedDirectories([] as any, allowed)).toBe(false);
391 |       expect(isPathWithinAllowedDirectories(null as any, allowed)).toBe(false);
392 |       expect(isPathWithinAllowedDirectories(undefined as any, allowed)).toBe(false);
393 | 
394 |       // Non-string in allowed directories
395 |       expect(isPathWithinAllowedDirectories('/home/user/project', [123 as any])).toBe(false);
396 |       expect(isPathWithinAllowedDirectories('/home/user/project', [{} as any])).toBe(false);
397 |     });
398 | 
399 |     it('handles very long paths', () => {
400 |       const allowed = ['/home/user/project'];
401 | 
402 |       // Create a very long path that's still valid
403 |       const longSubPath = 'a/'.repeat(1000) + 'file.txt';
404 |       expect(isPathWithinAllowedDirectories(`/home/user/project/${longSubPath}`, allowed)).toBe(true);
405 | 
406 |       // Very long path that escapes
407 |       const escapePath = 'a/'.repeat(1000) + '../'.repeat(1001) + 'etc/passwd';
408 |       expect(isPathWithinAllowedDirectories(`/home/user/project/${escapePath}`, allowed)).toBe(false);
409 |     });
410 |   });
411 | 
412 |   describe('Additional Coverage', () => {
413 |     it('handles allowed directories with traversal that normalizes safely', () => {
414 |       // These allowed dirs contain traversal but normalize to valid paths
415 |       const allowed = ['/home/user/../user/project'];
416 | 
417 |       // Should normalize to /home/user/project and work correctly
418 |       expect(isPathWithinAllowedDirectories('/home/user/project/file', allowed)).toBe(true);
419 |       expect(isPathWithinAllowedDirectories('/home/user/other', allowed)).toBe(false);
420 |     });
421 | 
422 |     it('handles symbolic dots in filenames', () => {
423 |       const allowed = ['/home/user/project'];
424 | 
425 |       // Single and double dots as actual filenames (not traversal)
426 |       expect(isPathWithinAllowedDirectories('/home/user/project/.', allowed)).toBe(true);
427 |       expect(isPathWithinAllowedDirectories('/home/user/project/..', allowed)).toBe(false); // This normalizes to parent
428 |       expect(isPathWithinAllowedDirectories('/home/user/project/...', allowed)).toBe(true); // Three dots is a valid filename
429 |       expect(isPathWithinAllowedDirectories('/home/user/project/....', allowed)).toBe(true); // Four dots is a valid filename
430 |     });
431 | 
432 |     it('handles UNC paths on Windows', () => {
433 |       if (path.sep === '\\') {
434 |         const allowed = ['\\\\server\\share\\project'];
435 | 
436 |         expect(isPathWithinAllowedDirectories('\\\\server\\share\\project', allowed)).toBe(true);
437 |         expect(isPathWithinAllowedDirectories('\\\\server\\share\\project\\file', allowed)).toBe(true);
438 |         expect(isPathWithinAllowedDirectories('\\\\server\\share\\other', allowed)).toBe(false);
439 |         expect(isPathWithinAllowedDirectories('\\\\other\\share\\project', allowed)).toBe(false);
440 |       }
441 |     });
442 |   });
443 | 
444 |   describe('Symlink Tests', () => {
445 |     let testDir: string;
446 |     let allowedDir: string;
447 |     let forbiddenDir: string;
448 | 
449 |     beforeEach(async () => {
450 |       testDir = await fs.mkdtemp(path.join(os.tmpdir(), 'fs-error-test-'));
451 |       allowedDir = path.join(testDir, 'allowed');
452 |       forbiddenDir = path.join(testDir, 'forbidden');
453 | 
454 |       await fs.mkdir(allowedDir, { recursive: true });
455 |       await fs.mkdir(forbiddenDir, { recursive: true });
456 |     });
457 | 
458 |     afterEach(async () => {
459 |       await fs.rm(testDir, { recursive: true, force: true });
460 |     });
461 | 
462 |     it('validates symlink handling', async () => {
463 |       // Test with symlinks
464 |       try {
465 |         const linkPath = path.join(allowedDir, 'bad-link');
466 |         const targetPath = path.join(forbiddenDir, 'target.txt');
467 | 
468 |         await fs.writeFile(targetPath, 'content');
469 |         await fs.symlink(targetPath, linkPath);
470 | 
471 |         // In real implementation, this would throw with the resolved path
472 |         const realPath = await fs.realpath(linkPath);
473 |         const allowed = [allowedDir];
474 | 
475 |         // Symlink target should be outside allowed directory
476 |         expect(isPathWithinAllowedDirectories(realPath, allowed)).toBe(false);
477 |       } catch (error) {
478 |         // Skip if no symlink permissions
479 |       }
480 |     });
481 | 
482 |     it('handles non-existent paths correctly', async () => {
483 |       const newFilePath = path.join(allowedDir, 'subdir', 'newfile.txt');
484 | 
485 |       // Parent directory doesn't exist
486 |       try {
487 |         await fs.access(newFilePath);
488 |       } catch (error) {
489 |         expect((error as NodeJS.ErrnoException).code).toBe('ENOENT');
490 |       }
491 | 
492 |       // After creating parent, validation should work
493 |       await fs.mkdir(path.dirname(newFilePath), { recursive: true });
494 |       const allowed = [allowedDir];
495 |       expect(isPathWithinAllowedDirectories(newFilePath, allowed)).toBe(true);
496 |     });
497 | 
498 |     // Test path resolution consistency for symlinked files
499 |     it('validates symlinked files consistently between path and resolved forms', async () => {
500 |       try {
501 |         // Setup: Create target file in forbidden area
502 |         const targetFile = path.join(forbiddenDir, 'target.txt');
503 |         await fs.writeFile(targetFile, 'TARGET_CONTENT');
504 | 
505 |         // Create symlink inside allowed directory pointing to forbidden file
506 |         const symlinkPath = path.join(allowedDir, 'link-to-target.txt');
507 |         await fs.symlink(targetFile, symlinkPath);
508 | 
509 |         // The symlink path itself passes validation (looks like it's in allowed dir)
510 |         expect(isPathWithinAllowedDirectories(symlinkPath, [allowedDir])).toBe(true);
511 | 
512 |         // But the resolved path should fail validation
513 |         const resolvedPath = await fs.realpath(symlinkPath);
514 |         expect(isPathWithinAllowedDirectories(resolvedPath, [allowedDir])).toBe(false);
515 | 
516 |         // Verify the resolved path goes to the forbidden location (normalize both paths for macOS temp dirs)
517 |         expect(await fs.realpath(resolvedPath)).toBe(await fs.realpath(targetFile));
518 |       } catch (error) {
519 |         // Skip if no symlink permissions on the system
520 |         if ((error as NodeJS.ErrnoException).code !== 'EPERM') {
521 |           throw error;
522 |         }
523 |       }
524 |     });
525 | 
526 |     // Test allowed directory resolution behavior
527 |     it('validates paths correctly when allowed directory is resolved from symlink', async () => {
528 |       try {
529 |         // Setup: Create the actual target directory with content
530 |         const actualTargetDir = path.join(testDir, 'actual-target');
531 |         await fs.mkdir(actualTargetDir, { recursive: true });
532 |         const targetFile = path.join(actualTargetDir, 'file.txt');
533 |         await fs.writeFile(targetFile, 'FILE_CONTENT');
534 | 
535 |         // Setup: Create symlink directory that points to target
536 |         const symlinkDir = path.join(testDir, 'symlink-dir');
537 |         await fs.symlink(actualTargetDir, symlinkDir);
538 | 
539 |         // Simulate resolved allowed directory (what the server startup should do)
540 |         const resolvedAllowedDir = await fs.realpath(symlinkDir);
541 |         const resolvedTargetDir = await fs.realpath(actualTargetDir);
542 |         expect(resolvedAllowedDir).toBe(resolvedTargetDir);
543 | 
544 |         // Test 1: File access through original symlink path should pass validation with resolved allowed dir
545 |         const fileViaSymlink = path.join(symlinkDir, 'file.txt');
546 |         const resolvedFile = await fs.realpath(fileViaSymlink);
547 |         expect(isPathWithinAllowedDirectories(resolvedFile, [resolvedAllowedDir])).toBe(true);
548 | 
549 |         // Test 2: File access through resolved path should also pass validation
550 |         const fileViaResolved = path.join(resolvedTargetDir, 'file.txt');
551 |         expect(isPathWithinAllowedDirectories(fileViaResolved, [resolvedAllowedDir])).toBe(true);
552 | 
553 |         // Test 3: Demonstrate inconsistent behavior with unresolved allowed directories
554 |         // If allowed dirs were not resolved (storing symlink paths instead):
555 |         const unresolvedAllowedDirs = [symlinkDir];
556 |         // This validation would incorrectly fail for the same content:
557 |         expect(isPathWithinAllowedDirectories(resolvedFile, unresolvedAllowedDirs)).toBe(false);
558 | 
559 |       } catch (error) {
560 |         // Skip if no symlink permissions on the system
561 |         if ((error as NodeJS.ErrnoException).code !== 'EPERM') {
562 |           throw error;
563 |         }
564 |       }
565 |     });
566 | 
567 |     it('resolves nested symlink chains completely', async () => {
568 |       try {
569 |         // Setup: Create target file in forbidden area
570 |         const actualTarget = path.join(forbiddenDir, 'target-file.txt');
571 |         await fs.writeFile(actualTarget, 'FINAL_CONTENT');
572 | 
573 |         // Create chain of symlinks: allowedFile -> link2 -> link1 -> actualTarget
574 |         const link1 = path.join(testDir, 'intermediate-link1');
575 |         const link2 = path.join(testDir, 'intermediate-link2');
576 |         const allowedFile = path.join(allowedDir, 'seemingly-safe-file');
577 | 
578 |         await fs.symlink(actualTarget, link1);
579 |         await fs.symlink(link1, link2);
580 |         await fs.symlink(link2, allowedFile);
581 | 
582 |         // The allowed file path passes basic validation
583 |         expect(isPathWithinAllowedDirectories(allowedFile, [allowedDir])).toBe(true);
584 | 
585 |         // But complete resolution reveals the forbidden target
586 |         const fullyResolvedPath = await fs.realpath(allowedFile);
587 |         expect(isPathWithinAllowedDirectories(fullyResolvedPath, [allowedDir])).toBe(false);
588 |         expect(await fs.realpath(fullyResolvedPath)).toBe(await fs.realpath(actualTarget));
589 | 
590 |       } catch (error) {
591 |         // Skip if no symlink permissions on the system
592 |         if ((error as NodeJS.ErrnoException).code !== 'EPERM') {
593 |           throw error;
594 |         }
595 |       }
596 |     });
597 |   });
598 | 
599 |   describe('Path Validation Race Condition Tests', () => {
600 |     let testDir: string;
601 |     let allowedDir: string;
602 |     let forbiddenDir: string;
603 |     let targetFile: string;
604 |     let testPath: string;
605 | 
606 |     beforeEach(async () => {
607 |       testDir = await fs.mkdtemp(path.join(os.tmpdir(), 'race-test-'));
608 |       allowedDir = path.join(testDir, 'allowed');
609 |       forbiddenDir = path.join(testDir, 'outside');
610 |       targetFile = path.join(forbiddenDir, 'target.txt');
611 |       testPath = path.join(allowedDir, 'test.txt');
612 | 
613 |       await fs.mkdir(allowedDir, { recursive: true });
614 |       await fs.mkdir(forbiddenDir, { recursive: true });
615 |       await fs.writeFile(targetFile, 'ORIGINAL CONTENT', 'utf-8');
616 |     });
617 | 
618 |     afterEach(async () => {
619 |       await fs.rm(testDir, { recursive: true, force: true });
620 |     });
621 | 
622 |     it('validates non-existent file paths based on parent directory', async () => {
623 |       const allowed = [allowedDir];
624 | 
625 |       expect(isPathWithinAllowedDirectories(testPath, allowed)).toBe(true);
626 |       await expect(fs.access(testPath)).rejects.toThrow();
627 | 
628 |       const parentDir = path.dirname(testPath);
629 |       expect(isPathWithinAllowedDirectories(parentDir, allowed)).toBe(true);
630 |     });
631 | 
632 |     it('demonstrates symlink race condition allows writing outside allowed directories', async () => {
633 |       const symlinkSupported = await getSymlinkSupport();
634 |       if (!symlinkSupported) {
635 |         console.log('   ⏭️  Skipping symlink race condition test - symlinks not supported');
636 |         return;
637 |       }
638 | 
639 |       const allowed = [allowedDir];
640 | 
641 |       await expect(fs.access(testPath)).rejects.toThrow();
642 |       expect(isPathWithinAllowedDirectories(testPath, allowed)).toBe(true);
643 | 
644 |       await fs.symlink(targetFile, testPath);
645 |       await fs.writeFile(testPath, 'MODIFIED CONTENT', 'utf-8');
646 | 
647 |       const targetContent = await fs.readFile(targetFile, 'utf-8');
648 |       expect(targetContent).toBe('MODIFIED CONTENT');
649 | 
650 |       const resolvedPath = await fs.realpath(testPath);
651 |       expect(isPathWithinAllowedDirectories(resolvedPath, allowed)).toBe(false);
652 |     });
653 | 
654 |     it('shows timing differences between validation approaches', async () => {
655 |       const symlinkSupported = await getSymlinkSupport();
656 |       if (!symlinkSupported) {
657 |         console.log('   ⏭️  Skipping timing validation test - symlinks not supported');
658 |         return;
659 |       }
660 | 
661 |       const allowed = [allowedDir];
662 | 
663 |       const validation1 = isPathWithinAllowedDirectories(testPath, allowed);
664 |       expect(validation1).toBe(true);
665 | 
666 |       await fs.symlink(targetFile, testPath);
667 | 
668 |       const resolvedPath = await fs.realpath(testPath);
669 |       const validation2 = isPathWithinAllowedDirectories(resolvedPath, allowed);
670 |       expect(validation2).toBe(false);
671 | 
672 |       expect(validation1).not.toBe(validation2);
673 |     });
674 | 
675 |     it('validates directory creation timing', async () => {
676 |       const symlinkSupported = await getSymlinkSupport();
677 |       if (!symlinkSupported) {
678 |         console.log('   ⏭️  Skipping directory creation timing test - symlinks not supported');
679 |         return;
680 |       }
681 | 
682 |       const allowed = [allowedDir];
683 |       const testDir = path.join(allowedDir, 'newdir');
684 | 
685 |       expect(isPathWithinAllowedDirectories(testDir, allowed)).toBe(true);
686 | 
687 |       await fs.symlink(forbiddenDir, testDir);
688 | 
689 |       expect(isPathWithinAllowedDirectories(testDir, allowed)).toBe(true);
690 | 
691 |       const resolved = await fs.realpath(testDir);
692 |       expect(isPathWithinAllowedDirectories(resolved, allowed)).toBe(false);
693 |     });
694 | 
695 |     it('demonstrates exclusive file creation behavior', async () => {
696 |       const symlinkSupported = await getSymlinkSupport();
697 |       if (!symlinkSupported) {
698 |         console.log('   ⏭️  Skipping exclusive file creation test - symlinks not supported');
699 |         return;
700 |       }
701 | 
702 |       const allowed = [allowedDir];
703 | 
704 |       await fs.symlink(targetFile, testPath);
705 | 
706 |       await expect(fs.open(testPath, 'wx')).rejects.toThrow(/EEXIST/);
707 | 
708 |       await fs.writeFile(testPath, 'NEW CONTENT', 'utf-8');
709 |       const targetContent = await fs.readFile(targetFile, 'utf-8');
710 |       expect(targetContent).toBe('NEW CONTENT');
711 |     });
712 | 
713 |     it('should use resolved parent paths for non-existent files', async () => {
714 |       const symlinkSupported = await getSymlinkSupport();
715 |       if (!symlinkSupported) {
716 |         console.log('   ⏭️  Skipping resolved parent paths test - symlinks not supported');
717 |         return;
718 |       }
719 | 
720 |       const allowed = [allowedDir];
721 | 
722 |       const symlinkDir = path.join(allowedDir, 'link');
723 |       await fs.symlink(forbiddenDir, symlinkDir);
724 | 
725 |       const fileThroughSymlink = path.join(symlinkDir, 'newfile.txt');
726 | 
727 |       expect(fileThroughSymlink.startsWith(allowedDir)).toBe(true);
728 | 
729 |       const parentDir = path.dirname(fileThroughSymlink);
730 |       const resolvedParent = await fs.realpath(parentDir);
731 |       expect(isPathWithinAllowedDirectories(resolvedParent, allowed)).toBe(false);
732 | 
733 |       const expectedSafePath = path.join(resolvedParent, path.basename(fileThroughSymlink));
734 |       expect(isPathWithinAllowedDirectories(expectedSafePath, allowed)).toBe(false);
735 |     });
736 | 
737 |     it('demonstrates parent directory symlink traversal', async () => {
738 |       const symlinkSupported = await getSymlinkSupport();
739 |       if (!symlinkSupported) {
740 |         console.log('   ⏭️  Skipping parent directory symlink traversal test - symlinks not supported');
741 |         return;
742 |       }
743 | 
744 |       const allowed = [allowedDir];
745 |       const deepPath = path.join(allowedDir, 'sub1', 'sub2', 'file.txt');
746 | 
747 |       expect(isPathWithinAllowedDirectories(deepPath, allowed)).toBe(true);
748 | 
749 |       const sub1Path = path.join(allowedDir, 'sub1');
750 |       await fs.symlink(forbiddenDir, sub1Path);
751 | 
752 |       await fs.mkdir(path.join(sub1Path, 'sub2'), { recursive: true });
753 |       await fs.writeFile(deepPath, 'CONTENT', 'utf-8');
754 | 
755 |       const realPath = await fs.realpath(deepPath);
756 |       const realAllowedDir = await fs.realpath(allowedDir);
757 |       const realForbiddenDir = await fs.realpath(forbiddenDir);
758 | 
759 |       expect(realPath.startsWith(realAllowedDir)).toBe(false);
760 |       expect(realPath.startsWith(realForbiddenDir)).toBe(true);
761 |     });
762 | 
763 |     it('should prevent race condition between validatePath and file operation', async () => {
764 |       const symlinkSupported = await getSymlinkSupport();
765 |       if (!symlinkSupported) {
766 |         console.log('   ⏭️  Skipping race condition prevention test - symlinks not supported');
767 |         return;
768 |       }
769 | 
770 |       const allowed = [allowedDir];
771 |       const racePath = path.join(allowedDir, 'race-file.txt');
772 |       const targetFile = path.join(forbiddenDir, 'target.txt');
773 | 
774 |       await fs.writeFile(targetFile, 'ORIGINAL CONTENT', 'utf-8');
775 | 
776 |       // Path validation would pass (file doesn't exist, parent is in allowed dir)
777 |       expect(await fs.access(racePath).then(() => false).catch(() => true)).toBe(true);
778 |       expect(isPathWithinAllowedDirectories(racePath, allowed)).toBe(true);
779 | 
780 |       // Race condition: symlink created after validation but before write
781 |       await fs.symlink(targetFile, racePath);
782 | 
783 |       // With exclusive write flag, write should fail on symlink
784 |       await expect(
785 |         fs.writeFile(racePath, 'NEW CONTENT', { encoding: 'utf-8', flag: 'wx' })
786 |       ).rejects.toThrow(/EEXIST/);
787 | 
788 |       // Verify content unchanged
789 |       const targetContent = await fs.readFile(targetFile, 'utf-8');
790 |       expect(targetContent).toBe('ORIGINAL CONTENT');
791 | 
792 |       // The symlink exists but write was blocked
793 |       const actualWritePath = await fs.realpath(racePath);
794 |       expect(actualWritePath).toBe(await fs.realpath(targetFile));
795 |       expect(isPathWithinAllowedDirectories(actualWritePath, allowed)).toBe(false);
796 |     });
797 | 
798 |     it('should allow overwrites to legitimate files within allowed directories', async () => {
799 |       const allowed = [allowedDir];
800 |       const legitFile = path.join(allowedDir, 'legit-file.txt');
801 | 
802 |       // Create a legitimate file
803 |       await fs.writeFile(legitFile, 'ORIGINAL', 'utf-8');
804 | 
805 |       // Opening with w should work for legitimate files
806 |       const fd = await fs.open(legitFile, 'w');
807 |       try {
808 |         await fd.write('UPDATED', 0, 'utf-8');
809 |       } finally {
810 |         await fd.close();
811 |       }
812 | 
813 |       const content = await fs.readFile(legitFile, 'utf-8');
814 |       expect(content).toBe('UPDATED');
815 |     });
816 | 
817 |     it('should handle symlinks that point within allowed directories', async () => {
818 |       const symlinkSupported = await getSymlinkSupport();
819 |       if (!symlinkSupported) {
820 |         console.log('   ⏭️  Skipping symlinks within allowed directories test - symlinks not supported');
821 |         return;
822 |       }
823 | 
824 |       const allowed = [allowedDir];
825 |       const targetFile = path.join(allowedDir, 'target.txt');
826 |       const symlinkPath = path.join(allowedDir, 'symlink.txt');
827 | 
828 |       // Create target file within allowed directory
829 |       await fs.writeFile(targetFile, 'TARGET CONTENT', 'utf-8');
830 | 
831 |       // Create symlink pointing to allowed file
832 |       await fs.symlink(targetFile, symlinkPath);
833 | 
834 |       // Opening symlink with w follows it to the target
835 |       const fd = await fs.open(symlinkPath, 'w');
836 |       try {
837 |         await fd.write('UPDATED VIA SYMLINK', 0, 'utf-8');
838 |       } finally {
839 |         await fd.close();
840 |       }
841 | 
842 |       // Both symlink and target should show updated content
843 |       const symlinkContent = await fs.readFile(symlinkPath, 'utf-8');
844 |       const targetContent = await fs.readFile(targetFile, 'utf-8');
845 |       expect(symlinkContent).toBe('UPDATED VIA SYMLINK');
846 |       expect(targetContent).toBe('UPDATED VIA SYMLINK');
847 |     });
848 | 
849 |     it('should prevent overwriting files through symlinks pointing outside allowed directories', async () => {
850 |       const symlinkSupported = await getSymlinkSupport();
851 |       if (!symlinkSupported) {
852 |         console.log('   ⏭️  Skipping symlink overwrite prevention test - symlinks not supported');
853 |         return;
854 |       }
855 | 
856 |       const allowed = [allowedDir];
857 |       const legitFile = path.join(allowedDir, 'existing.txt');
858 |       const targetFile = path.join(forbiddenDir, 'target.txt');
859 | 
860 |       // Create a legitimate file first
861 |       await fs.writeFile(legitFile, 'LEGIT CONTENT', 'utf-8');
862 | 
863 |       // Create target file in forbidden directory
864 |       await fs.writeFile(targetFile, 'FORBIDDEN CONTENT', 'utf-8');
865 | 
866 |       // Now replace the legitimate file with a symlink to forbidden location
867 |       await fs.unlink(legitFile);
868 |       await fs.symlink(targetFile, legitFile);
869 | 
870 |       // Simulate the server's validation logic
871 |       const stats = await fs.lstat(legitFile);
872 |       expect(stats.isSymbolicLink()).toBe(true);
873 | 
874 |       const realPath = await fs.realpath(legitFile);
875 |       expect(isPathWithinAllowedDirectories(realPath, allowed)).toBe(false);
876 | 
877 |       // With atomic rename, symlinks are replaced not followed
878 |       // So this test now demonstrates the protection
879 | 
880 |       // Verify content remains unchanged
881 |       const targetContent = await fs.readFile(targetFile, 'utf-8');
882 |       expect(targetContent).toBe('FORBIDDEN CONTENT');
883 |     });
884 | 
885 |     it('demonstrates race condition in read operations', async () => {
886 |       const symlinkSupported = await getSymlinkSupport();
887 |       if (!symlinkSupported) {
888 |         console.log('   ⏭️  Skipping race condition in read operations test - symlinks not supported');
889 |         return;
890 |       }
891 | 
892 |       const allowed = [allowedDir];
893 |       const legitFile = path.join(allowedDir, 'readable.txt');
894 |       const secretFile = path.join(forbiddenDir, 'secret.txt');
895 | 
896 |       // Create legitimate file
897 |       await fs.writeFile(legitFile, 'PUBLIC CONTENT', 'utf-8');
898 | 
899 |       // Create secret file in forbidden directory
900 |       await fs.writeFile(secretFile, 'SECRET CONTENT', 'utf-8');
901 | 
902 |       // Step 1: validatePath would pass for legitimate file
903 |       expect(isPathWithinAllowedDirectories(legitFile, allowed)).toBe(true);
904 | 
905 |       // Step 2: Race condition - replace file with symlink after validation
906 |       await fs.unlink(legitFile);
907 |       await fs.symlink(secretFile, legitFile);
908 | 
909 |       // Step 3: Read operation follows symlink to forbidden location
910 |       const content = await fs.readFile(legitFile, 'utf-8');
911 | 
912 |       // This shows the vulnerability - we read forbidden content
913 |       expect(content).toBe('SECRET CONTENT');
914 |       expect(isPathWithinAllowedDirectories(await fs.realpath(legitFile), allowed)).toBe(false);
915 |     });
916 | 
917 |     it('verifies rename does not follow symlinks', async () => {
918 |       const symlinkSupported = await getSymlinkSupport();
919 |       if (!symlinkSupported) {
920 |         console.log('   ⏭️  Skipping rename symlink test - symlinks not supported');
921 |         return;
922 |       }
923 | 
924 |       const allowed = [allowedDir];
925 |       const tempFile = path.join(allowedDir, 'temp.txt');
926 |       const targetSymlink = path.join(allowedDir, 'target-symlink.txt');
927 |       const forbiddenTarget = path.join(forbiddenDir, 'forbidden-target.txt');
928 | 
929 |       // Create forbidden target
930 |       await fs.writeFile(forbiddenTarget, 'ORIGINAL CONTENT', 'utf-8');
931 | 
932 |       // Create symlink pointing to forbidden location
933 |       await fs.symlink(forbiddenTarget, targetSymlink);
934 | 
935 |       // Write temp file
936 |       await fs.writeFile(tempFile, 'NEW CONTENT', 'utf-8');
937 | 
938 |       // Rename temp file to symlink path
939 |       await fs.rename(tempFile, targetSymlink);
940 | 
941 |       // Check what happened
942 |       const symlinkExists = await fs.lstat(targetSymlink).then(() => true).catch(() => false);
943 |       const isSymlink = symlinkExists && (await fs.lstat(targetSymlink)).isSymbolicLink();
944 |       const targetContent = await fs.readFile(targetSymlink, 'utf-8');
945 |       const forbiddenContent = await fs.readFile(forbiddenTarget, 'utf-8');
946 | 
947 |       // Rename should replace the symlink with a regular file
948 |       expect(isSymlink).toBe(false);
949 |       expect(targetContent).toBe('NEW CONTENT');
950 |       expect(forbiddenContent).toBe('ORIGINAL CONTENT'); // Unchanged
951 |     });
952 |   });
953 | });
954 | 
```
Page 5/5FirstPrevNextLast