This is page 1 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
--------------------------------------------------------------------------------
/.dockerignore:
--------------------------------------------------------------------------------
```
1 | # Git files
2 | .git
3 | .gitignore
4 |
5 | # Node modules (installed within Docker)
6 | node_modules
7 |
8 | # Build output (created within Docker)
9 | dist
10 |
11 | # Logs
12 | logs
13 | *.log
14 |
15 | # OS generated files
16 | .DS_Store
17 | Thumbs.db
18 |
19 | # Environment files (should be injected, not built-in)
20 | .env*
21 | !/.env.example
22 |
23 | # Editor/IDE config
24 | .vscode/
25 | .idea/
26 |
27 | # Backups
28 | backups/
29 |
```
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
```
1 | # Operating System Files
2 | .DS_Store
3 | .DS_Store?
4 | ._*
5 | .Spotlight-V100
6 | .Trashes
7 | ehthumbs.db
8 | Thumbs.db
9 |
10 | # IDE and Editor Files
11 | .idea/
12 | .vscode/
13 | *.swp
14 | *.swo
15 | *~
16 | *.sublime-workspace
17 | *.sublime-project
18 |
19 | # TypeScript
20 | *.tsbuildinfo
21 | .tscache/
22 | *.js.map
23 | *.tgz
24 | .npm
25 | .eslintcache
26 | .rollup.cache
27 | *.mjs.map
28 | *.cjs.map
29 | *.d.ts.map
30 | *.d.ts
31 | !*.d.ts.template
32 | .pnp.js
33 | .pnp.cjs
34 | .pnp.mjs
35 | .pnp.json
36 | .pnp.ts
37 |
38 | # Demo and Example Directories
39 | demo/
40 | demos/
41 | example/
42 | examples/
43 | samples/
44 | .sample-env
45 | sample.*
46 | !sample.template.*
47 |
48 | # Node.js
49 | node_modules/
50 | npm-debug.log*
51 | yarn-debug.log*
52 | yarn-error.log*
53 | .pnpm-debug.log*
54 | .env
55 | .env.local
56 | .env.development.local
57 | .env.test.local
58 | .env.production.local
59 |
60 | # Python
61 | __pycache__/
62 | *.py[cod]
63 | *$py.class
64 | *.so
65 | .Python
66 | build/
67 | develop-eggs/
68 | dist/
69 | downloads/
70 | eggs/
71 | .eggs/
72 | lib/
73 | lib64/
74 | parts/
75 | sdist/
76 | var/
77 | wheels/
78 | *.egg-info/
79 | .installed.cfg
80 | *.egg
81 | .pytest_cache/
82 | .coverage
83 | htmlcov/
84 | .tox/
85 | .venv
86 | venv/
87 | ENV/
88 |
89 | # Java
90 | *.class
91 | *.log
92 | *.jar
93 | *.war
94 | *.nar
95 | *.ear
96 | *.zip
97 | *.tar.gz
98 | *.rar
99 | hs_err_pid*
100 | target/
101 | .gradle/
102 | build/
103 |
104 | # Ruby
105 | *.gem
106 | *.rbc
107 | /.config
108 | /coverage/
109 | /InstalledFiles
110 | /pkg/
111 | /spec/reports/
112 | /spec/examples.txt
113 | /test/tmp/
114 | /test/version_tmp/
115 | /tmp/
116 | .byebug_history
117 |
118 | # Compiled Files
119 | *.com
120 | *.class
121 | *.dll
122 | *.exe
123 | *.o
124 | *.so
125 |
126 | # Package Files
127 | *.7z
128 | *.dmg
129 | *.gz
130 | *.iso
131 | *.rar
132 | *.tar
133 | *.zip
134 |
135 | # Logs and Databases
136 | *.log
137 | *.sql
138 | *.sqlite
139 | *.sqlite3
140 |
141 | # Build and Distribution
142 | dist/
143 | build/
144 | out/
145 |
146 | # Testing
147 | coverage/
148 | .nyc_output/
149 |
150 | # Cache
151 | .cache/
152 | .parcel-cache/
153 |
154 | # Misc
155 | .DS_Store
156 | .env.local
157 | .env.development.local
158 | .env.test.local
159 | .env.production.local
160 | *.bak
161 | *.swp
162 | *.swo
163 | *~
164 | .history/
165 | repomix-output*
166 | mcp-servers.json
167 | logs/
168 |
```
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
```markdown
1 | # Filesystem MCP Server
2 |
3 | [](https://www.typescriptlang.org/)
4 | [](https://modelcontextprotocol.io/)
5 | []()
6 | [](https://opensource.org/licenses/Apache-2.0)
7 | []()
8 | [](https://github.com/cyanheads/filesystem-mcp-server)
9 |
10 | **Empower your AI agents with robust, platform-agnostic file system capabilities, now with STDIO & Streamable HTTP transport options.**
11 |
12 | This [Model Context Protocol (MCP)](https://modelcontextprotocol.io/) server provides a secure and reliable interface for AI agents to interact with the local filesystem. It enables reading, writing, updating, and managing files and directories, backed by a production-ready TypeScript foundation featuring comprehensive logging, error handling, security measures, and now supporting both **STDIO and HTTP transports**.
13 |
14 | ## Table of Contents
15 |
16 | - [Overview](#overview)
17 | - [Features](#features)
18 | - [Installation](#installation)
19 | - [Configuration](#configuration)
20 | - [Usage with MCP Clients](#usage-with-mcp-clients)
21 | - [Available Tools](#available-tools)
22 | - [Project Structure](#project-structure)
23 | - [Development](#development)
24 | - [License](#license)
25 |
26 | ## Overview
27 |
28 | The Model Context Protocol (MCP) is a standard framework allowing AI models to securely interact with external tools and data sources (resources). This server implements the MCP standard to expose essential filesystem operations as tools, enabling AI agents to:
29 |
30 | - Read and analyze file contents.
31 | - Create, modify, or overwrite files.
32 | - Manage directories and file paths.
33 | - Perform targeted updates within files.
34 |
35 | Built with TypeScript, the server emphasizes type safety, modularity, and robust error handling, making it suitable for reliable integration into AI workflows. It now supports both STDIO for direct process communication and HTTP for network-based interactions.
36 |
37 | ### Architecture
38 |
39 | The server employs a layered architecture for clarity and maintainability:
40 |
41 | ```mermaid
42 | flowchart TB
43 | subgraph TransportLayer["Transport Layer"]
44 | direction LR
45 | STDIO["STDIO Transport"]
46 | HTTP["HTTP Transport (Express, JWT Auth)"]
47 | end
48 |
49 | subgraph APILayer["API Layer"]
50 | direction LR
51 | MCP["MCP Protocol Interface"]
52 | Val["Input Validation (Zod)"]
53 | PathSan["Path Sanitization"]
54 |
55 | MCP --> Val --> PathSan
56 | end
57 |
58 | subgraph CoreServices["Core Services"]
59 | direction LR
60 | Config["Configuration (Zod-validated Env Vars)"]
61 | Logger["Logging (Winston, Context-aware)"]
62 | ErrorH["Error Handling (McpError, ErrorHandler)"]
63 | ServerLogic["MCP Server Logic"]
64 | State["Session State (Default Path)"]
65 |
66 | Config --> ServerLogic
67 | Logger --> ServerLogic & ErrorH
68 | ErrorH --> ServerLogic
69 | State --> ServerLogic
70 | end
71 |
72 | subgraph ToolImpl["Tool Implementation"]
73 | direction LR
74 | FSTools["Filesystem Tools"]
75 | Utils["Core Utilities (Internal, Security, Metrics, Parsing)"]
76 |
77 | FSTools --> ServerLogic
78 | Utils -- Used by --> FSTools
79 | Utils -- Used by --> CoreServices
80 | Utils -- Used by --> APILayer
81 | end
82 |
83 | TransportLayer --> MCP
84 | PathSan --> FSTools
85 |
86 | classDef layer fill:#2d3748,stroke:#4299e1,stroke-width:3px,rx:5,color:#fff
87 | classDef component fill:#1a202c,stroke:#a0aec0,stroke-width:2px,rx:3,color:#fff
88 | class TransportLayer,APILayer,CoreServices,ToolImpl layer
89 | class STDIO,HTTP,MCP,Val,PathSan,Config,Logger,ErrorH,ServerLogic,State,FSTools,Utils component
90 | ```
91 |
92 | - **Transport Layer**: Handles communication via STDIO or HTTP (with Express.js and JWT authentication).
93 | - **API Layer**: Manages MCP communication, validates inputs using Zod, and sanitizes paths.
94 | - **Core Services**: Oversees configuration (Zod-validated environment variables), context-aware logging, standardized error reporting, session state (like the default working directory), and the main MCP server instance.
95 | - **Tool Implementation**: Contains the specific logic for each filesystem tool, leveraging a refactored set of shared utilities categorized into internal, security, metrics, and parsing modules.
96 |
97 | ## Features
98 |
99 | - **Comprehensive File Operations**: Tools for reading, writing, listing, deleting, moving, and copying files and directories.
100 | - **Targeted Updates**: `update_file` tool allows precise search-and-replace operations within files, supporting plain text and regex.
101 | - **Session-Aware Path Management**: `set_filesystem_default` tool establishes a default working directory for resolving relative paths during a session.
102 | - **Dual Transport Support**:
103 | - **STDIO**: For direct, efficient communication when run as a child process.
104 | - **HTTP**: For network-based interaction, featuring RESTful endpoints, Server-Sent Events (SSE) for streaming, and JWT-based authentication.
105 | - **Security First**:
106 | - Built-in path sanitization prevents directory traversal attacks.
107 | - JWT authentication for HTTP transport.
108 | - Input validation with Zod.
109 | - **Robust Foundation**: Includes production-grade utilities, now reorganized for better modularity:
110 | - **Internal Utilities**: Context-aware logging (Winston), standardized error handling (`McpError`, `ErrorHandler`), request context management.
111 | - **Security Utilities**: Input sanitization, rate limiting, UUID and prefixed ID generation.
112 | - **Metrics Utilities**: Token counting.
113 | - **Parsing Utilities**: Natural language date parsing, partial JSON parsing.
114 | - **Enhanced Configuration**: Zod-validated environment variables for type-safe and reliable setup.
115 | - **Type Safety**: Fully implemented in TypeScript for improved reliability and maintainability.
116 |
117 | ## Installation
118 |
119 | ### Steps
120 |
121 | 1. **Clone the repository:**
122 | ```bash
123 | git clone https://github.com/cyanheads/filesystem-mcp-server.git
124 | cd filesystem-mcp-server
125 | ```
126 | 2. **Install dependencies:**
127 | ```bash
128 | npm install
129 | ```
130 | 3. **Build the project:**
131 | ```bash
132 | npm run build
133 | ```
134 | This compiles the TypeScript code to JavaScript in the `dist/` directory and makes the main script executable. The executable will be located at `dist/index.js`.
135 |
136 | ## Configuration
137 |
138 | Configure the server using environment variables (a `.env` file is supported):
139 |
140 | **Core Server Settings:**
141 |
142 | - **`MCP_LOG_LEVEL`** (Optional): Minimum logging level (e.g., `debug`, `info`, `warn`, `error`). Defaults to `debug`.
143 | - **`LOGS_DIR`** (Optional): Directory for log files. Defaults to `./logs` in the project root.
144 | - **`NODE_ENV`** (Optional): Runtime environment (e.g., `development`, `production`). Defaults to `development`.
145 |
146 | **Transport Settings:**
147 |
148 | - **`MCP_TRANSPORT_TYPE`** (Optional): Communication transport (`stdio` or `http`). Defaults to `stdio`.
149 | - **If `http` is selected:**
150 | - **`MCP_HTTP_PORT`** (Optional): Port for the HTTP server. Defaults to `3010`.
151 | - **`MCP_HTTP_HOST`** (Optional): Host for the HTTP server. Defaults to `127.0.0.1`.
152 | - **`MCP_ALLOWED_ORIGINS`** (Optional): Comma-separated list of allowed CORS origins (e.g., `http://localhost:3000,https://example.com`).
153 | - **`MCP_AUTH_SECRET_KEY`** (Required for HTTP Auth): A secure secret key (at least 32 characters long) for JWT authentication. **CRITICAL for production.**
154 |
155 | **Filesystem Security:**
156 |
157 | - **`FS_BASE_DIRECTORY`** (Optional): Defines the root directory for all filesystem operations. This can be an **absolute path** or a **path relative to the project root** (e.g., `./data_sandbox`). If set, the server's tools will be restricted to accessing files and directories only within this specified (and resolved absolute) path and its subdirectories. This is a crucial security feature to prevent unintended access to other parts of the filesystem. If not set (which is not recommended for production environments), a warning will be logged, and operations will not be restricted.
158 |
159 | **LLM & API Integration (Optional):**
160 |
161 | - **`OPENROUTER_APP_URL`**: Your application's URL for OpenRouter.
162 | - **`OPENROUTER_APP_NAME`**: Your application's name for OpenRouter. Defaults to `MCP_SERVER_NAME`.
163 | - **`OPENROUTER_API_KEY`**: API key for OpenRouter services.
164 | - **`LLM_DEFAULT_MODEL`**: Default LLM model to use (e.g., `google/gemini-2.5-flash-preview-05-20`).
165 | - **`LLM_DEFAULT_TEMPERATURE`**, **`LLM_DEFAULT_TOP_P`**, **`LLM_DEFAULT_MAX_TOKENS`**, **`LLM_DEFAULT_TOP_K`**, **`LLM_DEFAULT_MIN_P`**: Default parameters for LLM calls.
166 | - **`GEMINI_API_KEY`**: API key for Google Gemini services.
167 |
168 | **OAuth Proxy Integration (Optional, for advanced scenarios):**
169 |
170 | - **`OAUTH_PROXY_AUTHORIZATION_URL`**, **`OAUTH_PROXY_TOKEN_URL`**, **`OAUTH_PROXY_REVOCATION_URL`**, **`OAUTH_PROXY_ISSUER_URL`**, **`OAUTH_PROXY_SERVICE_DOCUMENTATION_URL`**, **`OAUTH_PROXY_DEFAULT_CLIENT_REDIRECT_URIS`**: Configuration for an OAuth proxy.
171 |
172 | Refer to `src/config/index.ts` and the `.clinerules` file for the complete list and Zod schema definitions.
173 |
174 | ## Usage with MCP Clients
175 |
176 | To allow an MCP client (like an AI assistant) to use this server:
177 |
178 | 1. **Run the Server:** Start the server from your terminal:
179 | ```bash
180 | node dist/index.js
181 | # Or if you are in the project root:
182 | # npm start
183 | ```
184 | 2. **Configure the Client:** Add the server to your MCP client's configuration. The exact method depends on the client.
185 |
186 | **For STDIO Transport (Default):**
187 | Typically involves specifying:
188 |
189 | - **Command:** `node`
190 | - **Arguments:** The absolute path to the built server executable (e.g., `/path/to/filesystem-mcp-server/dist/index.js`).
191 | - **Environment Variables (Optional):** Set any required environment variables from the [Configuration](#configuration) section.
192 |
193 | **Example MCP Settings for STDIO (Conceptual):**
194 |
195 | ```json
196 | {
197 | "mcpServers": {
198 | "filesystem_stdio": {
199 | "command": "node",
200 | "args": ["/path/to/filesystem-mcp-server/dist/index.js"],
201 | "env": {
202 | "MCP_LOG_LEVEL": "debug"
203 | // Other relevant env vars
204 | },
205 | "disabled": false,
206 | "autoApprove": []
207 | }
208 | }
209 | }
210 | ```
211 |
212 | **For HTTP Transport:**
213 | The client will need to know the server's URL (e.g., `http://localhost:3010`) and how to authenticate (e.g., providing a JWT Bearer token if `MCP_AUTH_SECRET_KEY` is set). Refer to your MCP client's documentation for HTTP server configuration.
214 |
215 | Once configured and running, the client will detect the server and its available tools.
216 |
217 | ## Available Tools
218 |
219 | The server exposes the following tools for filesystem interaction:
220 |
221 | | Tool | Description |
222 | | :--------------------------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
223 | | **`set_filesystem_default`** | Sets a default absolute path for the current session. Relative paths used in subsequent tool calls will be resolved against this default. Resets on server restart. |
224 | | **`read_file`** | Reads the entire content of a specified file as UTF-8 text. Accepts relative (resolved against default) or absolute paths. |
225 | | **`write_file`** | Writes content to a specified file. Creates the file (and necessary parent directories) if it doesn't exist, or overwrites it if it does. Accepts relative or absolute paths. |
226 | | **`update_file`** | Performs targeted search-and-replace operations within an existing file using an array of `{search, replace}` blocks. Ideal for localized changes. Supports plain text or regex search (`useRegex: true`) and replacing all occurrences (`replaceAll: true`). Accepts relative or absolute paths. File must exist. |
227 | | **`list_files`** | Lists files and directories within a specified path. Options include recursive listing (`includeNested: true`) and limiting the number of entries (`maxEntries`). Returns a formatted tree structure. Accepts relative or absolute paths. |
228 | | **`delete_file`** | Permanently removes a specific file. Accepts relative or absolute paths. |
229 | | **`delete_directory`** | Permanently removes a directory. Use `recursive: true` to remove non-empty directories and their contents (use with caution!). Accepts relative or absolute paths. |
230 | | **`create_directory`** | Creates a new directory at the specified path. By default (`create_parents: true`), it also creates any necessary parent directories. Accepts relative or absolute paths. |
231 | | **`move_path`** | Moves or renames a file or directory from a source path to a destination path. Accepts relative or absolute paths for both. |
232 | | **`copy_path`** | Copies a file or directory from a source path to a destination path. For directories, it copies recursively by default (`recursive: true`). Accepts relative or absolute paths. |
233 |
234 | _Refer to the tool registration files (`src/mcp-server/tools/*/registration.ts`) for detailed input/output schemas (Zod/JSON Schema)._
235 |
236 | ## Project Structure
237 |
238 | The codebase is organized for clarity and maintainability:
239 |
240 | ```
241 | filesystem-mcp-server/
242 | ├── dist/ # Compiled JavaScript output (after npm run build)
243 | ├── logs/ # Log files (created at runtime)
244 | ├── node_modules/ # Project dependencies
245 | ├── src/ # TypeScript source code
246 | │ ├── config/ # Configuration loading (index.ts)
247 | │ ├── mcp-server/ # Core MCP server logic
248 | │ │ ├── server.ts # Server initialization, tool registration, transport handling
249 | │ │ ├── state.ts # Session state management (e.g., default path)
250 | │ │ ├── tools/ # Individual tool implementations (one subdir per tool)
251 | │ │ │ ├── readFile/
252 | │ │ │ │ ├── index.ts
253 | │ │ │ │ ├── readFileLogic.ts
254 | │ │ │ │ └── registration.ts
255 | │ │ │ └── ... # Other tools (writeFile, updateFile, etc.)
256 | │ │ └── transports/ # Communication transport implementations
257 | │ │ ├── authentication/ # Auth middleware for HTTP
258 | │ │ │ └── authMiddleware.ts
259 | │ │ ├── httpTransport.ts
260 | │ │ └── stdioTransport.ts
261 | │ ├── types-global/ # Shared TypeScript types and interfaces
262 | │ │ ├── errors.ts # Custom error classes and codes (McpError, BaseErrorCode)
263 | │ │ ├── mcp.ts # MCP related types
264 | │ │ └── tool.ts # Tool definition types
265 | │ ├── utils/ # Reusable utility modules, categorized
266 | │ │ ├── internal/ # Core internal utilities (errorHandler, logger, requestContext)
267 | │ │ ├── metrics/ # Metrics-related utilities (tokenCounter)
268 | │ │ ├── parsing/ # Parsing utilities (dateParser, jsonParser)
269 | │ │ ├── security/ # Security-related utilities (idGenerator, rateLimiter, sanitization)
270 | │ │ └── index.ts # Barrel export for all utilities
271 | │ └── index.ts # Main application entry point
272 | ├── .clinerules # Cheatsheet for LLM assistants
273 | ├── .dockerignore
274 | ├── Dockerfile
275 | ├── LICENSE
276 | ├── mcp.json # MCP server manifest (generated by SDK or manually)
277 | ├── package.json
278 | ├── package-lock.json
279 | ├── README.md # This file
280 | ├── repomix.config.json
281 | ├── smithery.yaml # Smithery configuration (if used)
282 | └── tsconfig.json # TypeScript compiler options
283 | ```
284 |
285 | For a live, detailed view of the current structure, run: `npm run tree` (This script might need to be updated if `src/scripts/tree.ts` was part of the changes).
286 |
287 | > **Developer Note:** This repository includes a [.clinerules](.clinerules) file. This cheat sheet provides your LLM coding assistant with essential context about codebase patterns, file locations, and usage examples. Keep it updated as the server evolves!
288 |
289 | ## License
290 |
291 | This project is licensed under the Apache License 2.0. See the [LICENSE](LICENSE) file for details.
292 |
293 | ---
294 |
295 | <div align="center">
296 | Built with ❤️ and the <a href="https://modelcontextprotocol.io/">Model Context Protocol</a>
297 | </div>
298 |
```
--------------------------------------------------------------------------------
/src/mcp-server/tools/copyPath/index.ts:
--------------------------------------------------------------------------------
```typescript
1 | export { registerCopyPathTool } from './registration.js';
2 |
```
--------------------------------------------------------------------------------
/src/mcp-server/tools/movePath/index.ts:
--------------------------------------------------------------------------------
```typescript
1 | export { registerMovePathTool } from './registration.js';
2 |
```
--------------------------------------------------------------------------------
/src/mcp-server/tools/listFiles/index.ts:
--------------------------------------------------------------------------------
```typescript
1 | export { registerListFilesTool } from './registration.js';
2 |
```
--------------------------------------------------------------------------------
/src/mcp-server/tools/writeFile/index.ts:
--------------------------------------------------------------------------------
```typescript
1 | export { registerWriteFileTool } from './registration.js';
2 |
```
--------------------------------------------------------------------------------
/src/mcp-server/tools/deleteFile/index.ts:
--------------------------------------------------------------------------------
```typescript
1 | export { registerDeleteFileTool } from './registration.js';
2 |
```
--------------------------------------------------------------------------------
/src/mcp-server/tools/updateFile/index.ts:
--------------------------------------------------------------------------------
```typescript
1 | export { registerUpdateFileTool } from './registration.js';
2 |
```
--------------------------------------------------------------------------------
/src/mcp-server/tools/createDirectory/index.ts:
--------------------------------------------------------------------------------
```typescript
1 | export { registerCreateDirectoryTool } from './registration.js';
2 |
```
--------------------------------------------------------------------------------
/src/mcp-server/tools/deleteDirectory/index.ts:
--------------------------------------------------------------------------------
```typescript
1 | export { registerDeleteDirectoryTool } from './registration.js';
2 |
```
--------------------------------------------------------------------------------
/src/mcp-server/tools/setFilesystemDefault/index.ts:
--------------------------------------------------------------------------------
```typescript
1 | export { registerSetFilesystemDefaultTool } from './registration.js';
2 |
```
--------------------------------------------------------------------------------
/src/mcp-server/tools/readFile/index.ts:
--------------------------------------------------------------------------------
```typescript
1 | /**
2 | * Barrel file for the 'read_file' tool.
3 | * Exports the registration function for easy import into the main server setup.
4 | */
5 | export { registerReadFileTool } from './registration.js';
6 |
```
--------------------------------------------------------------------------------
/src/utils/metrics/index.ts:
--------------------------------------------------------------------------------
```typescript
1 | /**
2 | * @fileoverview Barrel file for metrics-related utility modules.
3 | * This file re-exports utilities for collecting and processing metrics,
4 | * such as token counting.
5 | * @module src/utils/metrics
6 | */
7 |
8 | export * from "./tokenCounter.js";
9 |
```
--------------------------------------------------------------------------------
/src/utils/parsing/index.ts:
--------------------------------------------------------------------------------
```typescript
1 | /**
2 | * @fileoverview Barrel file for parsing utility modules.
3 | * This file re-exports utilities related to parsing various data formats,
4 | * such as JSON and dates.
5 | * @module src/utils/parsing
6 | */
7 |
8 | export * from "./dateParser.js";
9 | export * from "./jsonParser.js";
10 |
```
--------------------------------------------------------------------------------
/src/utils/security/index.ts:
--------------------------------------------------------------------------------
```typescript
1 | /**
2 | * @fileoverview Barrel file for security-related utility modules.
3 | * This file re-exports utilities for input sanitization, rate limiting,
4 | * and ID generation.
5 | * @module src/utils/security
6 | */
7 |
8 | export * from "./idGenerator.js";
9 | export * from "./rateLimiter.js";
10 | export * from "./sanitization.js";
11 |
```
--------------------------------------------------------------------------------
/src/utils/internal/index.ts:
--------------------------------------------------------------------------------
```typescript
1 | /**
2 | * @fileoverview Barrel file for internal utility modules.
3 | * This file re-exports core internal utilities related to error handling,
4 | * logging, and request context management.
5 | * @module src/utils/internal
6 | */
7 |
8 | export * from "./errorHandler.js";
9 | export * from "./logger.js";
10 | export * from "./requestContext.js";
11 |
```
--------------------------------------------------------------------------------
/mcp.json:
--------------------------------------------------------------------------------
```json
1 | {
2 | "mcpServers": {
3 | "@cyanheads/filesystem-mcp-server": {
4 | "command": "node",
5 | "args": ["dist/index.js"],
6 | "env": {
7 | "MCP_LOG_LEVEL": "debug",
8 | "MCP_TRANSPORT_TYPE": "stdio",
9 | "MCP_HTTP_PORT": "3010",
10 | "LOGS_DIR": "./logs",
11 | "FS_BASE_DIRECTORY": ""
12 | }
13 | }
14 | }
15 | }
16 |
```
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
```json
1 | {
2 | "compilerOptions": {
3 | "target": "ES2020",
4 | "module": "ESNext",
5 | "moduleResolution": "node",
6 | "esModuleInterop": true,
7 | "strict": true,
8 | "outDir": "./dist",
9 | "rootDir": "./src",
10 | "declaration": true,
11 | "skipLibCheck": true,
12 | "forceConsistentCasingInFileNames": true
13 | },
14 | "include": ["src/**/*"],
15 | "exclude": ["node_modules", "dist"]
16 | }
```
--------------------------------------------------------------------------------
/repomix.config.json:
--------------------------------------------------------------------------------
```json
1 | {
2 | "output": {
3 | "filePath": "repomix-output.xml",
4 | "style": "xml",
5 | "removeComments": false,
6 | "removeEmptyLines": false,
7 | "topFilesLength": 5,
8 | "showLineNumbers": false,
9 | "copyToClipboard": false
10 | },
11 | "include": [],
12 | "ignore": {
13 | "useGitignore": true,
14 | "useDefaultPatterns": true,
15 | "customPatterns": []
16 | },
17 | "security": {
18 | "enableSecurityCheck": true
19 | }
20 | }
```
--------------------------------------------------------------------------------
/.github/workflows/publish.yml:
--------------------------------------------------------------------------------
```yaml
1 | name: Publish Package to npm
2 | on:
3 | push:
4 | tags:
5 | - 'v*'
6 |
7 | jobs:
8 | build-and-publish:
9 | runs-on: ubuntu-latest
10 | steps:
11 | - uses: actions/checkout@v4
12 |
13 | - name: Setup Node.js
14 | uses: actions/setup-node@v4
15 | with:
16 | node-version: '18.x'
17 | registry-url: 'https://registry.npmjs.org'
18 |
19 | - name: Install dependencies
20 | run: npm ci
21 |
22 | - name: Build
23 | run: npm run build
24 |
25 | - name: Publish to npm
26 | run: npm publish
27 | env:
28 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
29 |
```
--------------------------------------------------------------------------------
/src/utils/index.ts:
--------------------------------------------------------------------------------
```typescript
1 | /**
2 | * @fileoverview Barrel file for the utils module.
3 | * This file re-exports all utilities from their categorized subdirectories,
4 | * providing a single entry point for accessing utility functions.
5 | * @module src/utils
6 | */
7 |
8 | // Re-export all utilities from their categorized subdirectories
9 | export * from "./internal/index.js";
10 | export * from "./metrics/index.js";
11 | export * from "./parsing/index.js";
12 | export * from "./security/index.js";
13 |
14 | // It's good practice to have index.ts files in each subdirectory
15 | // that export the contents of that directory.
16 | // Assuming those will be created or already exist.
17 | // If not, this might need adjustment to export specific files, e.g.:
18 | // export * from './internal/errorHandler.js';
19 | // export * from './internal/logger.js';
20 | // ... etc.
21 |
```
--------------------------------------------------------------------------------
/src/types-global/tool.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { z } from 'zod';
2 | import { RequestContext } from "../utils/internal/requestContext.js";
3 | import { McpToolResponse } from './mcp.js';
4 |
5 | /**
6 | * Base interface for tool input parameters
7 | */
8 | export interface BaseToolInput {
9 | [key: string]: unknown;
10 | }
11 |
12 | /**
13 | * Base interface for tool response content
14 | */
15 | export interface BaseToolResponse {
16 | [key: string]: unknown;
17 | }
18 |
19 | /**
20 | * Interface for tool registration options
21 | */
22 | export interface ToolRegistrationOptions<TInput extends BaseToolInput> {
23 | /** Zod schema for input validation */
24 | inputSchema: z.ZodType<TInput>;
25 | /** Description of the tool */
26 | description: string;
27 | /** Example usage scenarios */
28 | examples?: { name: string; input: TInput; description?: string }[];
29 | }
30 |
31 | /**
32 | * Interface for a tool handler function
33 | */
34 | export type ToolHandler<TInput extends BaseToolInput, TResponse extends McpToolResponse> = (
35 | input: TInput,
36 | context: RequestContext
37 | ) => Promise<TResponse>;
38 |
```
--------------------------------------------------------------------------------
/src/types-global/mcp.ts:
--------------------------------------------------------------------------------
```typescript
1 | // Type definitions for the MCP (Message Control Protocol) protocol
2 |
3 | // Common response types
4 | export interface McpContent {
5 | type: "text";
6 | text: string;
7 | }
8 |
9 | export interface McpToolResponse {
10 | content: McpContent[];
11 | isError?: boolean;
12 | }
13 |
14 | // Resource response types
15 | export interface ResourceContent {
16 | uri: string;
17 | text: string;
18 | mimeType?: string;
19 | }
20 |
21 | export interface ResourceResponse {
22 | contents: ResourceContent[];
23 | }
24 |
25 | // Prompt response types
26 | export interface PromptMessageContent {
27 | type: "text";
28 | text: string;
29 | }
30 |
31 | export interface PromptMessage {
32 | role: "user" | "assistant";
33 | content: PromptMessageContent;
34 | }
35 |
36 | export interface PromptResponse {
37 | messages: PromptMessage[];
38 | }
39 |
40 | // Helper functions
41 | export const createToolResponse = (text: string, isError?: boolean): McpToolResponse => ({
42 | content: [{
43 | type: "text",
44 | text
45 | }],
46 | isError
47 | });
48 |
49 | export const createResourceResponse = (uri: string, text: string, mimeType?: string): ResourceResponse => ({
50 | contents: [{
51 | uri,
52 | text,
53 | mimeType
54 | }]
55 | });
56 |
57 | export const createPromptResponse = (text: string, role: "user" | "assistant" = "assistant"): PromptResponse => ({
58 | messages: [{
59 | role,
60 | content: {
61 | type: "text",
62 | text
63 | }
64 | }]
65 | });
```
--------------------------------------------------------------------------------
/smithery.yaml:
--------------------------------------------------------------------------------
```yaml
1 | startCommand:
2 | type: stdio
3 | configSchema:
4 | type: object
5 | properties:
6 | MCP_TRANSPORT_TYPE:
7 | type: string
8 | enum: ["stdio", "http"]
9 | default: "stdio"
10 | description: "MCP communication transport ('stdio' or 'http')."
11 | MCP_HTTP_PORT:
12 | type: integer
13 | default: 3010
14 | description: "HTTP server port (if MCP_TRANSPORT_TYPE is 'http')."
15 | MCP_LOG_LEVEL:
16 | type: string
17 | default: "debug" # Default from src/config/index.ts
18 | description: "Minimum logging level (e.g., 'debug', 'info', 'warn', 'error')."
19 | LOGS_DIR:
20 | type: string
21 | default: "./logs"
22 | description: "Directory for log files. Relative to project root."
23 | FS_BASE_DIRECTORY:
24 | type: string
25 | default: ""
26 | description: "Optional base directory for all filesystem operations. If set, tools cannot access paths outside this directory. Must be an absolute path if provided."
27 | # Add other relevant env vars from src/config/index.ts if they should be configurable via Smithery
28 | # For example, MCP_HTTP_HOST, MCP_ALLOWED_ORIGINS, MCP_AUTH_SECRET_KEY could be added here
29 | # if they need to be set dynamically when the server is started by Smithery.
30 | # For now, keeping it simple with the core ones.
31 | commandFunction: |
32 | (config) => ({
33 | "command": "node",
34 | "args": ["dist/index.js"],
35 | "env": {
36 | "MCP_TRANSPORT_TYPE": config.MCP_TRANSPORT_TYPE,
37 | "MCP_HTTP_PORT": String(config.MCP_HTTP_PORT), // Ensure port is a string for env
38 | "MCP_LOG_LEVEL": config.MCP_LOG_LEVEL,
39 | "LOGS_DIR": config.LOGS_DIR,
40 | "FS_BASE_DIRECTORY": config.FS_BASE_DIRECTORY
41 | // Map other configured env vars here if added to configSchema
42 | }
43 | })
44 |
```
--------------------------------------------------------------------------------
/src/mcp-server/tools/setFilesystemDefault/setFilesystemDefaultLogic.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { z } from 'zod';
2 | import { McpError } from '../../../types-global/errors.js';
3 | import { RequestContext } from '../../../utils/internal/requestContext.js';
4 | import { serverState } from '../../state.js'; // Import the server state
5 |
6 | // Define the input schema using Zod for validation
7 | export const SetFilesystemDefaultInputSchema = z.object({
8 | path: z.string().min(1, 'Path cannot be empty')
9 | .describe('The absolute path to set as the default for resolving relative paths during this session.'),
10 | });
11 |
12 | // Define the TypeScript type for the input
13 | export type SetFilesystemDefaultInput = z.infer<typeof SetFilesystemDefaultInputSchema>;
14 |
15 | // Define the TypeScript type for the output (simple success message)
16 | export interface SetFilesystemDefaultOutput {
17 | message: string;
18 | currentDefaultPath: string | null;
19 | }
20 |
21 | /**
22 | * Sets the default filesystem path for the current session.
23 | *
24 | * @param {SetFilesystemDefaultInput} input - The input object containing the absolute path.
25 | * @param {RequestContext} context - The request context for logging and error handling.
26 | * @returns {Promise<SetFilesystemDefaultOutput>} A promise that resolves with a success message and the new default path.
27 | * @throws {McpError} Throws McpError if the path is invalid or not absolute.
28 | */
29 | export const setFilesystemDefaultLogic = async (input: SetFilesystemDefaultInput, context: RequestContext): Promise<SetFilesystemDefaultOutput> => {
30 | const { path: newPath } = input;
31 |
32 | // The validation (absolute check, sanitization) happens within serverState.setDefaultFilesystemPath
33 | serverState.setDefaultFilesystemPath(newPath, context);
34 |
35 | const currentPath = serverState.getDefaultFilesystemPath();
36 | return {
37 | message: `Default filesystem path successfully set to: ${currentPath}`,
38 | currentDefaultPath: currentPath,
39 | };
40 | };
41 |
```
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
```dockerfile
1 | # ---- Builder Stage ----
2 | FROM node:22-slim AS builder
3 |
4 | # Set working directory
5 | WORKDIR /app
6 |
7 | # Install dependencies
8 | # Copy package files first for better caching
9 | COPY package.json package-lock.json* ./
10 | # Install all dependencies (including devDependencies needed for build)
11 | RUN npm install --production=false --ignore-scripts
12 |
13 | # Copy source code (respecting .dockerignore)
14 | COPY . .
15 |
16 | # Build the application using the 'tsc' command specified in package.json
17 | RUN npm run build
18 |
19 | # Remove devDependencies after build
20 | RUN npm prune --production
21 |
22 |
23 | # ---- Final Stage ----
24 | FROM node:22-slim
25 |
26 | ARG FS_BASE_DIRECTORY=""
27 | ENV NODE_ENV=production \
28 | PATH="/home/service-user/.local/bin:${PATH}" \
29 | FS_BASE_DIRECTORY=${FS_BASE_DIRECTORY}
30 |
31 | # Install mcp-proxy globally for runtime use
32 | # Combine update, install, and clean in one layer
33 | RUN apt-get update && \
34 | apt-get install -y --no-install-recommends curl && \
35 | npm install -g [email protected] && \
36 | npm cache clean --force && \
37 | apt-get purge -y --auto-remove curl && \
38 | apt-get clean && \
39 | rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
40 |
41 | # Create non-root user and group
42 | # Create app directory and set permissions
43 | RUN groupadd --system --gid 1987 service-user && \
44 | useradd --system --uid 1987 --gid service-user -m service-user && \
45 | mkdir -p /app && \
46 | chown -R service-user:service-user /app
47 |
48 | # Set working directory
49 | WORKDIR /app
50 |
51 | # Copy necessary artifacts from builder stage
52 | # Ensure package.json is copied for runtime metadata if needed
53 | COPY --from=builder --chown=service-user:service-user /app/package.json ./package.json
54 | # Copy production node_modules
55 | COPY --from=builder --chown=service-user:service-user /app/node_modules ./node_modules
56 | # Copy the build output from the correct directory ('dist')
57 | COPY --from=builder --chown=service-user:service-user /app/dist ./dist
58 |
59 | # Switch to non-root user
60 | USER service-user
61 |
62 | # Expose port if necessary (Update port number if your app uses a different one)
63 | # EXPOSE 3000
64 |
65 | # Define the command to run the application using the correct build output path ('dist')
66 | CMD ["mcp-proxy", "node", "dist/index.js"]
67 |
```
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
```json
1 | {
2 | "name": "@cyanheads/filesystem-mcp-server",
3 | "version": "1.0.4",
4 | "description": "A Model Context Protocol (MCP) server for platform-agnostic file capabilities, including advanced search and replace, and directory tree traversal",
5 | "main": "dist/index.js",
6 | "files": [
7 | "dist"
8 | ],
9 | "type": "module",
10 | "repository": {
11 | "type": "git",
12 | "url": "git+https://github.com/cyanheads/filesystem-mcp-server.git"
13 | },
14 | "bugs": {
15 | "url": "https://github.com/cyanheads/filesystem-mcp-server/issues"
16 | },
17 | "homepage": "https://github.com/cyanheads/filesystem-mcp-server#readme",
18 | "scripts": {
19 | "build": "tsc",
20 | "clean": "ts-node scripts/clean.ts",
21 | "rebuild": "ts-node --esm scripts/clean.ts && npm run build",
22 | "start": "node dist/index.js",
23 | "start:stdio": "MCP_LOG_LEVEL=debug MCP_TRANSPORT_TYPE=stdio node dist/index.js",
24 | "start:http": "MCP_LOG_LEVEL=debug MCP_TRANSPORT_TYPE=http node dist/index.js",
25 | "tree": "ts-node --esm scripts/tree.ts",
26 | "format": "prettier --write \"**/*.{ts,js,json,md,html,css}\"",
27 | "inspector": "mcp-inspector --config mcp.json --server @cyanheads/filesystem-mcp-server"
28 | },
29 | "dependencies": {
30 | "@google/genai": "^1.0.1",
31 | "@modelcontextprotocol/sdk": "^1.12.0",
32 | "@types/jsonwebtoken": "^9.0.9",
33 | "@types/node": "^22.15.21",
34 | "@types/sanitize-html": "^2.16.0",
35 | "@types/validator": "13.15.1",
36 | "chalk": "^5.4.1",
37 | "chrono-node": "^2.8.0",
38 | "cli-table3": "^0.6.5",
39 | "dotenv": "^16.5.0",
40 | "express": "^5.1.0",
41 | "ignore": "^7.0.4",
42 | "jsonwebtoken": "^9.0.2",
43 | "openai": "^4.103.0",
44 | "partial-json": "^0.1.7",
45 | "sanitize-html": "^2.17.0",
46 | "tiktoken": "^1.0.21",
47 | "ts-node": "^10.9.2",
48 | "typescript": "^5.8.3",
49 | "validator": "13.15.0",
50 | "winston": "^3.17.0",
51 | "winston-daily-rotate-file": "^5.0.0",
52 | "yargs": "^17.7.2",
53 | "zod": "^3.25.23"
54 | },
55 | "devDependencies": {
56 | "@types/express": "^5.0.2",
57 | "@types/js-yaml": "^4.0.9",
58 | "axios": "^1.9.0",
59 | "js-yaml": "^4.1.0",
60 | "prettier": "^3.5.3",
61 | "typedoc": "^0.28.4"
62 | },
63 | "keywords": [
64 | "AI-integration",
65 | "MCP",
66 | "authentication",
67 | "client",
68 | "file-operations",
69 | "file-system",
70 | "filesystem",
71 | "http",
72 | "jwt",
73 | "LLM",
74 | "model-context-protocol",
75 | "sdk",
76 | "search-replace",
77 | "server",
78 | "sse",
79 | "template",
80 | "typescript"
81 | ],
82 | "author": "Casey Hand <[email protected]> (https://github.com/cyanheads/filesystem-mcp-server#readme)",
83 | "license": "Apache-2.0",
84 | "engines": {
85 | "node": ">=16.0.0"
86 | },
87 | "publishConfig": {
88 | "access": "public"
89 | }
90 | }
91 |
```
--------------------------------------------------------------------------------
/src/mcp-server/tools/writeFile/registration.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2 | import { BaseErrorCode, McpError } from '../../../types-global/errors.js';
3 | import { ErrorHandler } from '../../../utils/internal/errorHandler.js';
4 | import { logger } from '../../../utils/internal/logger.js';
5 | import { requestContextService } from '../../../utils/internal/requestContext.js';
6 | import {
7 | WriteFileInput,
8 | WriteFileInputSchema,
9 | writeFileLogic,
10 | } from './writeFileLogic.js';
11 |
12 | /**
13 | * Registers the 'write_file' tool with the MCP server.
14 | *
15 | * @param {McpServer} server - The McpServer instance to register the tool with.
16 | * @returns {Promise<void>} A promise that resolves when the tool is registered.
17 | * @throws {McpError} Throws an error if registration fails.
18 | */
19 | export const registerWriteFileTool = async (server: McpServer): Promise<void> => {
20 | const registrationContext = requestContextService.createRequestContext({ operation: 'RegisterWriteFileTool' });
21 | logger.info("Attempting to register 'write_file' tool", registrationContext);
22 |
23 | await ErrorHandler.tryCatch(
24 | async () => {
25 | server.tool(
26 | 'write_file', // Tool name
27 | 'Writes content to a specified file. Creates the file (and necessary directories) if it doesn\'t exist, or overwrites it if it does. Accepts relative or absolute paths (resolved like readFile).', // Description
28 | WriteFileInputSchema.shape, // Pass the schema shape
29 | async (params, extra) => {
30 | const typedParams = params as WriteFileInput;
31 | const callContext = requestContextService.createRequestContext({ operation: 'WriteFileToolExecution', parentId: registrationContext.requestId });
32 | logger.info(`Executing 'write_file' tool for path: ${typedParams.path}`, callContext);
33 |
34 | // ErrorHandler will catch McpErrors thrown by the logic
35 | const result = await ErrorHandler.tryCatch(
36 | () => writeFileLogic(typedParams, callContext),
37 | {
38 | operation: 'writeFileLogic',
39 | context: callContext,
40 | input: { path: typedParams.path, content: '[CONTENT REDACTED]' }, // Redact content for logging
41 | errorCode: BaseErrorCode.INTERNAL_ERROR
42 | }
43 | );
44 |
45 | logger.info(`Successfully executed 'write_file' for path: ${result.writtenPath}`, callContext);
46 |
47 | // Format the successful response
48 | return {
49 | content: [{ type: 'text', text: result.message }],
50 | };
51 | }
52 | );
53 | logger.info("'write_file' tool registered successfully", registrationContext);
54 | },
55 | {
56 | operation: 'registerWriteFileTool',
57 | context: registrationContext,
58 | errorCode: BaseErrorCode.CONFIGURATION_ERROR,
59 | critical: true
60 | }
61 | );
62 | };
63 |
```
--------------------------------------------------------------------------------
/scripts/clean.ts:
--------------------------------------------------------------------------------
```typescript
1 | #!/usr/bin/env node
2 |
3 | /**
4 | * Clean Script
5 | * ============
6 | *
7 | * Description:
8 | * A utility script to clean build artifacts and temporary directories from your project.
9 | * By default, it removes the 'dist' and 'logs' directories if they exist.
10 | *
11 | * Usage:
12 | * - Add to package.json: "clean": "node dist/scripts/clean.js"
13 | * - Can be run directly: npm run clean
14 | * - Often used in rebuild scripts: "rebuild": "npm run clean && npm run build"
15 | * - Can be used with arguments to specify custom directories: node dist/scripts/clean.js temp coverage
16 | *
17 | * Platform compatibility:
18 | * - Works on all platforms (Windows, macOS, Linux) using Node.js path normalization
19 | */
20 |
21 | import { rm, access } from 'fs/promises';
22 | import { join } from 'path';
23 |
24 | /**
25 | * Interface for clean operation result
26 | */
27 | interface CleanResult {
28 | dir: string;
29 | status: 'success' | 'skipped';
30 | reason?: string;
31 | }
32 |
33 | /**
34 | * Check if a directory exists without using fs.Stats
35 | */
36 | async function directoryExists(dirPath: string): Promise<boolean> {
37 | try {
38 | await access(dirPath);
39 | return true;
40 | } catch {
41 | return false;
42 | }
43 | }
44 |
45 | /**
46 | * Main clean function
47 | */
48 | const clean = async (): Promise<void> => {
49 | try {
50 | // Default directories to clean
51 | let dirsToClean: string[] = ['dist', 'logs'];
52 |
53 | // If directories are specified as command line arguments, use those instead
54 | const args = process.argv.slice(2);
55 | if (args.length > 0) {
56 | dirsToClean = args;
57 | }
58 |
59 | console.log(`Cleaning directories: ${dirsToClean.join(', ')}`);
60 |
61 | // Process each directory
62 | const results = await Promise.allSettled(
63 | dirsToClean.map(async (dir): Promise<CleanResult> => {
64 | const dirPath = join(process.cwd(), dir);
65 |
66 | try {
67 | // Check if directory exists before attempting to remove it
68 | const exists = await directoryExists(dirPath);
69 |
70 | if (!exists) {
71 | return { dir, status: 'skipped', reason: 'does not exist' };
72 | }
73 |
74 | // Remove directory if it exists
75 | await rm(dirPath, { recursive: true, force: true });
76 | return { dir, status: 'success' };
77 | } catch (error) {
78 | throw error;
79 | }
80 | })
81 | );
82 |
83 | // Report results
84 | for (const result of results) {
85 | if (result.status === 'fulfilled') {
86 | const { dir, status, reason } = result.value;
87 | if (status === 'success') {
88 | console.log(`✓ Successfully cleaned ${dir} directory`);
89 | } else {
90 | console.log(`- ${dir} directory ${reason}, skipping cleanup`);
91 | }
92 | } else {
93 | console.error(`× Error cleaning directory: ${result.reason}`);
94 | }
95 | }
96 | } catch (error) {
97 | console.error('× Error during cleanup:', error instanceof Error ? error.message : error);
98 | process.exit(1);
99 | }
100 | };
101 |
102 | // Execute the clean function
103 | clean();
```
--------------------------------------------------------------------------------
/src/mcp-server/tools/readFile/readFileLogic.ts:
--------------------------------------------------------------------------------
```typescript
1 | import fs from 'fs/promises'; // Ensure fs is imported
2 | import { z } from 'zod';
3 | // No longer need config for base directory here
4 | import { BaseErrorCode, McpError } from '../../../types-global/errors.js';
5 | import { RequestContext } from '../../../utils/internal/requestContext.js';
6 | import { serverState } from '../../state.js'; // Import serverState for path resolution
7 | // No longer need sanitization directly here for path resolution
8 |
9 | // Define the input schema using Zod for validation - Updated description
10 | export const ReadFileInputSchema = z.object({
11 | path: z.string().min(1, 'Path cannot be empty')
12 | .describe('The path to the file to read. Can be relative or absolute. If relative, it resolves against the path set by `set_filesystem_default`. If absolute, it is used directly. If relative and no default is set, an error occurs.'),
13 | });
14 |
15 | // Define the TypeScript type for the input
16 | export type ReadFileInput = z.infer<typeof ReadFileInputSchema>;
17 |
18 | // Define the TypeScript type for the output
19 | export interface ReadFileOutput {
20 | content: string;
21 | }
22 |
23 | /**
24 | * Reads the content of a specified file.
25 | *
26 | * @param {ReadFileInput} input - The input object containing the file path.
27 | * @param {RequestContext} context - The request context for logging and error handling.
28 | * @returns {Promise<ReadFileOutput>} A promise that resolves with the file content.
29 | * @throws {McpError} Throws McpError for path resolution errors, file not found, or I/O errors.
30 | */
31 | export const readFileLogic = async (input: ReadFileInput, context: RequestContext): Promise<ReadFileOutput> => {
32 | const { path: requestedPath } = input;
33 |
34 | // Resolve the path using serverState (handles relative/absolute logic and sanitization)
35 | // This will throw McpError if a relative path is given without a default set.
36 | const absolutePath = serverState.resolvePath(requestedPath, context);
37 |
38 | try {
39 | // Read the file content using the resolved absolute path
40 | const content = await fs.readFile(absolutePath, 'utf8');
41 | return { content };
42 | } catch (error: any) {
43 | // Handle specific file system errors
44 | // Handle specific file system errors using the resolved absolutePath in messages
45 | if (error.code === 'ENOENT') {
46 | // Use NOT_FOUND error code
47 | throw new McpError(BaseErrorCode.NOT_FOUND, `File not found at resolved path: ${absolutePath}`, { ...context, requestedPath, resolvedPath: absolutePath, originalError: error });
48 | }
49 | if (error.code === 'EISDIR') {
50 | // Use VALIDATION_ERROR
51 | throw new McpError(BaseErrorCode.VALIDATION_ERROR, `Resolved path is a directory, not a file: ${absolutePath}`, { ...context, requestedPath, resolvedPath: absolutePath, originalError: error });
52 | }
53 | // Handle other potential I/O errors using INTERNAL_ERROR
54 | throw new McpError(BaseErrorCode.INTERNAL_ERROR, `Failed to read file: ${error.message || 'Unknown I/O error'}`, { ...context, originalError: error });
55 | }
56 | };
57 |
```
--------------------------------------------------------------------------------
/src/mcp-server/tools/setFilesystemDefault/registration.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2 | import { BaseErrorCode, McpError } from '../../../types-global/errors.js';
3 | import { ErrorHandler } from '../../../utils/internal/errorHandler.js';
4 | import { logger } from '../../../utils/internal/logger.js';
5 | import { requestContextService } from '../../../utils/internal/requestContext.js';
6 | import {
7 | SetFilesystemDefaultInput,
8 | SetFilesystemDefaultInputSchema,
9 | setFilesystemDefaultLogic,
10 | } from './setFilesystemDefaultLogic.js';
11 |
12 | /**
13 | * Registers the 'set_filesystem_default' tool with the MCP server.
14 | *
15 | * @param {McpServer} server - The McpServer instance to register the tool with.
16 | * @returns {Promise<void>} A promise that resolves when the tool is registered.
17 | * @throws {McpError} Throws an error if registration fails.
18 | */
19 | export const registerSetFilesystemDefaultTool = async (server: McpServer): Promise<void> => {
20 | const registrationContext = requestContextService.createRequestContext({ operation: 'RegisterSetFilesystemDefaultTool' });
21 | logger.info("Attempting to register 'set_filesystem_default' tool", registrationContext);
22 |
23 | await ErrorHandler.tryCatch(
24 | async () => {
25 | server.tool(
26 | 'set_filesystem_default', // Tool name
27 | 'Sets a default absolute path for the current session. Relative paths used in other filesystem tools (like readFile) will be resolved against this default. The default is cleared on server restart.', // Description
28 | SetFilesystemDefaultInputSchema.shape, // Pass the schema shape
29 | async (params, extra) => {
30 | const typedParams = params as SetFilesystemDefaultInput;
31 | const callContext = requestContextService.createRequestContext({ operation: 'SetFilesystemDefaultToolExecution', parentId: registrationContext.requestId });
32 | logger.info(`Executing 'set_filesystem_default' tool with path: ${typedParams.path}`, callContext);
33 |
34 | // ErrorHandler will catch McpErrors thrown by the logic (e.g., non-absolute path)
35 | const result = await ErrorHandler.tryCatch(
36 | () => setFilesystemDefaultLogic(typedParams, callContext),
37 | {
38 | operation: 'setFilesystemDefaultLogic',
39 | context: callContext,
40 | input: typedParams, // Input is automatically sanitized by ErrorHandler
41 | errorCode: BaseErrorCode.INTERNAL_ERROR // Default error if unexpected failure
42 | }
43 | );
44 |
45 | logger.info(`Successfully executed 'set_filesystem_default'. Current default: ${result.currentDefaultPath}`, callContext);
46 |
47 | // Format the successful response
48 | return {
49 | content: [{ type: 'text', text: result.message }],
50 | };
51 | }
52 | );
53 | logger.info("'set_filesystem_default' tool registered successfully", registrationContext);
54 | },
55 | {
56 | operation: 'registerSetFilesystemDefaultTool',
57 | context: registrationContext,
58 | errorCode: BaseErrorCode.CONFIGURATION_ERROR,
59 | critical: true
60 | }
61 | );
62 | };
63 |
```
--------------------------------------------------------------------------------
/src/mcp-server/tools/readFile/registration.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2 | import { BaseErrorCode, McpError } from '../../../types-global/errors.js';
3 | import { ErrorHandler } from '../../../utils/internal/errorHandler.js';
4 | import { logger } from '../../../utils/internal/logger.js';
5 | import { requestContextService } from '../../../utils/internal/requestContext.js';
6 | import { ReadFileInput, ReadFileInputSchema, readFileLogic } from './readFileLogic.js';
7 |
8 | /**
9 | * Registers the 'read_file' tool with the MCP server.
10 | *
11 | * @param {McpServer} server - The McpServer instance to register the tool with.
12 | * @returns {Promise<void>} A promise that resolves when the tool is registered.
13 | * @throws {McpError} Throws an error if registration fails.
14 | */
15 | export const registerReadFileTool = async (server: McpServer): Promise<void> => {
16 | const registrationContext = requestContextService.createRequestContext({ operation: 'RegisterReadFileTool' });
17 | logger.info("Attempting to register 'read_file' tool", registrationContext);
18 |
19 | await ErrorHandler.tryCatch(
20 | async () => {
21 | // Removed explicit generic <ReadFileInput>, let it be inferred from the schema
22 | server.tool(
23 | 'read_file', // Tool name
24 | 'Reads the entire content of a specified file as UTF-8 text. Accepts relative or absolute paths. Relative paths are resolved against the session default set by `set_filesystem_default`.', // Updated Description
25 | ReadFileInputSchema.shape, // Pass the schema shape, not the object instance
26 | async (params, extra) => { // Correct handler signature: params and extra
27 | // Cast params to the correct type within the handler for type safety
28 | const typedParams = params as ReadFileInput;
29 | // Create a new context for this specific tool execution
30 | // We might potentially use `extra.requestId` if available and needed for tracing, but let's keep it simple for now.
31 | const callContext = requestContextService.createRequestContext({ operation: 'ReadFileToolExecution', parentId: registrationContext.requestId });
32 | logger.info(`Executing 'read_file' tool for path: ${typedParams.path}`, callContext);
33 |
34 | // ErrorHandler will catch McpErrors thrown by readFileLogic and format them
35 | const result = await ErrorHandler.tryCatch(
36 | () => readFileLogic(typedParams, callContext), // Use typedParams
37 | {
38 | operation: 'readFileLogic',
39 | context: callContext,
40 | input: typedParams, // Input is automatically sanitized by ErrorHandler for logging
41 | errorCode: BaseErrorCode.INTERNAL_ERROR // Default error if unexpected failure
42 | }
43 | );
44 |
45 | logger.info(`Successfully read file: ${typedParams.path}`, callContext); // Use typedParams
46 |
47 | // Format the successful response
48 | return {
49 | content: [{ type: 'text', text: result.content }],
50 | };
51 | }
52 | );
53 | logger.info("'read_file' tool registered successfully", registrationContext);
54 | },
55 | {
56 | operation: 'registerReadFileTool',
57 | context: registrationContext,
58 | errorCode: BaseErrorCode.CONFIGURATION_ERROR, // Error code if registration itself fails
59 | critical: true // Failure to register is critical
60 | }
61 | );
62 | };
63 |
```
--------------------------------------------------------------------------------
/src/mcp-server/tools/deleteFile/registration.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2 | import { BaseErrorCode, McpError } from '../../../types-global/errors.js';
3 | import { ErrorHandler } from '../../../utils/internal/errorHandler.js';
4 | import { logger } from '../../../utils/internal/logger.js';
5 | import { requestContextService } from '../../../utils/internal/requestContext.js';
6 | import { sanitization } from '../../../utils/security/sanitization.js';
7 | import {
8 | DeleteFileInputSchema,
9 | deleteFileLogic
10 | } from './deleteFileLogic.js';
11 |
12 | /**
13 | * Registers the 'delete_file' tool with the MCP server.
14 | *
15 | * @param {McpServer} server - The McpServer instance to register the tool with.
16 | * @returns {Promise<void>} A promise that resolves when the tool is registered.
17 | * @throws {McpError} Throws an error if registration fails.
18 | */
19 | export const registerDeleteFileTool = async (server: McpServer): Promise<void> => {
20 | const registrationContext = requestContextService.createRequestContext({ operation: 'RegisterDeleteFileTool' });
21 | logger.info("Attempting to register 'delete_file' tool", registrationContext);
22 |
23 | await ErrorHandler.tryCatch(
24 | async () => {
25 | server.tool(
26 | 'delete_file', // Tool name
27 | 'Removes a specific file. Accepts relative or absolute paths.', // Description
28 | DeleteFileInputSchema.shape, // Pass the schema shape
29 | async (params, extra) => {
30 | // Validate input using the Zod schema
31 | const validationResult = DeleteFileInputSchema.safeParse(params);
32 | if (!validationResult.success) {
33 | const errorContext = requestContextService.createRequestContext({ operation: 'DeleteFileToolValidation' });
34 | logger.error('Invalid input parameters for delete_file tool', { ...errorContext, errors: validationResult.error.errors });
35 | throw new McpError(BaseErrorCode.VALIDATION_ERROR, `Invalid parameters: ${validationResult.error.errors.map(e => `${e.path.join('.')} - ${e.message}`).join(', ')}`, errorContext);
36 | }
37 | const typedParams = validationResult.data; // Use validated data
38 |
39 | // Create context for this execution
40 | const callContext = requestContextService.createRequestContext({ operation: 'DeleteFileToolExecution' });
41 | logger.info(`Executing 'delete_file' tool for path: ${typedParams.path}`, callContext);
42 |
43 | // ErrorHandler will catch McpErrors thrown by the logic
44 | const result = await ErrorHandler.tryCatch(
45 | () => deleteFileLogic(typedParams, callContext),
46 | {
47 | operation: 'deleteFileLogic',
48 | context: callContext,
49 | input: sanitization.sanitizeForLogging(typedParams), // Sanitize path if needed
50 | errorCode: BaseErrorCode.INTERNAL_ERROR
51 | }
52 | );
53 |
54 | logger.info(`Successfully executed 'delete_file' for path: ${result.deletedPath}`, callContext);
55 |
56 | // Format the successful response
57 | return {
58 | content: [{ type: 'text', text: result.message }],
59 | };
60 | }
61 | );
62 | logger.info("'delete_file' tool registered successfully", registrationContext);
63 | },
64 | {
65 | operation: 'registerDeleteFileTool',
66 | context: registrationContext,
67 | errorCode: BaseErrorCode.CONFIGURATION_ERROR,
68 | critical: true
69 | }
70 | );
71 | };
72 |
```
--------------------------------------------------------------------------------
/src/mcp-server/transports/stdioTransport.ts:
--------------------------------------------------------------------------------
```typescript
1 | /**
2 | * @fileoverview Handles the setup and connection for the Stdio MCP transport.
3 | * Implements the MCP Specification 2025-03-26 for stdio transport.
4 | * This transport communicates directly over standard input (stdin) and
5 | * standard output (stdout), typically used when the MCP server is launched
6 | * as a child process by a host application.
7 | *
8 | * Specification Reference:
9 | * https://github.com/modelcontextprotocol/modelcontextprotocol/blob/main/docs/specification/2025-03-26/basic/transports.mdx#stdio
10 | *
11 | * --- Authentication Note ---
12 | * As per the MCP Authorization Specification (2025-03-26, Section 1.2),
13 | * STDIO transports SHOULD NOT implement HTTP-based authentication flows.
14 | * Authorization is typically handled implicitly by the host application
15 | * controlling the server process. This implementation follows that guideline.
16 | *
17 | * @see {@link https://github.com/modelcontextprotocol/modelcontextprotocol/blob/main/docs/specification/2025-03-26/basic/authorization.mdx | MCP Authorization Specification}
18 | * @module src/mcp-server/transports/stdioTransport
19 | */
20 |
21 | import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
22 | import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
23 | import { ErrorHandler, logger, RequestContext } from "../../utils/index.js";
24 |
25 | /**
26 | * Connects a given `McpServer` instance to the Stdio transport.
27 | * This function initializes the SDK's `StdioServerTransport`, which manages
28 | * communication over `process.stdin` and `process.stdout` according to the
29 | * MCP stdio transport specification.
30 | *
31 | * MCP Spec Points Covered by SDK's `StdioServerTransport`:
32 | * - Reads JSON-RPC messages (requests, notifications, responses, batches) from stdin.
33 | * - Writes JSON-RPC messages to stdout.
34 | * - Handles newline delimiters and ensures no embedded newlines in output messages.
35 | * - Ensures only valid MCP messages are written to stdout.
36 | *
37 | * Logging via the `logger` utility MAY result in output to stderr, which is
38 | * permitted by the spec for logging purposes.
39 | *
40 | * @param server - The `McpServer` instance.
41 | * @param parentContext - The logging and tracing context from the calling function.
42 | * @returns A promise that resolves when the Stdio transport is successfully connected.
43 | * @throws {Error} If the connection fails during setup.
44 | */
45 | export async function connectStdioTransport(
46 | server: McpServer,
47 | parentContext: RequestContext,
48 | ): Promise<void> {
49 | const operationContext = {
50 | ...parentContext,
51 | operation: "connectStdioTransport",
52 | transportType: "Stdio",
53 | };
54 | logger.debug("Attempting to connect stdio transport...", operationContext);
55 |
56 | try {
57 | logger.debug("Creating StdioServerTransport instance...", operationContext);
58 | const transport = new StdioServerTransport();
59 |
60 | logger.debug(
61 | "Connecting McpServer instance to StdioServerTransport...",
62 | operationContext,
63 | );
64 | await server.connect(transport);
65 |
66 | logger.info(
67 | "MCP Server connected and listening via stdio transport.",
68 | operationContext,
69 | );
70 | if (process.stdout.isTTY) {
71 | console.log(
72 | `\n🚀 MCP Server running in STDIO mode.\n (MCP Spec: 2025-03-26 Stdio Transport)\n`,
73 | );
74 | }
75 | } catch (err) {
76 | ErrorHandler.handleError(err, { ...operationContext, critical: true });
77 | throw err; // Re-throw after handling to allow caller to react if necessary
78 | }
79 | }
80 |
```
--------------------------------------------------------------------------------
/src/mcp-server/tools/movePath/registration.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2 | import { BaseErrorCode, McpError } from '../../../types-global/errors.js';
3 | import { ErrorHandler } from '../../../utils/internal/errorHandler.js';
4 | import { logger } from '../../../utils/internal/logger.js';
5 | import { requestContextService } from '../../../utils/internal/requestContext.js';
6 | import { sanitization } from '../../../utils/security/sanitization.js';
7 | import {
8 | MovePathInputSchema,
9 | movePathLogic
10 | } from './movePathLogic.js';
11 |
12 | /**
13 | * Registers the 'move_path' tool with the MCP server.
14 | *
15 | * @param {McpServer} server - The McpServer instance to register the tool with.
16 | * @returns {Promise<void>} A promise that resolves when the tool is registered.
17 | * @throws {McpError} Throws an error if registration fails.
18 | */
19 | export const registerMovePathTool = async (server: McpServer): Promise<void> => {
20 | const registrationContext = requestContextService.createRequestContext({ operation: 'RegisterMovePathTool' });
21 | logger.info("Attempting to register 'move_path' tool", registrationContext);
22 |
23 | await ErrorHandler.tryCatch(
24 | async () => {
25 | server.tool(
26 | 'move_path', // Tool name
27 | 'Moves or renames a file or directory. Accepts relative or absolute paths for source and destination.', // Description
28 | MovePathInputSchema.shape, // Pass the schema shape
29 | async (params, extra) => {
30 | // Validate input using the Zod schema
31 | const validationResult = MovePathInputSchema.safeParse(params);
32 | if (!validationResult.success) {
33 | const errorContext = requestContextService.createRequestContext({ operation: 'MovePathToolValidation' });
34 | logger.error('Invalid input parameters for move_path tool', { ...errorContext, errors: validationResult.error.errors });
35 | throw new McpError(BaseErrorCode.VALIDATION_ERROR, `Invalid parameters: ${validationResult.error.errors.map(e => `${e.path.join('.')} - ${e.message}`).join(', ')}`, errorContext);
36 | }
37 | const typedParams = validationResult.data; // Use validated data
38 |
39 | // Create context for this execution
40 | const callContext = requestContextService.createRequestContext({ operation: 'MovePathToolExecution' });
41 | logger.info(`Executing 'move_path' tool from "${typedParams.source_path}" to "${typedParams.destination_path}"`, callContext);
42 |
43 | // ErrorHandler will catch McpErrors thrown by the logic
44 | const result = await ErrorHandler.tryCatch(
45 | () => movePathLogic(typedParams, callContext),
46 | {
47 | operation: 'movePathLogic',
48 | context: callContext,
49 | input: sanitization.sanitizeForLogging(typedParams), // Sanitize paths
50 | errorCode: BaseErrorCode.INTERNAL_ERROR
51 | }
52 | );
53 |
54 | logger.info(`Successfully executed 'move_path' from "${result.sourcePath}" to "${result.destinationPath}"`, callContext);
55 |
56 | // Format the successful response
57 | return {
58 | content: [{ type: 'text', text: result.message }],
59 | };
60 | }
61 | );
62 | logger.info("'move_path' tool registered successfully", registrationContext);
63 | },
64 | {
65 | operation: 'registerMovePathTool',
66 | context: registrationContext,
67 | errorCode: BaseErrorCode.CONFIGURATION_ERROR,
68 | critical: true
69 | }
70 | );
71 | };
72 |
```
--------------------------------------------------------------------------------
/src/mcp-server/tools/deleteFile/deleteFileLogic.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 input schema using Zod for validation
9 | export const DeleteFileInputSchema = z.object({
10 | path: z.string().min(1, 'Path cannot be empty')
11 | .describe('The path to the file to delete. Can be relative or absolute (resolved like readFile).'),
12 | });
13 |
14 | // Define the TypeScript type for the input
15 | export type DeleteFileInput = z.infer<typeof DeleteFileInputSchema>;
16 |
17 | // Define the TypeScript type for the output
18 | export interface DeleteFileOutput {
19 | message: string;
20 | deletedPath: string;
21 | }
22 |
23 | /**
24 | * Deletes a specified file.
25 | *
26 | * @param {DeleteFileInput} input - The input object containing the path to the file.
27 | * @param {RequestContext} context - The request context.
28 | * @returns {Promise<DeleteFileOutput>} A promise resolving with the deletion status.
29 | * @throws {McpError} For path errors, file not found, or I/O errors.
30 | */
31 | export const deleteFileLogic = async (input: DeleteFileInput, context: RequestContext): Promise<DeleteFileOutput> => {
32 | const { path: requestedPath } = input;
33 | const logicContext = { ...context, tool: 'deleteFileLogic' };
34 | logger.debug(`deleteFileLogic: Received request to delete path "${requestedPath}"`, logicContext);
35 |
36 | // Resolve the path
37 | const absolutePath = serverState.resolvePath(requestedPath, context);
38 | logger.debug(`deleteFileLogic: Resolved path to "${absolutePath}"`, { ...logicContext, requestedPath });
39 |
40 | try {
41 | // Check if the path exists and is a file before attempting deletion
42 | const stats = await fs.stat(absolutePath);
43 | if (!stats.isFile()) {
44 | logger.warning(`deleteFileLogic: Path is not a file "${absolutePath}"`, { ...logicContext, requestedPath });
45 | throw new McpError(BaseErrorCode.VALIDATION_ERROR, `Path is not a file: ${absolutePath}`, { ...logicContext, requestedPath, resolvedPath: absolutePath });
46 | }
47 |
48 | // Attempt to delete the file
49 | await fs.unlink(absolutePath);
50 | logger.info(`deleteFileLogic: Successfully deleted file "${absolutePath}"`, { ...logicContext, requestedPath });
51 |
52 | return {
53 | message: `Successfully deleted file: ${absolutePath}`,
54 | deletedPath: absolutePath,
55 | };
56 |
57 | } catch (error: any) {
58 | logger.error(`deleteFileLogic: Error deleting file "${absolutePath}"`, { ...logicContext, requestedPath, error: error.message, code: error.code });
59 |
60 | if (error instanceof McpError) {
61 | throw error; // Re-throw known McpErrors
62 | }
63 |
64 | if (error.code === 'ENOENT') {
65 | logger.warning(`deleteFileLogic: File not found at "${absolutePath}"`, { ...logicContext, requestedPath });
66 | // Even though we checked with stat, there's a small race condition possibility,
67 | // or the error came from stat itself. Treat ENOENT as file not found.
68 | throw new McpError(BaseErrorCode.NOT_FOUND, `File not found at path: ${absolutePath}`, { ...logicContext, requestedPath, resolvedPath: absolutePath, originalError: error });
69 | }
70 |
71 | // Handle other potential I/O errors
72 | throw new McpError(BaseErrorCode.INTERNAL_ERROR, `Failed to delete file: ${error.message || 'Unknown I/O error'}`, { ...logicContext, requestedPath, resolvedPath: absolutePath, originalError: error });
73 | }
74 | };
75 |
```
--------------------------------------------------------------------------------
/src/mcp-server/tools/copyPath/registration.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2 | import { BaseErrorCode, McpError } from '../../../types-global/errors.js';
3 | import { ErrorHandler } from '../../../utils/internal/errorHandler.js';
4 | import { logger } from '../../../utils/internal/logger.js';
5 | import { requestContextService } from '../../../utils/internal/requestContext.js';
6 | import { sanitization } from '../../../utils/security/sanitization.js';
7 | import {
8 | CopyPathInputSchema,
9 | copyPathLogic
10 | } from './copyPathLogic.js';
11 |
12 | /**
13 | * Registers the 'copy_path' tool with the MCP server.
14 | *
15 | * @param {McpServer} server - The McpServer instance to register the tool with.
16 | * @returns {Promise<void>} A promise that resolves when the tool is registered.
17 | * @throws {McpError} Throws an error if registration fails.
18 | */
19 | export const registerCopyPathTool = async (server: McpServer): Promise<void> => {
20 | const registrationContext = requestContextService.createRequestContext({ operation: 'RegisterCopyPathTool' });
21 | logger.info("Attempting to register 'copy_path' tool", registrationContext);
22 |
23 | await ErrorHandler.tryCatch(
24 | async () => {
25 | server.tool(
26 | 'copy_path', // Tool name
27 | 'Copies a file or directory to a new location. Accepts relative or absolute paths. Defaults to recursive copy for directories.', // Description
28 | CopyPathInputSchema.shape, // Pass the schema shape
29 | async (params, extra) => {
30 | // Validate input using the Zod schema
31 | const validationResult = CopyPathInputSchema.safeParse(params);
32 | if (!validationResult.success) {
33 | const errorContext = requestContextService.createRequestContext({ operation: 'CopyPathToolValidation' });
34 | logger.error('Invalid input parameters for copy_path tool', { ...errorContext, errors: validationResult.error.errors });
35 | throw new McpError(BaseErrorCode.VALIDATION_ERROR, `Invalid parameters: ${validationResult.error.errors.map(e => `${e.path.join('.')} - ${e.message}`).join(', ')}`, errorContext);
36 | }
37 | const typedParams = validationResult.data; // Use validated data
38 |
39 | // Create context for this execution
40 | const callContext = requestContextService.createRequestContext({ operation: 'CopyPathToolExecution' });
41 | logger.info(`Executing 'copy_path' tool from "${typedParams.source_path}" to "${typedParams.destination_path}", recursive: ${typedParams.recursive}`, callContext);
42 |
43 | // ErrorHandler will catch McpErrors thrown by the logic
44 | const result = await ErrorHandler.tryCatch(
45 | () => copyPathLogic(typedParams, callContext),
46 | {
47 | operation: 'copyPathLogic',
48 | context: callContext,
49 | input: sanitization.sanitizeForLogging(typedParams), // Sanitize paths
50 | errorCode: BaseErrorCode.INTERNAL_ERROR
51 | }
52 | );
53 |
54 | logger.info(`Successfully executed 'copy_path' from "${result.sourcePath}" to "${result.destinationPath}", recursive: ${result.wasRecursive}`, callContext);
55 |
56 | // Format the successful response
57 | return {
58 | content: [{ type: 'text', text: result.message }],
59 | };
60 | }
61 | );
62 | logger.info("'copy_path' tool registered successfully", registrationContext);
63 | },
64 | {
65 | operation: 'registerCopyPathTool',
66 | context: registrationContext,
67 | errorCode: BaseErrorCode.CONFIGURATION_ERROR,
68 | critical: true
69 | }
70 | );
71 | };
72 |
```
--------------------------------------------------------------------------------
/src/mcp-server/tools/deleteDirectory/registration.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2 | import { BaseErrorCode, McpError } from '../../../types-global/errors.js';
3 | import { ErrorHandler } from '../../../utils/internal/errorHandler.js';
4 | import { logger } from '../../../utils/internal/logger.js';
5 | import { requestContextService } from '../../../utils/internal/requestContext.js';
6 | import { sanitization } from '../../../utils/security/sanitization.js';
7 | import {
8 | DeleteDirectoryInputSchema,
9 | deleteDirectoryLogic
10 | } from './deleteDirectoryLogic.js';
11 |
12 | /**
13 | * Registers the 'delete_directory' tool with the MCP server.
14 | *
15 | * @param {McpServer} server - The McpServer instance to register the tool with.
16 | * @returns {Promise<void>} A promise that resolves when the tool is registered.
17 | * @throws {McpError} Throws an error if registration fails.
18 | */
19 | export const registerDeleteDirectoryTool = async (server: McpServer): Promise<void> => {
20 | const registrationContext = requestContextService.createRequestContext({ operation: 'RegisterDeleteDirectoryTool' });
21 | logger.info("Attempting to register 'delete_directory' tool", registrationContext);
22 |
23 | await ErrorHandler.tryCatch(
24 | async () => {
25 | server.tool(
26 | 'delete_directory', // Tool name
27 | 'Removes a directory. Optionally removes recursively. Accepts relative or absolute paths.', // Description
28 | DeleteDirectoryInputSchema.shape, // Pass the schema shape
29 | async (params, extra) => {
30 | // Validate input using the Zod schema
31 | const validationResult = DeleteDirectoryInputSchema.safeParse(params);
32 | if (!validationResult.success) {
33 | const errorContext = requestContextService.createRequestContext({ operation: 'DeleteDirectoryToolValidation' });
34 | logger.error('Invalid input parameters for delete_directory tool', { ...errorContext, errors: validationResult.error.errors });
35 | throw new McpError(BaseErrorCode.VALIDATION_ERROR, `Invalid parameters: ${validationResult.error.errors.map(e => `${e.path.join('.')} - ${e.message}`).join(', ')}`, errorContext);
36 | }
37 | const typedParams = validationResult.data; // Use validated data
38 |
39 | // Create context for this execution
40 | const callContext = requestContextService.createRequestContext({ operation: 'DeleteDirectoryToolExecution' });
41 | logger.info(`Executing 'delete_directory' tool for path: ${typedParams.path}, recursive: ${typedParams.recursive}`, callContext);
42 |
43 | // ErrorHandler will catch McpErrors thrown by the logic
44 | const result = await ErrorHandler.tryCatch(
45 | () => deleteDirectoryLogic(typedParams, callContext),
46 | {
47 | operation: 'deleteDirectoryLogic',
48 | context: callContext,
49 | input: sanitization.sanitizeForLogging(typedParams), // Sanitize path
50 | errorCode: BaseErrorCode.INTERNAL_ERROR
51 | }
52 | );
53 |
54 | logger.info(`Successfully executed 'delete_directory' for path: ${result.deletedPath}, recursive: ${result.wasRecursive}`, callContext);
55 |
56 | // Format the successful response
57 | return {
58 | content: [{ type: 'text', text: result.message }],
59 | };
60 | }
61 | );
62 | logger.info("'delete_directory' tool registered successfully", registrationContext);
63 | },
64 | {
65 | operation: 'registerDeleteDirectoryTool',
66 | context: registrationContext,
67 | errorCode: BaseErrorCode.CONFIGURATION_ERROR,
68 | critical: true
69 | }
70 | );
71 | };
72 |
```
--------------------------------------------------------------------------------
/src/mcp-server/tools/createDirectory/registration.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2 | import { BaseErrorCode, McpError } from '../../../types-global/errors.js';
3 | import { ErrorHandler } from '../../../utils/internal/errorHandler.js';
4 | import { logger } from '../../../utils/internal/logger.js';
5 | import { requestContextService } from '../../../utils/internal/requestContext.js';
6 | import { sanitization } from '../../../utils/security/sanitization.js';
7 | import {
8 | CreateDirectoryInputSchema,
9 | createDirectoryLogic
10 | } from './createDirectoryLogic.js';
11 |
12 | /**
13 | * Registers the 'create_directory' tool with the MCP server.
14 | *
15 | * @param {McpServer} server - The McpServer instance to register the tool with.
16 | * @returns {Promise<void>} A promise that resolves when the tool is registered.
17 | * @throws {McpError} Throws an error if registration fails.
18 | */
19 | export const registerCreateDirectoryTool = async (server: McpServer): Promise<void> => {
20 | const registrationContext = requestContextService.createRequestContext({ operation: 'RegisterCreateDirectoryTool' });
21 | logger.info("Attempting to register 'create_directory' tool", registrationContext);
22 |
23 | await ErrorHandler.tryCatch(
24 | async () => {
25 | server.tool(
26 | 'create_directory', // Tool name
27 | 'Creates a directory. Optionally creates parent directories. Accepts relative or absolute paths.', // Description
28 | CreateDirectoryInputSchema.shape, // Pass the schema shape
29 | async (params, extra) => {
30 | // Validate input using the Zod schema
31 | const validationResult = CreateDirectoryInputSchema.safeParse(params);
32 | if (!validationResult.success) {
33 | const errorContext = requestContextService.createRequestContext({ operation: 'CreateDirectoryToolValidation' });
34 | logger.error('Invalid input parameters for create_directory tool', { ...errorContext, errors: validationResult.error.errors });
35 | throw new McpError(BaseErrorCode.VALIDATION_ERROR, `Invalid parameters: ${validationResult.error.errors.map(e => `${e.path.join('.')} - ${e.message}`).join(', ')}`, errorContext);
36 | }
37 | const typedParams = validationResult.data; // Use validated data
38 |
39 | // Create context for this execution
40 | const callContext = requestContextService.createRequestContext({ operation: 'CreateDirectoryToolExecution' });
41 | logger.info(`Executing 'create_directory' tool for path: ${typedParams.path}, create_parents: ${typedParams.create_parents}`, callContext);
42 |
43 | // ErrorHandler will catch McpErrors thrown by the logic
44 | const result = await ErrorHandler.tryCatch(
45 | () => createDirectoryLogic(typedParams, callContext),
46 | {
47 | operation: 'createDirectoryLogic',
48 | context: callContext,
49 | input: sanitization.sanitizeForLogging(typedParams), // Sanitize path
50 | errorCode: BaseErrorCode.INTERNAL_ERROR
51 | }
52 | );
53 |
54 | logger.info(`Successfully executed 'create_directory' for path: ${result.createdPath}, parentsCreated: ${result.parentsCreated}`, callContext);
55 |
56 | // Format the successful response
57 | return {
58 | content: [{ type: 'text', text: result.message }],
59 | };
60 | }
61 | );
62 | logger.info("'create_directory' tool registered successfully", registrationContext);
63 | },
64 | {
65 | operation: 'registerCreateDirectoryTool',
66 | context: registrationContext,
67 | errorCode: BaseErrorCode.CONFIGURATION_ERROR,
68 | critical: true
69 | }
70 | );
71 | };
72 |
```
--------------------------------------------------------------------------------
/src/mcp-server/tools/listFiles/registration.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2 | import { BaseErrorCode, McpError } from '../../../types-global/errors.js';
3 | import { ErrorHandler } from '../../../utils/internal/errorHandler.js';
4 | import { logger } from '../../../utils/internal/logger.js';
5 | import { requestContextService } from '../../../utils/internal/requestContext.js';
6 | import { sanitization } from '../../../utils/security/sanitization.js';
7 | import {
8 | ListFilesInputSchema,
9 | listFilesLogic
10 | } from './listFilesLogic.js';
11 |
12 | /**
13 | * Registers the 'list_files' tool with the MCP server.
14 | * This tool lists files and directories at a specified path.
15 | *
16 | * @param {McpServer} server - The McpServer instance to register the tool with.
17 | * @returns {Promise<void>} A promise that resolves when the tool is registered.
18 | * @throws {McpError} Throws an error if registration fails.
19 | */
20 | export const registerListFilesTool = async (server: McpServer): Promise<void> => {
21 | const registrationContext = requestContextService.createRequestContext({ operation: 'RegisterListFilesTool' });
22 | logger.info("Attempting to register 'list_files' tool", registrationContext);
23 |
24 | await ErrorHandler.tryCatch(
25 | async () => {
26 | server.tool(
27 | 'list_files', // Tool name
28 | 'Lists files and directories within the specified directory. Optionally lists recursively and returns a tree-like structure. Includes an optional `maxEntries` parameter (default 50) to limit the number of items returned.', // Updated Description
29 | ListFilesInputSchema.shape, // Pass the schema shape (already updated in logic file)
30 | async (params, extra) => {
31 | // Validate input using the Zod schema
32 | const validationResult = ListFilesInputSchema.safeParse(params);
33 | if (!validationResult.success) {
34 | // Create context without explicit parentRequestId
35 | const errorContext = requestContextService.createRequestContext({ operation: 'ListFilesToolValidation' });
36 | logger.error('Invalid input parameters for list_files tool', { ...errorContext, errors: validationResult.error.errors });
37 | throw new McpError(BaseErrorCode.VALIDATION_ERROR, `Invalid parameters: ${validationResult.error.errors.map(e => `${e.path.join('.')} - ${e.message}`).join(', ')}`, errorContext);
38 | }
39 | const typedParams = validationResult.data; // Use validated data
40 |
41 | // Create context for this execution without explicit parentRequestId
42 | const callContext = requestContextService.createRequestContext({ operation: 'ListFilesToolExecution' });
43 | logger.info(`Executing 'list_files' tool for path: ${typedParams.path}, nested: ${typedParams.includeNested}`, callContext);
44 |
45 | // Call the logic function
46 | const result = await ErrorHandler.tryCatch(
47 | () => listFilesLogic(typedParams, callContext),
48 | {
49 | operation: 'listFilesLogic',
50 | context: callContext,
51 | input: sanitization.sanitizeForLogging(typedParams), // Sanitize input for logging
52 | errorCode: BaseErrorCode.INTERNAL_ERROR
53 | }
54 | );
55 |
56 | logger.info(`Successfully executed 'list_files' for path: ${result.resolvedPath}. Items: ${result.itemCount}`, callContext);
57 |
58 | // Format the successful response - return the tree structure
59 | return {
60 | content: [{ type: 'text', text: result.tree }],
61 | };
62 | }
63 | );
64 | logger.info("'list_files' tool registered successfully", registrationContext);
65 | },
66 | {
67 | operation: 'registerListFilesTool',
68 | context: registrationContext,
69 | errorCode: BaseErrorCode.CONFIGURATION_ERROR,
70 | critical: true // Critical for server startup
71 | }
72 | );
73 | };
74 |
```
--------------------------------------------------------------------------------
/src/types-global/errors.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { z } from "zod";
2 | import { McpContent, McpToolResponse } from "./mcp.js";
3 |
4 | /**
5 | * Defines a set of standardized error codes for common issues within MCP servers or tools.
6 | * These codes help clients understand the nature of an error programmatically.
7 | */
8 | export enum BaseErrorCode {
9 | /** Access denied due to invalid credentials or lack of authentication. */
10 | UNAUTHORIZED = 'UNAUTHORIZED',
11 | /** Access denied despite valid authentication, due to insufficient permissions. */
12 | FORBIDDEN = 'FORBIDDEN',
13 | /** The requested resource or entity could not be found. */
14 | NOT_FOUND = 'NOT_FOUND',
15 | /** The request could not be completed due to a conflict with the current state of the resource. */
16 | CONFLICT = 'CONFLICT',
17 | /** The request failed due to invalid input parameters or data. */
18 | VALIDATION_ERROR = 'VALIDATION_ERROR',
19 | /** The request was rejected because the client has exceeded rate limits. */
20 | RATE_LIMITED = 'RATE_LIMITED',
21 | /** The request timed out before a response could be generated. */
22 | TIMEOUT = 'TIMEOUT',
23 | /** The service is temporarily unavailable, possibly due to maintenance or overload. */
24 | SERVICE_UNAVAILABLE = 'SERVICE_UNAVAILABLE',
25 | /** An unexpected error occurred on the server side. */
26 | INTERNAL_ERROR = 'INTERNAL_ERROR',
27 | /** An error occurred, but the specific cause is unknown or cannot be categorized. */
28 | UNKNOWN_ERROR = 'UNKNOWN_ERROR',
29 | /** An error occurred during the loading or validation of configuration data. */
30 | CONFIGURATION_ERROR = 'CONFIGURATION_ERROR'
31 | }
32 |
33 | /**
34 | * Custom error class for MCP-specific errors.
35 | * Encapsulates a `BaseErrorCode`, a descriptive message, and optional details.
36 | * Provides a method to format the error into a standard MCP tool response.
37 | */
38 | export class McpError extends Error {
39 | /**
40 | * Creates an instance of McpError.
41 | * @param {BaseErrorCode} code - The standardized error code.
42 | * @param {string} message - A human-readable description of the error.
43 | * @param {Record<string, unknown>} [details] - Optional additional details about the error.
44 | */
45 | constructor(
46 | public code: BaseErrorCode,
47 | message: string,
48 | public details?: Record<string, unknown>
49 | ) {
50 | super(message);
51 | // Set the error name for identification
52 | this.name = 'McpError';
53 | // Ensure the prototype chain is correct
54 | Object.setPrototypeOf(this, McpError.prototype);
55 | }
56 |
57 | /**
58 | * Converts the McpError instance into a standard MCP tool response format.
59 | * This is useful for returning structured errors from tool handlers.
60 | * @returns {McpToolResponse} An object representing the error, suitable for an MCP tool response.
61 | */
62 | toResponse(): McpToolResponse {
63 | // Construct the text content for the error response
64 | const errorText = `Error [${this.code}]: ${this.message}${
65 | this.details ? `\nDetails: ${JSON.stringify(this.details, null, 2)}` : ''
66 | }`;
67 |
68 | const content: McpContent = {
69 | type: "text",
70 | text: errorText
71 | };
72 |
73 | // Return the structured error response
74 | return {
75 | content: [content],
76 | isError: true // Mark this response as an error
77 | };
78 | }
79 | }
80 |
81 | /**
82 | * Zod schema for validating error objects, potentially used for parsing
83 | * error responses or validating error structures internally.
84 | */
85 | export const ErrorSchema = z.object({
86 | /** The error code, corresponding to BaseErrorCode enum values. */
87 | code: z.nativeEnum(BaseErrorCode).describe("Standardized error code"),
88 | /** A human-readable description of the error. */
89 | message: z.string().describe("Detailed error message"),
90 | /** Optional additional details or context about the error. */
91 | details: z.record(z.unknown()).optional().describe("Optional structured error details")
92 | }).describe(
93 | "Schema for validating structured error objects."
94 | );
95 |
96 | /**
97 | * TypeScript type inferred from `ErrorSchema`.
98 | * Represents a validated error object structure.
99 | * @typedef {z.infer<typeof ErrorSchema>} ErrorResponse
100 | */
101 | export type ErrorResponse = z.infer<typeof ErrorSchema>;
102 |
```
--------------------------------------------------------------------------------
/src/utils/internal/requestContext.ts:
--------------------------------------------------------------------------------
```typescript
1 | /**
2 | * @fileoverview Utilities for creating and managing request contexts.
3 | * A request context is an object carrying a unique ID, timestamp, and other
4 | * relevant data for logging, tracing, and processing. It also defines
5 | * configuration and operational context structures.
6 | * @module src/utils/internal/requestContext
7 | */
8 |
9 | import { generateUUID } from "../index.js";
10 | import { logger } from "./logger.js";
11 |
12 | /**
13 | * Defines the core structure for context information associated with a request or operation.
14 | * This is fundamental for logging, tracing, and passing operational data.
15 | */
16 | export interface RequestContext {
17 | /**
18 | * Unique ID for the context instance.
19 | * Used for log correlation and request tracing.
20 | */
21 | requestId: string;
22 |
23 | /**
24 | * ISO 8601 timestamp indicating when the context was created.
25 | */
26 | timestamp: string;
27 |
28 | /**
29 | * Allows arbitrary key-value pairs for specific context needs.
30 | * Using `unknown` promotes type-safe access.
31 | * Consumers must type-check/assert when accessing extended properties.
32 | */
33 | [key: string]: unknown;
34 | }
35 |
36 | /**
37 | * Configuration for the {@link requestContextService}.
38 | * Allows for future extensibility of service-wide settings.
39 | */
40 | export interface ContextConfig {
41 | /** Custom configuration properties. Allows for arbitrary key-value pairs. */
42 | [key: string]: unknown;
43 | }
44 |
45 | /**
46 | * Represents a broader context for a specific operation or task.
47 | * It can optionally include a base {@link RequestContext} and other custom properties
48 | * relevant to the operation.
49 | */
50 | export interface OperationContext {
51 | /** Optional base request context data, adhering to the `RequestContext` structure. */
52 | requestContext?: RequestContext;
53 |
54 | /** Allows for additional, custom properties specific to the operation. */
55 | [key: string]: unknown;
56 | }
57 |
58 | /**
59 | * Singleton-like service object for managing request context operations.
60 | * @private
61 | */
62 | const requestContextServiceInstance = {
63 | /**
64 | * Internal configuration store for the service.
65 | */
66 | config: {} as ContextConfig,
67 |
68 | /**
69 | * Configures the request context service with new settings.
70 | * Merges the provided partial configuration with existing settings.
71 | *
72 | * @param config - A partial `ContextConfig` object containing settings to update or add.
73 | * @returns A shallow copy of the newly updated configuration.
74 | */
75 | configure(config: Partial<ContextConfig>): ContextConfig {
76 | this.config = {
77 | ...this.config,
78 | ...config,
79 | };
80 | const logContext = this.createRequestContext({
81 | operation: "RequestContextService.configure",
82 | newConfigState: { ...this.config },
83 | });
84 | logger.debug("RequestContextService configuration updated", logContext);
85 | return { ...this.config };
86 | },
87 |
88 | /**
89 | * Retrieves a shallow copy of the current service configuration.
90 | * This prevents direct mutation of the internal configuration state.
91 | *
92 | * @returns A shallow copy of the current `ContextConfig`.
93 | */
94 | getConfig(): ContextConfig {
95 | return { ...this.config };
96 | },
97 |
98 | /**
99 | * Creates a new {@link RequestContext} instance.
100 | * Each context is assigned a unique `requestId` (UUID) and a current `timestamp` (ISO 8601).
101 | * Additional custom properties can be merged into the context.
102 | *
103 | * @param additionalContext - An optional record of key-value pairs to be
104 | * included in the created request context.
105 | * @returns A new `RequestContext` object.
106 | */
107 | createRequestContext(
108 | additionalContext: Record<string, unknown> = {},
109 | ): RequestContext {
110 | const requestId = generateUUID();
111 | const timestamp = new Date().toISOString();
112 |
113 | const context: RequestContext = {
114 | requestId,
115 | timestamp,
116 | ...additionalContext,
117 | };
118 | return context;
119 | },
120 | };
121 |
122 | /**
123 | * Primary export for request context functionalities.
124 | * This service provides methods to create and manage {@link RequestContext} instances,
125 | * which are essential for logging, tracing, and correlating operations.
126 | */
127 | export const requestContextService = requestContextServiceInstance;
128 |
```
--------------------------------------------------------------------------------
/src/mcp-server/tools/updateFile/registration.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2 | import { BaseErrorCode, McpError } from '../../../types-global/errors.js';
3 | import { ErrorHandler } from '../../../utils/internal/errorHandler.js';
4 | import { logger } from '../../../utils/internal/logger.js';
5 | import { requestContextService } from '../../../utils/internal/requestContext.js';
6 | import { sanitization } from '../../../utils/security/sanitization.js';
7 | import {
8 | UpdateFileInputSchema,
9 | updateFileLogic
10 | } from './updateFileLogic.js';
11 |
12 | /**
13 | * Registers the 'update_file' tool with the MCP server.
14 | * This tool accepts a JSON object with 'path', 'blocks' (array of {search, replace}),
15 | * and optional 'useRegex' and 'replaceAll' flags.
16 | *
17 | * @param {McpServer} server - The McpServer instance to register the tool with.
18 | * @returns {Promise<void>} A promise that resolves when the tool is registered.
19 | * @throws {McpError} Throws an error if registration fails.
20 | */
21 | export const registerUpdateFileTool = async (server: McpServer): Promise<void> => {
22 | const registrationContext = requestContextService.createRequestContext({ operation: 'RegisterUpdateFileTool' });
23 | logger.info("Attempting to register 'update_file' tool with JSON input format", registrationContext);
24 |
25 | await ErrorHandler.tryCatch(
26 | async () => {
27 | server.tool(
28 | 'update_file', // Tool name
29 | 'Performs targeted search-and-replace operations within an existing file using an array of {search, replace} blocks. Preferred for smaller, localized changes. For large-scale updates or overwrites, consider using `write_file`. Accepts relative or absolute paths. File must exist. Supports optional `useRegex` (boolean, default false) and `replaceAll` (boolean, default false).', // Emphasized usage guidance
30 | UpdateFileInputSchema.shape, // Pass the updated schema shape
31 | async (params, extra) => {
32 | // Validate input using the Zod schema before proceeding
33 | const validationResult = UpdateFileInputSchema.safeParse(params);
34 | if (!validationResult.success) {
35 | // Create a new context for validation error
36 | const errorContext = requestContextService.createRequestContext({ operation: 'UpdateFileToolValidation' });
37 | logger.error('Invalid input parameters for update_file tool', { ...errorContext, errors: validationResult.error.errors });
38 | // Throw McpError for invalid parameters
39 | throw new McpError(BaseErrorCode.VALIDATION_ERROR, `Invalid parameters: ${validationResult.error.errors.map(e => `${e.path.join('.')} - ${e.message}`).join(', ')}`, errorContext);
40 | }
41 | const typedParams = validationResult.data; // Use validated data
42 |
43 | // Create a new context for this specific tool execution
44 | const callContext = requestContextService.createRequestContext({ operation: 'UpdateFileToolExecution' });
45 | logger.info(`Executing 'update_file' tool for path: ${typedParams.path} with ${typedParams.blocks.length} blocks`, callContext);
46 |
47 | // ErrorHandler will catch McpErrors thrown by the logic
48 | const result = await ErrorHandler.tryCatch(
49 | () => updateFileLogic(typedParams, callContext),
50 | {
51 | operation: 'updateFileLogic',
52 | context: callContext,
53 | // Sanitize input for logging: keep path, redact block content
54 | input: sanitization.sanitizeForLogging({
55 | path: typedParams.path,
56 | blocks: typedParams.blocks.map((_, index) => `[Block ${index + 1} REDACTED]`), // Redact block details
57 | useRegex: typedParams.useRegex,
58 | replaceAll: typedParams.replaceAll,
59 | }),
60 | errorCode: BaseErrorCode.INTERNAL_ERROR
61 | }
62 | );
63 |
64 | logger.info(`Successfully executed 'update_file' for path: ${result.updatedPath}. Blocks Applied: ${result.blocksApplied}, Failed: ${result.blocksFailed}`, callContext);
65 |
66 | // Format the successful response
67 | return {
68 | content: [{ type: 'text', text: result.message }],
69 | };
70 | }
71 | );
72 | logger.info("'update_file' tool registered successfully with JSON input format", registrationContext);
73 | },
74 | {
75 | operation: 'registerUpdateFileTool',
76 | context: registrationContext,
77 | errorCode: BaseErrorCode.CONFIGURATION_ERROR,
78 | critical: true
79 | }
80 | );
81 | };
82 |
```
--------------------------------------------------------------------------------
/src/mcp-server/tools/createDirectory/createDirectoryLogic.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 input schema using Zod for validation
9 | export const CreateDirectoryInputSchema = z.object({
10 | path: z.string().min(1, 'Path cannot be empty')
11 | .describe('The path to the directory to create. Can be relative or absolute.'),
12 | create_parents: z.boolean().default(true)
13 | .describe('If true, create any necessary parent directories that don\'t exist. If false, fail if a parent directory is missing.'),
14 | });
15 |
16 | // Define the TypeScript type for the input
17 | export type CreateDirectoryInput = z.infer<typeof CreateDirectoryInputSchema>;
18 |
19 | // Define the TypeScript type for the output
20 | export interface CreateDirectoryOutput {
21 | message: string;
22 | createdPath: string;
23 | parentsCreated: boolean; // Indicate if parent directories were also created
24 | }
25 |
26 | /**
27 | * Creates a specified directory, optionally creating parent directories.
28 | *
29 | * @param {CreateDirectoryInput} input - The input object containing path and create_parents flag.
30 | * @param {RequestContext} context - The request context.
31 | * @returns {Promise<CreateDirectoryOutput>} A promise resolving with the creation status.
32 | * @throws {McpError} For path errors, if the path already exists and is not a directory, or I/O errors.
33 | */
34 | export const createDirectoryLogic = async (input: CreateDirectoryInput, context: RequestContext): Promise<CreateDirectoryOutput> => {
35 | const { path: requestedPath, create_parents } = input;
36 | const logicContext = { ...context, tool: 'createDirectoryLogic', create_parents };
37 | logger.debug(`createDirectoryLogic: Received request to create directory "${requestedPath}"`, logicContext);
38 |
39 | // Resolve the path
40 | const absolutePath = serverState.resolvePath(requestedPath, context);
41 | logger.debug(`createDirectoryLogic: Resolved path to "${absolutePath}"`, { ...logicContext, requestedPath });
42 |
43 | try {
44 | // Check if path already exists
45 | try {
46 | const stats = await fs.stat(absolutePath);
47 | if (stats.isDirectory()) {
48 | logger.info(`createDirectoryLogic: Directory already exists at "${absolutePath}"`, { ...logicContext, requestedPath });
49 | // Directory already exists, consider this a success (idempotent)
50 | return {
51 | message: `Directory already exists: ${absolutePath}`,
52 | createdPath: absolutePath,
53 | parentsCreated: false, // No parents needed to be created now
54 | };
55 | } else {
56 | // Path exists but is not a directory (e.g., a file)
57 | logger.error(`createDirectoryLogic: Path exists but is not a directory "${absolutePath}"`, { ...logicContext, requestedPath });
58 | throw new McpError(BaseErrorCode.VALIDATION_ERROR, `Path already exists but is not a directory: ${absolutePath}`, { ...logicContext, requestedPath, resolvedPath: absolutePath });
59 | }
60 | } catch (statError: any) {
61 | if (statError.code !== 'ENOENT') {
62 | // If error is something other than "Not Found", re-throw it
63 | throw statError;
64 | }
65 | // Path does not exist, proceed with creation
66 | logger.debug(`createDirectoryLogic: Path does not exist, proceeding with creation "${absolutePath}"`, { ...logicContext, requestedPath });
67 | }
68 |
69 | // Attempt to create the directory
70 | await fs.mkdir(absolutePath, { recursive: create_parents });
71 | logger.info(`createDirectoryLogic: Successfully created directory "${absolutePath}" (parents: ${create_parents})`, { ...logicContext, requestedPath });
72 |
73 | return {
74 | message: `Successfully created directory: ${absolutePath}${create_parents ? ' (including parents if needed)' : ''}`,
75 | createdPath: absolutePath,
76 | parentsCreated: create_parents, // Reflects the *option* enabled, not necessarily if they *were* created
77 | };
78 |
79 | } catch (error: any) {
80 | logger.error(`createDirectoryLogic: Error creating directory "${absolutePath}"`, { ...logicContext, requestedPath, error: error.message, code: error.code });
81 |
82 | if (error instanceof McpError) {
83 | throw error; // Re-throw known McpErrors
84 | }
85 |
86 | // Handle potential I/O errors (e.g., permissions, invalid path components)
87 | throw new McpError(BaseErrorCode.INTERNAL_ERROR, `Failed to create directory: ${error.message || 'Unknown I/O error'}`, { ...logicContext, requestedPath, resolvedPath: absolutePath, originalError: error });
88 | }
89 | };
90 |
```
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
```markdown
1 | # Changelog
2 |
3 | All notable changes to this project will be documented in this file.
4 |
5 | ## [1.0.4] - 2025-05-23
6 |
7 | ### Changed
8 | - **Configuration**: `FS_BASE_DIRECTORY` can now be set as a path relative to the project root (e.g., `./data_sandbox`) in addition to an absolute path. The server will resolve relative paths to an absolute path. If the directory doesn't exist, it will be created. This feature remains optional, with a warning logged if `FS_BASE_DIRECTORY` is not set.
9 | - Updated `src/config/index.ts` to handle relative path resolution for `FS_BASE_DIRECTORY`.
10 | - Updated `README.md` to reflect this new flexibility.
11 |
12 | ## [1.0.3] - 2025-05-23
13 |
14 | ### Added
15 |
16 | - **Filesystem Access Control**: Introduced `FS_BASE_DIRECTORY` environment variable. If set to an absolute path, all filesystem tool operations are restricted to this directory and its subdirectories, enhancing security by preventing unintended access to other parts of the filesystem.
17 | - Configuration (`src/config/index.ts`) updated to include `FS_BASE_DIRECTORY`, with validation ensuring it's an absolute path if provided.
18 | - Server state (`src/mcp-server/state.ts`) now initializes and enforces this base directory. The `resolvePath` method checks if the resolved path is within the `FS_BASE_DIRECTORY` and throws a `FORBIDDEN` error if it's outside.
19 | - Dockerfile updated to include `FS_BASE_DIRECTORY` as a build argument and environment variable.
20 | - `mcp.json` and `smithery.yaml` updated to include `FS_BASE_DIRECTORY`.
21 |
22 | ### Changed
23 |
24 | - **Version Bump**: Project version updated to `1.0.3` in `package.json`, `README.md`.
25 | - **Documentation**:
26 | - `README.md` updated to document the new `FS_BASE_DIRECTORY` feature.
27 | - `docs/tree.md` updated to reflect the inclusion of `CHANGELOG.md` (though this was part of the diff, it's a documentation update).
28 |
29 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
30 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
31 |
32 | ## [1.0.2] - 2025-05-23
33 |
34 | ### Added
35 |
36 | - **HTTP Transport Support**: Introduced an HTTP transport layer (`src/mcp-server/transports/httpTransport.ts`) alongside the existing STDIO transport. This allows the server to be accessed over the network.
37 | - Includes JWT-based authentication (`src/mcp-server/transports/authentication/authMiddleware.ts`) for secure HTTP communication.
38 | - Supports RESTful endpoints and Server-Sent Events (SSE) for streaming.
39 | - **Enhanced Configuration System**: Major overhaul of `src/config/index.ts` using Zod for robust validation of environment variables.
40 | - Added new configuration options for HTTP transport (port, host, allowed origins, auth secret key).
41 | - Added new configuration options for LLM integrations (OpenRouter, Gemini API keys, default model parameters).
42 | - Added new configuration options for OAuth Proxy integration.
43 | - **New Dependencies**: Added `express`, `jsonwebtoken`, `chrono-node`, `openai`, `partial-json`, `tiktoken` to support new features.
44 | - **Untracked Files Added**: `mcp.json` and `smithery.yaml` are now part of the project.
45 |
46 | ### Changed
47 |
48 | - **Utils Refactoring**: Major refactoring of the `src/utils/` directory. Utilities are now organized into subdirectories:
49 | - `src/utils/internal/` (errorHandler, logger, requestContext)
50 | - `src/utils/security/` (idGenerator, rateLimiter, sanitization)
51 | - `src/utils/metrics/` (tokenCounter)
52 | - `src/utils/parsing/` (dateParser, jsonParser)
53 | - **Project Version**: Bumped version in `package.json` and `package-lock.json` to `1.0.1`.
54 | - **Documentation**:
55 | - Updated `README.md` to reflect new features, architecture changes (transports, utils structure), and new configuration options.
56 | - Updated `.clinerules` (developer cheatsheet) with the new project structure and utility usage.
57 | - Updated `docs/tree.md` to reflect the new directory structure.
58 | - **Tool Registration**: Minor updates in tool registration files to align with refactored utility paths and error handling.
59 | - **Server Initialization**: Modified `src/index.ts` and `src/mcp-server/server.ts` to accommodate the new transport layer and configuration system.
60 |
61 | ### Removed
62 |
63 | - Old top-level utility files from `src/utils/` (e.g., `errorHandler.ts`, `logger.ts`) have been moved into the new categorized subdirectories.
64 |
65 | ## [1.0.0] - Initial Release Date
66 |
67 | - Initial release of the Filesystem MCP Server.
68 | - Core filesystem tools: `set_filesystem_default`, `read_file`, `write_file`, `update_file`, `list_files`, `delete_file`, `delete_directory`, `create_directory`, `move_path`, `copy_path`.
69 | - STDIO transport.
70 | - Basic logging, error handling, and sanitization utilities.
71 |
```
--------------------------------------------------------------------------------
/src/mcp-server/tools/writeFile/writeFileLogic.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'; // Import serverState for path resolution
8 |
9 | // Define the input schema using Zod for validation
10 | export const WriteFileInputSchema = z.object({
11 | path: z.string().min(1, 'Path cannot be empty')
12 | .describe('The path to the file to write. Can be relative or absolute. If relative, it resolves against the path set by `set_filesystem_default`. If absolute, it is used directly. Missing directories will be created.'),
13 | content: z.string() // Allow empty content
14 | .describe('The content to write to the file. If the file exists, it will be overwritten.'),
15 | });
16 |
17 | // Define the TypeScript type for the input
18 | export type WriteFileInput = z.infer<typeof WriteFileInputSchema>;
19 |
20 | // Define the TypeScript type for the output (simple success message)
21 | export interface WriteFileOutput {
22 | message: string;
23 | writtenPath: string;
24 | bytesWritten: number;
25 | }
26 |
27 | /**
28 | * Writes content to a specified file, overwriting it if it exists,
29 | * and creating necessary directories.
30 | *
31 | * @param {WriteFileInput} input - The input object containing the file path and content.
32 | * @param {RequestContext} context - The request context for logging and error handling.
33 | * @returns {Promise<WriteFileOutput>} A promise that resolves with a success message, the path written to, and bytes written.
34 | * @throws {McpError} Throws McpError for path resolution errors, I/O errors, or if the path resolves to a directory.
35 | */
36 | export const writeFileLogic = async (input: WriteFileInput, context: RequestContext): Promise<WriteFileOutput> => {
37 | const { path: requestedPath, content } = input;
38 | logger.debug(`writeFileLogic: Received request for path "${requestedPath}"`, context);
39 |
40 | // Resolve the path using serverState (handles relative/absolute logic and sanitization)
41 | const absolutePath = serverState.resolvePath(requestedPath, context);
42 | logger.debug(`writeFileLogic: Resolved path to "${absolutePath}"`, { ...context, requestedPath });
43 |
44 | try {
45 | // Ensure the target path is not a directory before attempting to write
46 | try {
47 | const stats = await fs.stat(absolutePath);
48 | if (stats.isDirectory()) {
49 | logger.warning(`writeFileLogic: Attempted to write to a directory path "${absolutePath}"`, { ...context, requestedPath });
50 | throw new McpError(BaseErrorCode.VALIDATION_ERROR, `Cannot write file. Path exists and is a directory: ${absolutePath}`, { ...context, requestedPath, resolvedPath: absolutePath });
51 | }
52 | } catch (statError: any) {
53 | // ENOENT (file/dir doesn't exist) is expected and okay, we'll create it.
54 | // Other errors during stat (like permission issues) should be thrown.
55 | if (statError.code !== 'ENOENT') {
56 | throw statError; // Re-throw other stat errors
57 | }
58 | // If ENOENT, proceed to create directory and file
59 | logger.debug(`writeFileLogic: Path "${absolutePath}" does not exist, will create.`, { ...context, requestedPath });
60 | }
61 |
62 | // Ensure the directory exists before writing the file
63 | const dirName = path.dirname(absolutePath);
64 | logger.debug(`writeFileLogic: Ensuring directory "${dirName}" exists`, { ...context, requestedPath, resolvedPath: absolutePath });
65 | await fs.mkdir(dirName, { recursive: true });
66 | logger.debug(`writeFileLogic: Directory "${dirName}" confirmed/created`, { ...context, requestedPath, resolvedPath: absolutePath });
67 |
68 | // Write the file content
69 | logger.debug(`writeFileLogic: Writing content to "${absolutePath}"`, { ...context, requestedPath });
70 | await fs.writeFile(absolutePath, content, 'utf8');
71 | const bytesWritten = Buffer.byteLength(content, 'utf8');
72 | logger.info(`writeFileLogic: Successfully wrote ${bytesWritten} bytes to "${absolutePath}"`, { ...context, requestedPath });
73 |
74 | return {
75 | message: `Successfully wrote content to ${absolutePath}`,
76 | writtenPath: absolutePath,
77 | bytesWritten: bytesWritten,
78 | };
79 | } catch (error: any) {
80 | logger.error(`writeFileLogic: Error writing file to "${absolutePath}"`, { ...context, requestedPath, error: error.message, code: error.code });
81 | // Handle specific file system errors
82 | if (error instanceof McpError) {
83 | throw error; // Re-throw McpErrors (like the directory check)
84 | }
85 | // Handle potential I/O errors during mkdir or writeFile
86 | throw new McpError(BaseErrorCode.INTERNAL_ERROR, `Failed to write file: ${error.message || 'Unknown I/O error'}`, { ...context, requestedPath, resolvedPath: absolutePath, originalError: error });
87 | }
88 | };
89 |
```
--------------------------------------------------------------------------------
/src/utils/parsing/dateParser.ts:
--------------------------------------------------------------------------------
```typescript
1 | /**
2 | * @fileoverview Provides utility functions for parsing natural language date strings
3 | * into Date objects or detailed parsing results using the `chrono-node` library.
4 | * @module src/utils/parsing/dateParser
5 | */
6 | import * as chrono from "chrono-node";
7 | import { BaseErrorCode, McpError } from "../../types-global/errors.js";
8 | import { ErrorHandler, logger, RequestContext } from "../index.js";
9 |
10 | /**
11 | * Parses a natural language date string into a JavaScript Date object.
12 | * Uses `chrono.parseDate` for lenient parsing of various date formats.
13 | *
14 | * @param text - The natural language date string to parse.
15 | * @param context - The request context for logging and error tracking.
16 | * @param refDate - Optional reference date for parsing relative dates. Defaults to current date/time.
17 | * @returns A promise resolving with a Date object or `null` if parsing fails.
18 | * @throws {McpError} If an unexpected error occurs during parsing.
19 | * @private
20 | */
21 | async function parseDateString(
22 | text: string,
23 | context: RequestContext,
24 | refDate?: Date,
25 | ): Promise<Date | null> {
26 | const operation = "parseDateString";
27 | const logContext = { ...context, operation, inputText: text, refDate };
28 | logger.debug(`Attempting to parse date string: "${text}"`, logContext);
29 |
30 | return await ErrorHandler.tryCatch(
31 | async () => {
32 | const parsedDate = chrono.parseDate(text, refDate, { forwardDate: true });
33 | if (parsedDate) {
34 | logger.debug(
35 | `Successfully parsed "${text}" to ${parsedDate.toISOString()}`,
36 | logContext,
37 | );
38 | return parsedDate;
39 | } else {
40 | logger.warning(`Failed to parse date string: "${text}"`, logContext);
41 | return null;
42 | }
43 | },
44 | {
45 | operation,
46 | context: logContext,
47 | input: { text, refDate },
48 | errorCode: BaseErrorCode.VALIDATION_ERROR,
49 | },
50 | );
51 | }
52 |
53 | /**
54 | * Parses a natural language date string and returns detailed parsing results.
55 | * Provides more information than just the Date object, including matched text and components.
56 | *
57 | * @param text - The natural language date string to parse.
58 | * @param context - The request context for logging and error tracking.
59 | * @param refDate - Optional reference date for parsing relative dates. Defaults to current date/time.
60 | * @returns A promise resolving with an array of `chrono.ParsedResult` objects. Empty if no dates found.
61 | * @throws {McpError} If an unexpected error occurs during parsing.
62 | * @private
63 | */
64 | async function parseDateStringDetailed(
65 | text: string,
66 | context: RequestContext,
67 | refDate?: Date,
68 | ): Promise<chrono.ParsedResult[]> {
69 | const operation = "parseDateStringDetailed";
70 | const logContext = { ...context, operation, inputText: text, refDate };
71 | logger.debug(
72 | `Attempting detailed parse of date string: "${text}"`,
73 | logContext,
74 | );
75 |
76 | return await ErrorHandler.tryCatch(
77 | async () => {
78 | const results = chrono.parse(text, refDate, { forwardDate: true });
79 | logger.debug(
80 | `Detailed parse of "${text}" resulted in ${results.length} result(s)`,
81 | logContext,
82 | );
83 | return results;
84 | },
85 | {
86 | operation,
87 | context: logContext,
88 | input: { text, refDate },
89 | errorCode: BaseErrorCode.VALIDATION_ERROR,
90 | },
91 | );
92 | }
93 |
94 | /**
95 | * An object providing date parsing functionalities.
96 | *
97 | * @example
98 | * ```typescript
99 | * import { dateParser, requestContextService } from './utils'; // Assuming utils/index.js exports these
100 | * const context = requestContextService.createRequestContext({ operation: 'TestDateParsing' });
101 | *
102 | * async function testParsing() {
103 | * const dateObj = await dateParser.parseDate("next Friday at 3pm", context);
104 | * if (dateObj) {
105 | * console.log("Parsed Date:", dateObj.toISOString());
106 | * }
107 | *
108 | * const detailedResults = await dateParser.parse("Meeting on 2024-12-25 and another one tomorrow", context);
109 | * detailedResults.forEach(result => {
110 | * console.log("Detailed Result:", result.text, result.start.date());
111 | * });
112 | * }
113 | * testParsing();
114 | * ```
115 | */
116 | export const dateParser = {
117 | /**
118 | * Parses a natural language date string and returns detailed parsing results
119 | * from `chrono-node`.
120 | * @param text - The natural language date string to parse.
121 | * @param context - The request context for logging and error tracking.
122 | * @param refDate - Optional reference date for parsing relative dates.
123 | * @returns A promise resolving with an array of `chrono.ParsedResult` objects.
124 | */
125 | parse: parseDateStringDetailed,
126 | /**
127 | * Parses a natural language date string into a single JavaScript Date object.
128 | * @param text - The natural language date string to parse.
129 | * @param context - The request context for logging and error tracking.
130 | * @param refDate - Optional reference date for parsing relative dates.
131 | * @returns A promise resolving with a Date object or `null`.
132 | */
133 | parseDate: parseDateString,
134 | };
135 |
```
--------------------------------------------------------------------------------
/src/utils/metrics/tokenCounter.ts:
--------------------------------------------------------------------------------
```typescript
1 | /**
2 | * @fileoverview Provides utility functions for counting tokens in text and chat messages
3 | * using the `tiktoken` library, specifically configured for 'gpt-4o' tokenization.
4 | * These functions are essential for managing token limits and estimating costs
5 | * when interacting with language models.
6 | * @module src/utils/metrics/tokenCounter
7 | */
8 | import { ChatCompletionMessageParam } from "openai/resources/chat/completions";
9 | import { encoding_for_model, Tiktoken, TiktokenModel } from "tiktoken";
10 | import { BaseErrorCode, McpError } from "../../types-global/errors.js";
11 | import { ErrorHandler, logger, RequestContext } from "../index.js";
12 |
13 | /**
14 | * The specific Tiktoken model used for all tokenization operations in this module.
15 | * This ensures consistent token counting.
16 | * @private
17 | */
18 | const TOKENIZATION_MODEL: TiktokenModel = "gpt-4o";
19 |
20 | /**
21 | * Calculates the number of tokens for a given text string using the
22 | * tokenizer specified by `TOKENIZATION_MODEL`.
23 | * Wraps tokenization in `ErrorHandler.tryCatch` for robust error management.
24 | *
25 | * @param text - The input text to tokenize.
26 | * @param context - Optional request context for logging and error handling.
27 | * @returns A promise that resolves with the number of tokens in the text.
28 | * @throws {McpError} If tokenization fails.
29 | */
30 | export async function countTokens(
31 | text: string,
32 | context?: RequestContext,
33 | ): Promise<number> {
34 | return ErrorHandler.tryCatch(
35 | () => {
36 | let encoding: Tiktoken | null = null;
37 | try {
38 | encoding = encoding_for_model(TOKENIZATION_MODEL);
39 | const tokens = encoding.encode(text);
40 | return tokens.length;
41 | } finally {
42 | encoding?.free();
43 | }
44 | },
45 | {
46 | operation: "countTokens",
47 | context: context,
48 | input: { textSample: text.substring(0, 50) + "..." },
49 | errorCode: BaseErrorCode.INTERNAL_ERROR,
50 | },
51 | );
52 | }
53 |
54 | /**
55 | * Calculates the estimated number of tokens for an array of chat messages.
56 | * Uses the tokenizer specified by `TOKENIZATION_MODEL` and accounts for
57 | * special tokens and message overhead according to OpenAI's guidelines.
58 | *
59 | * For multi-part content, only text parts are currently tokenized.
60 | *
61 | * Reference: {@link https://github.com/openai/openai-cookbook/blob/main/examples/How_to_count_tokens_with_tiktoken.ipynb}
62 | *
63 | * @param messages - An array of chat messages.
64 | * @param context - Optional request context for logging and error handling.
65 | * @returns A promise that resolves with the estimated total number of tokens.
66 | * @throws {McpError} If tokenization fails.
67 | */
68 | export async function countChatTokens(
69 | messages: ReadonlyArray<ChatCompletionMessageParam>,
70 | context?: RequestContext,
71 | ): Promise<number> {
72 | return ErrorHandler.tryCatch(
73 | () => {
74 | let encoding: Tiktoken | null = null;
75 | let num_tokens = 0;
76 | try {
77 | encoding = encoding_for_model(TOKENIZATION_MODEL);
78 |
79 | const tokens_per_message = 3; // For gpt-4o, gpt-4, gpt-3.5-turbo
80 | const tokens_per_name = 1; // For gpt-4o, gpt-4, gpt-3.5-turbo
81 |
82 | for (const message of messages) {
83 | num_tokens += tokens_per_message;
84 | num_tokens += encoding.encode(message.role).length;
85 |
86 | if (typeof message.content === "string") {
87 | num_tokens += encoding.encode(message.content).length;
88 | } else if (Array.isArray(message.content)) {
89 | for (const part of message.content) {
90 | if (part.type === "text") {
91 | num_tokens += encoding.encode(part.text).length;
92 | } else {
93 | logger.warning(
94 | `Non-text content part found (type: ${part.type}), token count contribution ignored.`,
95 | context,
96 | );
97 | }
98 | }
99 | }
100 |
101 | if ("name" in message && message.name) {
102 | num_tokens += tokens_per_name;
103 | num_tokens += encoding.encode(message.name).length;
104 | }
105 |
106 | if (
107 | message.role === "assistant" &&
108 | "tool_calls" in message &&
109 | message.tool_calls
110 | ) {
111 | for (const tool_call of message.tool_calls) {
112 | if (tool_call.function.name) {
113 | num_tokens += encoding.encode(tool_call.function.name).length;
114 | }
115 | if (tool_call.function.arguments) {
116 | num_tokens += encoding.encode(
117 | tool_call.function.arguments,
118 | ).length;
119 | }
120 | }
121 | }
122 |
123 | if (
124 | message.role === "tool" &&
125 | "tool_call_id" in message &&
126 | message.tool_call_id
127 | ) {
128 | num_tokens += encoding.encode(message.tool_call_id).length;
129 | }
130 | }
131 | num_tokens += 3; // Every reply is primed with <|start|>assistant<|message|>
132 | return num_tokens;
133 | } finally {
134 | encoding?.free();
135 | }
136 | },
137 | {
138 | operation: "countChatTokens",
139 | context: context,
140 | input: { messageCount: messages.length },
141 | errorCode: BaseErrorCode.INTERNAL_ERROR,
142 | },
143 | );
144 | }
145 |
```
--------------------------------------------------------------------------------
/src/mcp-server/tools/deleteDirectory/deleteDirectoryLogic.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 input schema using Zod for validation
9 | export const DeleteDirectoryInputSchema = z.object({
10 | path: z.string().min(1, 'Path cannot be empty')
11 | .describe('The path to the directory to delete. Can be relative or absolute.'),
12 | recursive: z.boolean().default(false)
13 | .describe('If true, delete the directory and all its contents. If false, only delete if the directory is empty.'),
14 | });
15 |
16 | // Define the TypeScript type for the input
17 | export type DeleteDirectoryInput = z.infer<typeof DeleteDirectoryInputSchema>;
18 |
19 | // Define the TypeScript type for the output
20 | export interface DeleteDirectoryOutput {
21 | message: string;
22 | deletedPath: string;
23 | wasRecursive: boolean;
24 | }
25 |
26 | /**
27 | * Deletes a specified directory, optionally recursively.
28 | *
29 | * @param {DeleteDirectoryInput} input - The input object containing path and recursive flag.
30 | * @param {RequestContext} context - The request context.
31 | * @returns {Promise<DeleteDirectoryOutput>} A promise resolving with the deletion status.
32 | * @throws {McpError} For path errors, directory not found, not a directory, directory not empty (if not recursive), or I/O errors.
33 | */
34 | export const deleteDirectoryLogic = async (input: DeleteDirectoryInput, context: RequestContext): Promise<DeleteDirectoryOutput> => {
35 | const { path: requestedPath, recursive } = input;
36 | const logicContext = { ...context, tool: 'deleteDirectoryLogic', recursive };
37 | logger.debug(`deleteDirectoryLogic: Received request to delete directory "${requestedPath}"`, logicContext);
38 |
39 | // Resolve the path
40 | const absolutePath = serverState.resolvePath(requestedPath, context);
41 | logger.debug(`deleteDirectoryLogic: Resolved path to "${absolutePath}"`, { ...logicContext, requestedPath });
42 |
43 | try {
44 | // Check if the path exists and is a directory before attempting deletion
45 | let stats;
46 | try {
47 | stats = await fs.stat(absolutePath);
48 | } catch (statError: any) {
49 | if (statError.code === 'ENOENT') {
50 | logger.warning(`deleteDirectoryLogic: Directory not found at "${absolutePath}"`, { ...logicContext, requestedPath });
51 | throw new McpError(BaseErrorCode.NOT_FOUND, `Directory not found at path: ${absolutePath}`, { ...logicContext, requestedPath, resolvedPath: absolutePath, originalError: statError });
52 | }
53 | throw statError; // Re-throw other stat errors
54 | }
55 |
56 | if (!stats.isDirectory()) {
57 | logger.warning(`deleteDirectoryLogic: Path is not a directory "${absolutePath}"`, { ...logicContext, requestedPath });
58 | throw new McpError(BaseErrorCode.VALIDATION_ERROR, `Path is not a directory: ${absolutePath}`, { ...logicContext, requestedPath, resolvedPath: absolutePath });
59 | }
60 |
61 | // Attempt to delete the directory
62 | if (recursive) {
63 | // Use fs.rm for recursive deletion (available in Node.js 14.14.0+)
64 | await fs.rm(absolutePath, { recursive: true, force: true }); // force helps with potential permission issues on subfiles sometimes
65 | logger.info(`deleteDirectoryLogic: Successfully deleted directory recursively "${absolutePath}"`, { ...logicContext, requestedPath });
66 | } else {
67 | // Use fs.rmdir for non-recursive deletion (fails if not empty)
68 | await fs.rmdir(absolutePath);
69 | logger.info(`deleteDirectoryLogic: Successfully deleted empty directory "${absolutePath}"`, { ...logicContext, requestedPath });
70 | }
71 |
72 | return {
73 | message: `Successfully deleted directory: ${absolutePath}${recursive ? ' (recursively)' : ' (empty)'}`,
74 | deletedPath: absolutePath,
75 | wasRecursive: recursive,
76 | };
77 |
78 | } catch (error: any) {
79 | logger.error(`deleteDirectoryLogic: Error deleting directory "${absolutePath}"`, { ...logicContext, requestedPath, error: error.message, code: error.code });
80 |
81 | if (error instanceof McpError) {
82 | throw error; // Re-throw known McpErrors
83 | }
84 |
85 | if (error.code === 'ENOENT') {
86 | // Should have been caught by stat, but handle defensively
87 | logger.warning(`deleteDirectoryLogic: Directory not found during delete operation "${absolutePath}"`, { ...logicContext, requestedPath });
88 | throw new McpError(BaseErrorCode.NOT_FOUND, `Directory not found at path: ${absolutePath}`, { ...logicContext, requestedPath, resolvedPath: absolutePath, originalError: error });
89 | }
90 |
91 | if (error.code === 'ENOTEMPTY' && !recursive) {
92 | logger.warning(`deleteDirectoryLogic: Directory not empty and recursive=false "${absolutePath}"`, { ...logicContext, requestedPath });
93 | throw new McpError(BaseErrorCode.VALIDATION_ERROR, `Directory not empty: ${absolutePath}. Use recursive=true to delete non-empty directories.`, { ...logicContext, requestedPath, resolvedPath: absolutePath, originalError: error });
94 | }
95 |
96 | // Handle other potential I/O errors (e.g., permissions)
97 | throw new McpError(BaseErrorCode.INTERNAL_ERROR, `Failed to delete directory: ${error.message || 'Unknown I/O error'}`, { ...logicContext, requestedPath, resolvedPath: absolutePath, originalError: error });
98 | }
99 | };
100 |
```
--------------------------------------------------------------------------------
/src/utils/parsing/jsonParser.ts:
--------------------------------------------------------------------------------
```typescript
1 | /**
2 | * @fileoverview Provides a utility class for parsing potentially partial JSON strings.
3 | * It wraps the 'partial-json' npm library and includes functionality to handle
4 | * optional <think>...</think> blocks often found at the beginning of LLM outputs.
5 | * @module src/utils/parsing/jsonParser
6 | */
7 | import {
8 | parse as parsePartialJson,
9 | Allow as PartialJsonAllow,
10 | } from "partial-json";
11 | import { BaseErrorCode, McpError } from "../../types-global/errors.js";
12 | import { logger, RequestContext, requestContextService } from "../index.js";
13 |
14 | /**
15 | * Enum mirroring `partial-json`'s `Allow` constants. These specify
16 | * what types of partial JSON structures are permissible during parsing.
17 | * They can be combined using bitwise OR (e.g., `Allow.STR | Allow.OBJ`).
18 | *
19 | * The available properties are:
20 | * - `STR`: Allow partial string.
21 | * - `NUM`: Allow partial number.
22 | * - `ARR`: Allow partial array.
23 | * - `OBJ`: Allow partial object.
24 | * - `NULL`: Allow partial null.
25 | * - `BOOL`: Allow partial boolean.
26 | * - `NAN`: Allow partial NaN. (Note: Standard JSON does not support NaN)
27 | * - `INFINITY`: Allow partial Infinity. (Note: Standard JSON does not support Infinity)
28 | * - `_INFINITY`: Allow partial -Infinity. (Note: Standard JSON does not support -Infinity)
29 | * - `INF`: Allow both partial Infinity and -Infinity.
30 | * - `SPECIAL`: Allow all special values (NaN, Infinity, -Infinity).
31 | * - `ATOM`: Allow all atomic values (strings, numbers, booleans, null, special values).
32 | * - `COLLECTION`: Allow all collection values (objects, arrays).
33 | * - `ALL`: Allow all value types to be partial (default for `partial-json`'s parse).
34 | * @see {@link https://github.com/promplate/partial-json-parser-js} for more details.
35 | */
36 | export const Allow = PartialJsonAllow;
37 |
38 | /**
39 | * Regular expression to find a <think> block at the start of a string.
40 | * Captures content within <think>...</think> (Group 1) and the rest of the string (Group 2).
41 | * @private
42 | */
43 | const thinkBlockRegex = /^<think>([\s\S]*?)<\/think>\s*([\s\S]*)$/;
44 |
45 | /**
46 | * Utility class for parsing potentially partial JSON strings.
47 | * Wraps the 'partial-json' library for robust JSON parsing, handling
48 | * incomplete structures and optional <think> blocks from LLMs.
49 | */
50 | export class JsonParser {
51 | /**
52 | * Parses a JSON string, which may be partial or prefixed with a <think> block.
53 | * If a <think> block is present, its content is logged, and parsing proceeds on the
54 | * remainder. Uses 'partial-json' to handle incomplete JSON.
55 | *
56 | * @template T The expected type of the parsed JSON object. Defaults to `any`.
57 | * @param jsonString - The JSON string to parse.
58 | * @param allowPartial - Bitwise OR combination of `Allow` constants specifying permissible
59 | * partial JSON types. Defaults to `Allow.ALL`.
60 | * @param context - Optional `RequestContext` for logging and error correlation.
61 | * @returns The parsed JavaScript value.
62 | * @throws {McpError} If the string is empty after processing or if `partial-json` fails.
63 | */
64 | parse<T = any>(
65 | jsonString: string,
66 | allowPartial: number = Allow.ALL,
67 | context?: RequestContext,
68 | ): T {
69 | let stringToParse = jsonString;
70 | const match = jsonString.match(thinkBlockRegex);
71 |
72 | if (match) {
73 | const thinkContent = match[1].trim();
74 | const restOfString = match[2];
75 |
76 | const logContext =
77 | context ||
78 | requestContextService.createRequestContext({
79 | operation: "JsonParser.thinkBlock",
80 | });
81 | if (thinkContent) {
82 | logger.debug("LLM <think> block detected and logged.", {
83 | ...logContext,
84 | thinkContent,
85 | });
86 | } else {
87 | logger.debug("Empty LLM <think> block detected.", logContext);
88 | }
89 | stringToParse = restOfString;
90 | }
91 |
92 | stringToParse = stringToParse.trim();
93 |
94 | if (!stringToParse) {
95 | throw new McpError(
96 | BaseErrorCode.VALIDATION_ERROR,
97 | "JSON string is empty after removing <think> block and trimming.",
98 | context,
99 | );
100 | }
101 |
102 | try {
103 | return parsePartialJson(stringToParse, allowPartial) as T;
104 | } catch (error: any) {
105 | const errorLogContext =
106 | context ||
107 | requestContextService.createRequestContext({
108 | operation: "JsonParser.parseError",
109 | });
110 | logger.error("Failed to parse JSON content.", {
111 | ...errorLogContext,
112 | errorDetails: error.message,
113 | contentAttempted: stringToParse.substring(0, 200),
114 | });
115 |
116 | throw new McpError(
117 | BaseErrorCode.VALIDATION_ERROR,
118 | `Failed to parse JSON: ${error.message}`,
119 | {
120 | ...context,
121 | originalContentSample:
122 | stringToParse.substring(0, 200) +
123 | (stringToParse.length > 200 ? "..." : ""),
124 | rawError: error instanceof Error ? error.stack : String(error),
125 | },
126 | );
127 | }
128 | }
129 | }
130 |
131 | /**
132 | * Singleton instance of the `JsonParser`.
133 | * Use this instance to parse JSON strings, with support for partial JSON and <think> blocks.
134 | * @example
135 | * ```typescript
136 | * import { jsonParser, Allow, requestContextService } from './utils';
137 | * const context = requestContextService.createRequestContext({ operation: 'TestJsonParsing' });
138 | *
139 | * const fullJson = '{"key": "value"}';
140 | * const parsedFull = jsonParser.parse(fullJson, Allow.ALL, context);
141 | * console.log(parsedFull); // Output: { key: 'value' }
142 | *
143 | * const partialObject = '<think>This is a thought.</think>{"key": "value", "arr": [1,';
144 | * try {
145 | * const parsedPartial = jsonParser.parse(partialObject, undefined, context);
146 | * console.log(parsedPartial);
147 | * } catch (e) {
148 | * console.error("Parsing partial object failed:", e);
149 | * }
150 | * ```
151 | */
152 | export const jsonParser = new JsonParser();
153 |
```
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
```typescript
1 | #!/usr/bin/env node
2 | import { config, environment } from "./config/index.js";
3 | import { initializeAndStartServer } from "./mcp-server/server.js"; // Changed import
4 | import { BaseErrorCode } from "./types-global/errors.js";
5 | import { ErrorHandler } from "./utils/internal/errorHandler.js";
6 | import { logger } from "./utils/internal/logger.js";
7 | // Import the service instance instead of the standalone function
8 | import { requestContextService } from "./utils/internal/requestContext.js";
9 |
10 | // Define a type alias for the server instance for better readability
11 | // initializeAndStartServer returns Promise<void | McpServer>, we'll handle the potential void/undefined later if needed
12 | // For now, assuming it returns McpServer as per current stdio-only logic
13 | type McpServerInstance = Awaited<ReturnType<typeof initializeAndStartServer>>;
14 |
15 | /**
16 | * The main MCP server instance.
17 | * @type {McpServerInstance | undefined}
18 | */
19 | let server: McpServerInstance | undefined;
20 |
21 | /**
22 | * Gracefully shuts down the main MCP server.
23 | * Handles process termination signals (SIGTERM, SIGINT) and critical errors.
24 | *
25 | * @param signal - The signal or event name that triggered the shutdown (e.g., "SIGTERM", "uncaughtException").
26 | */
27 | const shutdown = async (signal: string) => {
28 | // Define context for the shutdown operation
29 | const shutdownContext = requestContextService.createRequestContext({
30 | operation: 'Shutdown',
31 | signal,
32 | });
33 |
34 | logger.info(`Received ${signal}. Starting graceful shutdown...`, shutdownContext);
35 |
36 | try {
37 | // Close the main MCP server
38 | if (server) {
39 | logger.info("Closing main MCP server...", shutdownContext);
40 | await server.close();
41 | logger.info("Main MCP server closed successfully", shutdownContext);
42 | } else {
43 | // Based on the previous error, logger.warn does not exist. Let's use logger.info for now.
44 | logger.warning("Server instance not found during shutdown.", shutdownContext);
45 | }
46 |
47 | logger.info("Graceful shutdown completed successfully", shutdownContext);
48 | process.exit(0);
49 | } catch (error) {
50 | // Handle any errors during shutdown
51 | const errorLogContext = requestContextService.createRequestContext({
52 | ...shutdownContext, // Spread existing RequestContext properties
53 | error: error instanceof Error ? error.message : String(error),
54 | stack: error instanceof Error ? error.stack : undefined
55 | });
56 | logger.error("Critical error during shutdown", errorLogContext);
57 | process.exit(1); // Exit with error code if shutdown fails
58 | }
59 | };
60 |
61 | /**
62 | * Initializes and starts the main MCP server.
63 | * Sets up request context, creates the server instance, and registers signal handlers
64 | * for graceful shutdown and error handling.
65 | */
66 | const start = async () => {
67 | // Create application-level request context using the service instance
68 | const startupContext = requestContextService.createRequestContext({
69 | operation: 'ServerStartup',
70 | appName: config.mcpServerName,
71 | appVersion: config.mcpServerVersion,
72 | environment: environment // Use imported environment
73 | });
74 |
75 | logger.info(`Starting ${config.mcpServerName} v${config.mcpServerVersion}...`, startupContext);
76 |
77 | try {
78 | // Create and store the main server instance
79 | logger.debug("Creating main MCP server instance", startupContext);
80 | // Use ErrorHandler to wrap the server creation, ensuring errors are caught and logged
81 | server = await ErrorHandler.tryCatch(
82 | async () => await initializeAndStartServer(), // Changed function call
83 | {
84 | operation: 'creating main MCP server',
85 | context: startupContext, // Pass the established startup context
86 | errorCode: BaseErrorCode.INTERNAL_ERROR // Specify error code for failure
87 | }
88 | );
89 |
90 | // If tryCatch encountered an error, it would have thrown,
91 | // and execution would jump to the outer catch block.
92 |
93 | logger.info(`${config.mcpServerName} is running and awaiting messages`, {
94 | ...startupContext,
95 | startTime: new Date().toISOString(),
96 | });
97 |
98 | // --- Signal and Error Handling Setup ---
99 |
100 | // Handle process signals for graceful shutdown
101 | process.on("SIGTERM", () => shutdown("SIGTERM"));
102 | process.on("SIGINT", () => shutdown("SIGINT"));
103 |
104 | // Handle uncaught exceptions
105 | process.on("uncaughtException", async (error) => {
106 | const errorContext = requestContextService.createRequestContext({
107 | ...startupContext, // Include base context for correlation
108 | event: 'uncaughtException',
109 | error: error instanceof Error ? error.message : String(error),
110 | stack: error instanceof Error ? error.stack : undefined
111 | });
112 | logger.error("Uncaught exception detected. Initiating shutdown...", errorContext);
113 | // Attempt graceful shutdown; shutdown() handles its own errors.
114 | await shutdown("uncaughtException");
115 | // If shutdown fails internally, it will call process.exit(1).
116 | // If shutdown succeeds, it calls process.exit(0).
117 | // If shutdown itself throws unexpectedly *before* exiting, this process might terminate abruptly,
118 | // but the core shutdown logic is handled within shutdown().
119 | });
120 |
121 | // Handle unhandled promise rejections
122 | process.on("unhandledRejection", async (reason: unknown) => {
123 | const rejectionContext = requestContextService.createRequestContext({
124 | ...startupContext, // Include base context for correlation
125 | event: 'unhandledRejection',
126 | reason: reason instanceof Error ? reason.message : String(reason),
127 | stack: reason instanceof Error ? reason.stack : undefined
128 | });
129 | logger.error("Unhandled promise rejection detected. Initiating shutdown...", rejectionContext);
130 | // Attempt graceful shutdown; shutdown() handles its own errors.
131 | await shutdown("unhandledRejection");
132 | // Similar logic as uncaughtException: shutdown handles its exit codes.
133 | });
134 | } catch (error) {
135 | // Handle critical startup errors (already logged by ErrorHandler or caught above)
136 | // Log the final failure context, including error details, before exiting
137 | const finalErrorContext = requestContextService.createRequestContext({
138 | ...startupContext,
139 | finalErrorContext: 'Startup Failure',
140 | error: error instanceof Error ? error.message : String(error),
141 | stack: error instanceof Error ? error.stack : undefined,
142 | });
143 | logger.error("Critical error during startup, exiting.", finalErrorContext);
144 | process.exit(1);
145 | }
146 | };
147 |
148 | // Start the application
149 | start();
150 |
```
--------------------------------------------------------------------------------
/src/mcp-server/tools/movePath/movePathLogic.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 MovePathInputSchema = z.object({
11 | source_path: z.string().min(1, 'Source path cannot be empty')
12 | .describe('The current path of the file or directory to move. Can be relative or absolute.'),
13 | destination_path: z.string().min(1, 'Destination path cannot be empty')
14 | .describe('The new path for the file or directory. Can be relative or absolute.'),
15 | });
16 |
17 | // Define the TypeScript type for the input
18 | export type MovePathInput = z.infer<typeof MovePathInputSchema>;
19 |
20 | // Define the TypeScript type for the output
21 | export interface MovePathOutput {
22 | message: string;
23 | sourcePath: string;
24 | destinationPath: string;
25 | }
26 |
27 | /**
28 | * Moves or renames a file or directory.
29 | *
30 | * @param {MovePathInput} input - The input object containing source and destination paths.
31 | * @param {RequestContext} context - The request context.
32 | * @returns {Promise<MovePathOutput>} A promise resolving with the move status.
33 | * @throws {McpError} For path errors, source not found, destination already exists (depending on OS/FS behavior), or I/O errors.
34 | */
35 | export const movePathLogic = async (input: MovePathInput, context: RequestContext): Promise<MovePathOutput> => {
36 | const { source_path: requestedSourcePath, destination_path: requestedDestPath } = input;
37 | const logicContext = { ...context, tool: 'movePathLogic' };
38 | logger.debug(`movePathLogic: Received request to move "${requestedSourcePath}" to "${requestedDestPath}"`, logicContext);
39 |
40 | // Resolve source and destination paths
41 | const absoluteSourcePath = serverState.resolvePath(requestedSourcePath, context);
42 | const absoluteDestPath = serverState.resolvePath(requestedDestPath, context);
43 | logger.debug(`movePathLogic: Resolved source to "${absoluteSourcePath}", destination to "${absoluteDestPath}"`, { ...logicContext, requestedSourcePath, requestedDestPath });
44 |
45 | // Basic check: source and destination cannot be the same
46 | if (absoluteSourcePath === absoluteDestPath) {
47 | logger.warning(`movePathLogic: Source and destination paths are identical "${absoluteSourcePath}"`, logicContext);
48 | throw new McpError(BaseErrorCode.VALIDATION_ERROR, 'Source and destination paths cannot be the same.', { ...logicContext, absoluteSourcePath, absoluteDestPath });
49 | }
50 |
51 | try {
52 | // 1. Check if source exists
53 | try {
54 | await fs.access(absoluteSourcePath); // Check existence and permissions
55 | logger.debug(`movePathLogic: Source path "${absoluteSourcePath}" exists and is accessible`, logicContext);
56 | } catch (accessError: any) {
57 | if (accessError.code === 'ENOENT') {
58 | logger.warning(`movePathLogic: Source path not found "${absoluteSourcePath}"`, logicContext);
59 | throw new McpError(BaseErrorCode.NOT_FOUND, `Source path not found: ${absoluteSourcePath}`, { ...logicContext, requestedSourcePath, absoluteSourcePath, originalError: accessError });
60 | }
61 | // Other access errors (e.g., permissions)
62 | logger.error(`movePathLogic: Cannot access source path "${absoluteSourcePath}"`, { ...logicContext, error: accessError.message });
63 | throw new McpError(BaseErrorCode.INTERNAL_ERROR, `Cannot access source path: ${accessError.message}`, { ...logicContext, requestedSourcePath, absoluteSourcePath, originalError: accessError });
64 | }
65 |
66 | // 2. Check if destination *parent* directory exists
67 | const destDir = path.dirname(absoluteDestPath);
68 | try {
69 | await fs.access(destDir);
70 | logger.debug(`movePathLogic: Destination parent directory "${destDir}" exists`, logicContext);
71 | } catch (parentAccessError: any) {
72 | logger.error(`movePathLogic: Destination parent directory does not exist or is inaccessible "${destDir}"`, { ...logicContext, error: parentAccessError.message });
73 | throw new McpError(BaseErrorCode.VALIDATION_ERROR, `Destination directory does not exist or is inaccessible: ${destDir}`, { ...logicContext, requestedDestPath, absoluteDestPath, destDir, originalError: parentAccessError });
74 | }
75 |
76 | // 3. Check if destination path already exists (fs.rename behavior varies, so check explicitly)
77 | try {
78 | await fs.access(absoluteDestPath);
79 | // If access succeeds, the destination exists. Throw an error.
80 | logger.warning(`movePathLogic: Destination path already exists "${absoluteDestPath}"`, logicContext);
81 | throw new McpError(BaseErrorCode.VALIDATION_ERROR, `Destination path already exists: ${absoluteDestPath}. Cannot overwrite.`, { ...logicContext, requestedDestPath, absoluteDestPath });
82 | } catch (destAccessError: any) {
83 | if (destAccessError.code !== 'ENOENT') {
84 | // If error is something other than "Not Found", it's an unexpected issue.
85 | logger.error(`movePathLogic: Error checking destination path "${absoluteDestPath}"`, { ...logicContext, error: destAccessError.message });
86 | throw new McpError(BaseErrorCode.INTERNAL_ERROR, `Error checking destination path: ${destAccessError.message}`, { ...logicContext, requestedDestPath, absoluteDestPath, originalError: destAccessError });
87 | }
88 | // ENOENT means destination does not exist, which is good. Proceed.
89 | logger.debug(`movePathLogic: Destination path "${absoluteDestPath}" does not exist, proceeding with move`, logicContext);
90 | }
91 |
92 |
93 | // 4. Attempt to move/rename
94 | await fs.rename(absoluteSourcePath, absoluteDestPath);
95 | logger.info(`movePathLogic: Successfully moved "${absoluteSourcePath}" to "${absoluteDestPath}"`, logicContext);
96 |
97 | return {
98 | message: `Successfully moved ${absoluteSourcePath} to ${absoluteDestPath}`,
99 | sourcePath: absoluteSourcePath,
100 | destinationPath: absoluteDestPath,
101 | };
102 |
103 | } catch (error: any) {
104 | logger.error(`movePathLogic: Error moving path "${absoluteSourcePath}" to "${absoluteDestPath}"`, { ...logicContext, error: error.message, code: error.code });
105 |
106 | if (error instanceof McpError) {
107 | throw error; // Re-throw known McpErrors (like source not found, dest exists)
108 | }
109 |
110 | // Handle other potential I/O errors (e.g., permissions, cross-device link)
111 | throw new McpError(BaseErrorCode.INTERNAL_ERROR, `Failed to move path: ${error.message || 'Unknown I/O error'}`, { ...logicContext, absoluteSourcePath, absoluteDestPath, originalError: error });
112 | }
113 | };
114 |
```
--------------------------------------------------------------------------------
/src/utils/security/rateLimiter.ts:
--------------------------------------------------------------------------------
```typescript
1 | /**
2 | * @fileoverview Provides a generic `RateLimiter` class for implementing rate limiting logic.
3 | * It supports configurable time windows, request limits, and automatic cleanup of expired entries.
4 | * @module src/utils/security/rateLimiter
5 | */
6 | import { environment } from "../../config/index.js";
7 | import { BaseErrorCode, McpError } from "../../types-global/errors.js";
8 | import { logger, RequestContext, requestContextService } from "../index.js";
9 |
10 | /**
11 | * Defines configuration options for the {@link RateLimiter}.
12 | */
13 | export interface RateLimitConfig {
14 | /** Time window in milliseconds. */
15 | windowMs: number;
16 | /** Maximum number of requests allowed in the window. */
17 | maxRequests: number;
18 | /** Custom error message template. Can include `{waitTime}` placeholder. */
19 | errorMessage?: string;
20 | /** If true, skip rate limiting in development. */
21 | skipInDevelopment?: boolean;
22 | /** Optional function to generate a custom key for rate limiting. */
23 | keyGenerator?: (identifier: string, context?: RequestContext) => string;
24 | /** How often, in milliseconds, to clean up expired entries. */
25 | cleanupInterval?: number;
26 | }
27 |
28 | /**
29 | * Represents an individual entry for tracking requests against a rate limit key.
30 | */
31 | export interface RateLimitEntry {
32 | /** Current request count. */
33 | count: number;
34 | /** When the window resets (timestamp in milliseconds). */
35 | resetTime: number;
36 | }
37 |
38 | /**
39 | * A generic rate limiter class using an in-memory store.
40 | * Controls frequency of operations based on unique keys.
41 | */
42 | export class RateLimiter {
43 | /**
44 | * Stores current request counts and reset times for each key.
45 | * @private
46 | */
47 | private limits: Map<string, RateLimitEntry>;
48 | /**
49 | * Timer ID for periodic cleanup.
50 | * @private
51 | */
52 | private cleanupTimer: NodeJS.Timeout | null = null;
53 |
54 | /**
55 | * Default configuration values.
56 | * @private
57 | */
58 | private static DEFAULT_CONFIG: RateLimitConfig = {
59 | windowMs: 15 * 60 * 1000, // 15 minutes
60 | maxRequests: 100,
61 | errorMessage:
62 | "Rate limit exceeded. Please try again in {waitTime} seconds.",
63 | skipInDevelopment: false,
64 | cleanupInterval: 5 * 60 * 1000, // 5 minutes
65 | };
66 |
67 | /**
68 | * Creates a new `RateLimiter` instance.
69 | * @param config - Configuration options, merged with defaults.
70 | */
71 | constructor(private config: RateLimitConfig) {
72 | this.config = { ...RateLimiter.DEFAULT_CONFIG, ...config };
73 | this.limits = new Map();
74 | this.startCleanupTimer();
75 | }
76 |
77 | /**
78 | * Starts the periodic timer to clean up expired rate limit entries.
79 | * @private
80 | */
81 | private startCleanupTimer(): void {
82 | if (this.cleanupTimer) {
83 | clearInterval(this.cleanupTimer);
84 | }
85 |
86 | const interval =
87 | this.config.cleanupInterval ?? RateLimiter.DEFAULT_CONFIG.cleanupInterval;
88 |
89 | if (interval && interval > 0) {
90 | this.cleanupTimer = setInterval(() => {
91 | this.cleanupExpiredEntries();
92 | }, interval);
93 |
94 | if (this.cleanupTimer.unref) {
95 | this.cleanupTimer.unref(); // Allow Node.js process to exit if only timer active
96 | }
97 | }
98 | }
99 |
100 | /**
101 | * Removes expired rate limit entries from the store.
102 | * @private
103 | */
104 | private cleanupExpiredEntries(): void {
105 | const now = Date.now();
106 | let expiredCount = 0;
107 |
108 | for (const [key, entry] of this.limits.entries()) {
109 | if (now >= entry.resetTime) {
110 | this.limits.delete(key);
111 | expiredCount++;
112 | }
113 | }
114 |
115 | if (expiredCount > 0) {
116 | const logContext = requestContextService.createRequestContext({
117 | operation: "RateLimiter.cleanupExpiredEntries",
118 | cleanedCount: expiredCount,
119 | totalRemainingAfterClean: this.limits.size,
120 | });
121 | logger.debug(
122 | `Cleaned up ${expiredCount} expired rate limit entries`,
123 | logContext,
124 | );
125 | }
126 | }
127 |
128 | /**
129 | * Updates the configuration of the rate limiter instance.
130 | * @param config - New configuration options to merge.
131 | */
132 | public configure(config: Partial<RateLimitConfig>): void {
133 | this.config = { ...this.config, ...config };
134 | if (config.cleanupInterval !== undefined) {
135 | this.startCleanupTimer();
136 | }
137 | }
138 |
139 | /**
140 | * Retrieves a copy of the current rate limiter configuration.
141 | * @returns The current configuration.
142 | */
143 | public getConfig(): RateLimitConfig {
144 | return { ...this.config };
145 | }
146 |
147 | /**
148 | * Resets all rate limits by clearing the internal store.
149 | */
150 | public reset(): void {
151 | this.limits.clear();
152 | const logContext = requestContextService.createRequestContext({
153 | operation: "RateLimiter.reset",
154 | });
155 | logger.debug("Rate limiter reset, all limits cleared", logContext);
156 | }
157 |
158 | /**
159 | * Checks if a request exceeds the configured rate limit.
160 | * Throws an `McpError` if the limit is exceeded.
161 | *
162 | * @param key - A unique identifier for the request source.
163 | * @param context - Optional request context for custom key generation.
164 | * @throws {McpError} If the rate limit is exceeded.
165 | */
166 | public check(key: string, context?: RequestContext): void {
167 | if (this.config.skipInDevelopment && environment === "development") {
168 | return;
169 | }
170 |
171 | const limitKey = this.config.keyGenerator
172 | ? this.config.keyGenerator(key, context)
173 | : key;
174 |
175 | const now = Date.now();
176 | const entry = this.limits.get(limitKey);
177 |
178 | if (!entry || now >= entry.resetTime) {
179 | this.limits.set(limitKey, {
180 | count: 1,
181 | resetTime: now + this.config.windowMs,
182 | });
183 | return;
184 | }
185 |
186 | if (entry.count >= this.config.maxRequests) {
187 | const waitTime = Math.ceil((entry.resetTime - now) / 1000);
188 | const errorMessage = (
189 | this.config.errorMessage || RateLimiter.DEFAULT_CONFIG.errorMessage!
190 | ).replace("{waitTime}", waitTime.toString());
191 |
192 | throw new McpError(BaseErrorCode.RATE_LIMITED, errorMessage, {
193 | waitTimeSeconds: waitTime,
194 | key: limitKey,
195 | limit: this.config.maxRequests,
196 | windowMs: this.config.windowMs,
197 | });
198 | }
199 |
200 | entry.count++;
201 | }
202 |
203 | /**
204 | * Retrieves the current rate limit status for a specific key.
205 | * @param key - The rate limit key.
206 | * @returns Status object or `null` if no entry exists.
207 | */
208 | public getStatus(key: string): {
209 | current: number;
210 | limit: number;
211 | remaining: number;
212 | resetTime: number;
213 | } | null {
214 | const entry = this.limits.get(key);
215 | if (!entry) {
216 | return null;
217 | }
218 | return {
219 | current: entry.count,
220 | limit: this.config.maxRequests,
221 | remaining: Math.max(0, this.config.maxRequests - entry.count),
222 | resetTime: entry.resetTime,
223 | };
224 | }
225 |
226 | /**
227 | * Stops the cleanup timer and clears all rate limit entries.
228 | * Call when the rate limiter is no longer needed.
229 | */
230 | public dispose(): void {
231 | if (this.cleanupTimer) {
232 | clearInterval(this.cleanupTimer);
233 | this.cleanupTimer = null;
234 | }
235 | this.limits.clear();
236 | }
237 | }
238 |
239 | /**
240 | * Default singleton instance of the `RateLimiter`.
241 | * Initialized with default configuration. Use `rateLimiter.configure({})` to customize.
242 | */
243 | export const rateLimiter = new RateLimiter({
244 | windowMs: 15 * 60 * 1000, // Default: 15 minutes
245 | maxRequests: 100, // Default: 100 requests per window
246 | });
247 |
```
--------------------------------------------------------------------------------
/src/mcp-server/tools/copyPath/copyPathLogic.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 CopyPathInputSchema = z.object({
11 | source_path: z.string().min(1, 'Source path cannot be empty')
12 | .describe('The path of the file or directory to copy. Can be relative or absolute.'),
13 | destination_path: z.string().min(1, 'Destination path cannot be empty')
14 | .describe('The path where the copy should be created. Can be relative or absolute.'),
15 | recursive: z.boolean().default(true) // Defaulting to true as it's the common expectation for directory copies
16 | .describe('If copying a directory, whether to copy its contents recursively. Defaults to true.'),
17 | });
18 |
19 | // Define the TypeScript type for the input
20 | export type CopyPathInput = z.infer<typeof CopyPathInputSchema>;
21 |
22 | // Define the TypeScript type for the output
23 | export interface CopyPathOutput {
24 | message: string;
25 | sourcePath: string;
26 | destinationPath: string;
27 | wasRecursive: boolean | null; // null if source was a file
28 | }
29 |
30 | /**
31 | * Copies a file or directory to a new location.
32 | *
33 | * @param {CopyPathInput} input - The input object containing source, destination, and recursive flag.
34 | * @param {RequestContext} context - The request context.
35 | * @returns {Promise<CopyPathOutput>} A promise resolving with the copy status.
36 | * @throws {McpError} For path errors, source not found, destination already exists, or I/O errors.
37 | */
38 | export const copyPathLogic = async (input: CopyPathInput, context: RequestContext): Promise<CopyPathOutput> => {
39 | const { source_path: requestedSourcePath, destination_path: requestedDestPath, recursive } = input;
40 | const logicContext = { ...context, tool: 'copyPathLogic', recursive };
41 | logger.debug(`copyPathLogic: Received request to copy "${requestedSourcePath}" to "${requestedDestPath}"`, logicContext);
42 |
43 | // Resolve source and destination paths
44 | const absoluteSourcePath = serverState.resolvePath(requestedSourcePath, context);
45 | const absoluteDestPath = serverState.resolvePath(requestedDestPath, context);
46 | logger.debug(`copyPathLogic: Resolved source to "${absoluteSourcePath}", destination to "${absoluteDestPath}"`, { ...logicContext, requestedSourcePath, requestedDestPath });
47 |
48 | // Basic check: source and destination cannot be the same
49 | if (absoluteSourcePath === absoluteDestPath) {
50 | logger.warning(`copyPathLogic: Source and destination paths are identical "${absoluteSourcePath}"`, logicContext);
51 | throw new McpError(BaseErrorCode.VALIDATION_ERROR, 'Source and destination paths cannot be the same.', { ...logicContext, absoluteSourcePath, absoluteDestPath });
52 | }
53 |
54 | try {
55 | // 1. Check if source exists and get its type (file or directory)
56 | let sourceStats;
57 | try {
58 | sourceStats = await fs.stat(absoluteSourcePath);
59 | logger.debug(`copyPathLogic: Source path "${absoluteSourcePath}" exists`, logicContext);
60 | } catch (statError: any) {
61 | if (statError.code === 'ENOENT') {
62 | logger.warning(`copyPathLogic: Source path not found "${absoluteSourcePath}"`, logicContext);
63 | throw new McpError(BaseErrorCode.NOT_FOUND, `Source path not found: ${absoluteSourcePath}`, { ...logicContext, requestedSourcePath, absoluteSourcePath, originalError: statError });
64 | }
65 | logger.error(`copyPathLogic: Cannot stat source path "${absoluteSourcePath}"`, { ...logicContext, error: statError.message });
66 | throw new McpError(BaseErrorCode.INTERNAL_ERROR, `Cannot access source path: ${statError.message}`, { ...logicContext, requestedSourcePath, absoluteSourcePath, originalError: statError });
67 | }
68 |
69 | const isDirectory = sourceStats.isDirectory();
70 | const effectiveRecursive = isDirectory ? recursive : null; // Recursive flag only relevant for directories
71 |
72 | // 2. Check if destination *parent* directory exists
73 | const destDir = path.dirname(absoluteDestPath);
74 | try {
75 | await fs.access(destDir);
76 | logger.debug(`copyPathLogic: Destination parent directory "${destDir}" exists`, logicContext);
77 | } catch (parentAccessError: any) {
78 | logger.error(`copyPathLogic: Destination parent directory does not exist or is inaccessible "${destDir}"`, { ...logicContext, error: parentAccessError.message });
79 | throw new McpError(BaseErrorCode.VALIDATION_ERROR, `Destination directory does not exist or is inaccessible: ${destDir}`, { ...logicContext, requestedDestPath, absoluteDestPath, destDir, originalError: parentAccessError });
80 | }
81 |
82 | // 3. Check if destination path already exists (fs.cp throws by default if destination exists)
83 | try {
84 | await fs.access(absoluteDestPath);
85 | // If access succeeds, the destination exists. Throw an error.
86 | logger.warning(`copyPathLogic: Destination path already exists "${absoluteDestPath}"`, logicContext);
87 | throw new McpError(BaseErrorCode.VALIDATION_ERROR, `Destination path already exists: ${absoluteDestPath}. Cannot overwrite.`, { ...logicContext, requestedDestPath, absoluteDestPath });
88 | } catch (destAccessError: any) {
89 | if (destAccessError.code !== 'ENOENT') {
90 | // If error is something other than "Not Found", it's an unexpected issue.
91 | logger.error(`copyPathLogic: Error checking destination path "${absoluteDestPath}"`, { ...logicContext, error: destAccessError.message });
92 | throw new McpError(BaseErrorCode.INTERNAL_ERROR, `Error checking destination path: ${destAccessError.message}`, { ...logicContext, requestedDestPath, absoluteDestPath, originalError: destAccessError });
93 | }
94 | // ENOENT means destination does not exist, which is good. Proceed.
95 | logger.debug(`copyPathLogic: Destination path "${absoluteDestPath}" does not exist, proceeding with copy`, logicContext);
96 | }
97 |
98 | // 4. Attempt to copy using fs.cp (Node.js v16.7.0+)
99 | // fs.cp handles both files and directories, respecting the recursive flag for directories.
100 | // It throws if the destination exists by default.
101 | await fs.cp(absoluteSourcePath, absoluteDestPath, {
102 | recursive: effectiveRecursive ?? false, // Pass recursive flag only if it's a directory
103 | errorOnExist: true, // Explicitly ensure it errors if destination exists (default behavior)
104 | force: false // Do not overwrite
105 | });
106 |
107 | logger.info(`copyPathLogic: Successfully copied "${absoluteSourcePath}" to "${absoluteDestPath}" (Recursive: ${effectiveRecursive})`, logicContext);
108 |
109 | return {
110 | message: `Successfully copied ${absoluteSourcePath} to ${absoluteDestPath}${isDirectory ? (recursive ? ' (recursively)' : ' (non-recursively, directory structure only)') : ''}`,
111 | sourcePath: absoluteSourcePath,
112 | destinationPath: absoluteDestPath,
113 | wasRecursive: effectiveRecursive,
114 | };
115 |
116 | } catch (error: any) {
117 | logger.error(`copyPathLogic: Error copying path "${absoluteSourcePath}" to "${absoluteDestPath}"`, { ...logicContext, error: error.message, code: error.code });
118 |
119 | if (error instanceof McpError) {
120 | throw error; // Re-throw known McpErrors
121 | }
122 |
123 | // Handle potential I/O errors (e.g., permissions, disk full)
124 | throw new McpError(BaseErrorCode.INTERNAL_ERROR, `Failed to copy path: ${error.message || 'Unknown I/O error'}`, { ...logicContext, absoluteSourcePath, absoluteDestPath, originalError: error });
125 | }
126 | };
127 |
```
--------------------------------------------------------------------------------
/src/mcp-server/state.ts:
--------------------------------------------------------------------------------
```typescript
1 | import path from 'path';
2 | import { config } from '../config/index.js';
3 | import { BaseErrorCode, McpError } from '../types-global/errors.js';
4 | import { logger } from '../utils/internal/logger.js';
5 | import { RequestContext, requestContextService } from '../utils/internal/requestContext.js';
6 | import { sanitization } from '../utils/security/sanitization.js';
7 |
8 | /**
9 | * Simple in-memory state management for the MCP server session.
10 | * This state is cleared when the server restarts.
11 | */
12 | class ServerState {
13 | private defaultFilesystemPath: string | null = null;
14 | private fsBaseDirectory: string | null = null;
15 |
16 | constructor() {
17 | this.fsBaseDirectory = config.fsBaseDirectory || null;
18 | if (this.fsBaseDirectory) {
19 | // Ensure fsBaseDirectory itself is sanitized and absolute for internal use
20 | const initContext = requestContextService.createRequestContext({ operation: 'ServerStateInit' });
21 | try {
22 | const sanitizedBase = sanitization.sanitizePath(this.fsBaseDirectory, { allowAbsolute: true, toPosix: true });
23 | this.fsBaseDirectory = sanitizedBase.sanitizedPath;
24 | logger.info(`Filesystem operations will be restricted to base directory: ${this.fsBaseDirectory}`, initContext);
25 | } catch (error) {
26 | logger.error(`Invalid FS_BASE_DIRECTORY configured: ${this.fsBaseDirectory}. It will be ignored.`, { ...initContext, error: error instanceof Error ? error.message : String(error) });
27 | this.fsBaseDirectory = null; // Disable if invalid
28 | }
29 | }
30 | }
31 |
32 | /**
33 | * Sets the default filesystem path for the current session.
34 | * The path is sanitized and validated.
35 | *
36 | * @param newPath - The absolute path to set as default.
37 | * @param context - The request context for logging.
38 | * @throws {McpError} If the path is invalid or not absolute.
39 | */
40 | setDefaultFilesystemPath(newPath: string, context: RequestContext): void {
41 | logger.debug(`Attempting to set default filesystem path: ${newPath}`, context);
42 | try {
43 | // Ensure the path is absolute before storing
44 | if (!path.isAbsolute(newPath)) {
45 | throw new McpError(BaseErrorCode.VALIDATION_ERROR, 'Default path must be absolute.', { ...context, path: newPath });
46 | }
47 | // Sanitize the absolute path (mainly for normalization and basic checks)
48 | // We don't restrict to a rootDir here as it's a user-provided default.
49 | const sanitizedPathInfo = sanitization.sanitizePath(newPath, { allowAbsolute: true, toPosix: true });
50 |
51 | this.defaultFilesystemPath = sanitizedPathInfo.sanitizedPath;
52 | logger.info(`Default filesystem path set to: ${this.defaultFilesystemPath}`, context);
53 | } catch (error) {
54 | logger.error(`Failed to set default filesystem path: ${newPath}`, { ...context, error: error instanceof Error ? error.message : String(error) });
55 | // Rethrow McpError or wrap other errors
56 | if (error instanceof McpError) {
57 | throw error;
58 | }
59 | throw new McpError(BaseErrorCode.VALIDATION_ERROR, `Invalid default path provided: ${error instanceof Error ? error.message : String(error)}`, { ...context, path: newPath, originalError: error });
60 | }
61 | }
62 |
63 | /**
64 | * Gets the currently set default filesystem path.
65 | *
66 | * @returns The absolute default path or null if not set.
67 | */
68 | getDefaultFilesystemPath(): string | null {
69 | return this.defaultFilesystemPath;
70 | }
71 |
72 | /**
73 | * Clears the default filesystem path.
74 | * @param context - The request context for logging.
75 | */
76 | clearDefaultFilesystemPath(context: RequestContext): void {
77 | logger.info('Clearing default filesystem path.', context);
78 | this.defaultFilesystemPath = null;
79 | }
80 |
81 | /**
82 | * Resolves a given path against the default path if the given path is relative.
83 | * If the given path is absolute, it's returned directly after sanitization.
84 | * If the given path is relative and no default path is set, an error is thrown.
85 | *
86 | * @param requestedPath - The path provided by the user (can be relative or absolute).
87 | * @param context - The request context for logging and error handling.
88 | * @returns The resolved, sanitized, absolute path.
89 | * @throws {McpError} If a relative path is given without a default path set, or if sanitization fails.
90 | */
91 | resolvePath(requestedPath: string, context: RequestContext): string {
92 | logger.debug(`Resolving path: ${requestedPath}`, { ...context, defaultPath: this.defaultFilesystemPath, fsBaseDirectory: this.fsBaseDirectory });
93 |
94 | let absolutePath: string;
95 |
96 | if (path.isAbsolute(requestedPath)) {
97 | absolutePath = requestedPath;
98 | logger.debug('Provided path is absolute.', { ...context, path: absolutePath });
99 | } else {
100 | if (!this.defaultFilesystemPath) {
101 | logger.warning('Relative path provided but no default path is set.', { ...context, path: requestedPath });
102 | throw new McpError(
103 | BaseErrorCode.VALIDATION_ERROR,
104 | 'Relative path provided, but no default filesystem path has been set for this session. Please provide an absolute path or set a default path first.',
105 | { ...context, path: requestedPath }
106 | );
107 | }
108 | absolutePath = path.join(this.defaultFilesystemPath, requestedPath);
109 | logger.debug(`Resolved relative path against default: ${absolutePath}`, { ...context, relativePath: requestedPath, defaultPath: this.defaultFilesystemPath });
110 | }
111 |
112 | let sanitizedAbsolutePath: string;
113 | try {
114 | // Sanitize the path first. allowAbsolute is true as we've resolved it.
115 | // No rootDir is enforced by sanitizePath itself here; boundary check is next.
116 | const sanitizedPathInfo = sanitization.sanitizePath(absolutePath, { allowAbsolute: true, toPosix: true });
117 | sanitizedAbsolutePath = sanitizedPathInfo.sanitizedPath;
118 | logger.debug(`Sanitized resolved path: ${sanitizedAbsolutePath}`, { ...context, originalPath: absolutePath });
119 | } catch (error) {
120 | logger.error(`Failed to sanitize resolved path: ${absolutePath}`, { ...context, error: error instanceof Error ? error.message : String(error) });
121 | if (error instanceof McpError) {
122 | throw error; // Rethrow validation errors from sanitizePath
123 | }
124 | throw new McpError(BaseErrorCode.INTERNAL_ERROR, `Failed to process path: ${error instanceof Error ? error.message : String(error)}`, { ...context, path: absolutePath, originalError: error });
125 | }
126 |
127 | // Enforce FS_BASE_DIRECTORY boundary if it's set
128 | if (this.fsBaseDirectory) {
129 | // Normalize both paths for a reliable comparison
130 | const normalizedFsBaseDirectory = path.normalize(this.fsBaseDirectory);
131 | const normalizedSanitizedAbsolutePath = path.normalize(sanitizedAbsolutePath);
132 |
133 | // Check if the sanitized absolute path is within the base directory
134 | if (!normalizedSanitizedAbsolutePath.startsWith(normalizedFsBaseDirectory + path.sep) && normalizedSanitizedAbsolutePath !== normalizedFsBaseDirectory) {
135 | logger.error(
136 | `Path access violation: Attempted to access path "${sanitizedAbsolutePath}" which is outside the configured FS_BASE_DIRECTORY "${this.fsBaseDirectory}".`,
137 | { ...context, requestedPath, resolvedPath: sanitizedAbsolutePath, fsBaseDirectory: this.fsBaseDirectory }
138 | );
139 | throw new McpError(
140 | BaseErrorCode.FORBIDDEN,
141 | `Access denied: The path "${requestedPath}" resolves to a location outside the allowed base directory.`,
142 | { ...context, requestedPath, resolvedPath: sanitizedAbsolutePath }
143 | );
144 | }
145 | logger.debug(`Path is within FS_BASE_DIRECTORY: ${sanitizedAbsolutePath}`, context);
146 | }
147 |
148 | return sanitizedAbsolutePath;
149 | }
150 | }
151 |
152 | // Export a singleton instance
153 | export const serverState = new ServerState();
154 |
```
--------------------------------------------------------------------------------
/src/mcp-server/server.ts:
--------------------------------------------------------------------------------
```typescript
1 | /**
2 | * @fileoverview Main entry point for the Filesystem MCP (Model Context Protocol) server.
3 | * This file orchestrates the server's lifecycle:
4 | * 1. Initializes the core `McpServer` instance (from `@modelcontextprotocol/sdk`) with its identity and capabilities.
5 | * 2. Registers available filesystem tools, making them discoverable and usable by clients.
6 | * 3. Selects and starts the appropriate communication transport (currently stdio)
7 | * based on configuration.
8 | * 4. Handles top-level error management during startup.
9 | *
10 | * MCP Specification References:
11 | * - Lifecycle: https://github.com/modelcontextprotocol/modelcontextprotocol/blob/main/docs/specification/2025-03-26/basic/lifecycle.mdx
12 | * - Overview (Capabilities): https://github.com/modelcontextprotocol/modelcontextprotocol/blob/main/docs/specification/2025-03-26/basic/index.mdx
13 | * - Transports: https://github.com/modelcontextprotocol/modelcontextprotocol/blob/main/docs/specification/2025-03-26/basic/transports.mdx
14 | * @module src/mcp-server/server
15 | */
16 |
17 | import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
18 | import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
19 | import { config, environment } from '../config/index.js';
20 | import { ErrorHandler, logger, requestContextService } from '../utils/index.js'; // Corrected import path
21 | import { registerCopyPathTool } from './tools/copyPath/index.js';
22 | import { registerCreateDirectoryTool } from './tools/createDirectory/index.js';
23 | import { registerDeleteDirectoryTool } from './tools/deleteDirectory/index.js';
24 | import { registerDeleteFileTool } from './tools/deleteFile/index.js';
25 | import { registerListFilesTool } from './tools/listFiles/index.js';
26 | import { registerMovePathTool } from './tools/movePath/index.js';
27 | import { registerReadFileTool } from './tools/readFile/index.js';
28 | import { registerSetFilesystemDefaultTool } from './tools/setFilesystemDefault/index.js';
29 | import { registerUpdateFileTool } from './tools/updateFile/index.js';
30 | import { registerWriteFileTool } from './tools/writeFile/index.js';
31 | import { startHttpTransport } from "./transports/httpTransport.js";
32 |
33 | /**
34 | * Creates and configures a new instance of the `McpServer`.
35 | *
36 | * This function defines the server's identity and capabilities as presented
37 | * to clients during MCP initialization.
38 | *
39 | * @returns A promise resolving with the configured `McpServer` instance.
40 | * @throws {Error} If any tool registration fails.
41 | * @private
42 | */
43 | async function createMcpServerInstance(): Promise<McpServer> {
44 | const context = requestContextService.createRequestContext({
45 | operation: "createMcpServerInstance",
46 | });
47 | logger.info("Initializing MCP server instance", context);
48 |
49 | requestContextService.configure({
50 | appName: config.mcpServerName,
51 | appVersion: config.mcpServerVersion,
52 | environment,
53 | });
54 |
55 | logger.debug("Instantiating McpServer with capabilities", {
56 | ...context,
57 | serverInfo: {
58 | name: config.mcpServerName,
59 | version: config.mcpServerVersion,
60 | },
61 | capabilities: {
62 | logging: {},
63 | resources: { listChanged: true }, // Assuming dynamic resources might be added later
64 | tools: { listChanged: true }, // Filesystem tools are dynamically registered
65 | },
66 | });
67 |
68 | const server = new McpServer(
69 | { name: config.mcpServerName, version: config.mcpServerVersion },
70 | {
71 | capabilities: {
72 | logging: {},
73 | resources: { listChanged: true },
74 | tools: { listChanged: true },
75 | },
76 | },
77 | );
78 |
79 | try {
80 | logger.debug("Registering filesystem tools...", context);
81 | const registrationPromises = [
82 | registerReadFileTool(server),
83 | registerSetFilesystemDefaultTool(server),
84 | registerWriteFileTool(server),
85 | registerUpdateFileTool(server),
86 | registerListFilesTool(server),
87 | registerDeleteFileTool(server),
88 | registerDeleteDirectoryTool(server),
89 | registerCreateDirectoryTool(server),
90 | registerMovePathTool(server),
91 | registerCopyPathTool(server)
92 | ];
93 |
94 | await Promise.all(registrationPromises);
95 | logger.info("Filesystem tools registered successfully", context);
96 | } catch (err) {
97 | logger.error("Failed to register filesystem tools", {
98 | ...context,
99 | error: err instanceof Error ? err.message : String(err),
100 | stack: err instanceof Error ? err.stack : undefined,
101 | });
102 | throw err; // Rethrow to be caught by the caller
103 | }
104 |
105 | return server;
106 | }
107 |
108 | /**
109 | * Selects, sets up, and starts the appropriate MCP transport layer based on configuration.
110 | * Currently, only 'stdio' transport is implemented for filesystem-mcp-server.
111 | *
112 | * @returns Resolves with `McpServer` for 'stdio'.
113 | * @throws {Error} If transport type is unsupported or setup fails.
114 | * @private
115 | */
116 | async function startTransport(): Promise<McpServer | void> {
117 | const transportType = config.mcpTransportType; // Using the newly added config property
118 | const context = requestContextService.createRequestContext({
119 | operation: "startTransport",
120 | transport: transportType,
121 | });
122 | logger.info(`Starting transport: ${transportType}`, context);
123 |
124 | if (transportType === "http") {
125 | logger.debug("Delegating to startHttpTransport...", context);
126 | await startHttpTransport(createMcpServerInstance, context);
127 | return;
128 | }
129 |
130 | if (transportType === "stdio") {
131 | logger.debug(
132 | "Creating single McpServer instance for stdio transport...",
133 | context,
134 | );
135 | const server = await createMcpServerInstance();
136 | logger.debug("Connecting StdioServerTransport...", context);
137 | try {
138 | const transport = new StdioServerTransport();
139 | await server.connect(transport);
140 | logger.info(`${config.mcpServerName} connected successfully via stdio`, {
141 | ...context,
142 | serverName: config.mcpServerName,
143 | version: config.mcpServerVersion
144 | });
145 | } catch (connectionError) {
146 | // Handle connection errors specifically
147 | ErrorHandler.handleError(connectionError, {
148 | operation: 'StdioServerTransport Connection',
149 | context: context, // Pass the existing context
150 | critical: true,
151 | rethrow: true // Rethrow to allow the main startup process to handle exit
152 | });
153 | // This line won't be reached if rethrow is true
154 | throw connectionError;
155 | }
156 | return server; // Return the single server instance for stdio.
157 | }
158 |
159 | logger.fatal(
160 | `Unsupported transport type configured: ${transportType}`,
161 | context,
162 | );
163 | throw new Error(
164 | `Unsupported transport type: ${transportType}. Must be 'stdio' or 'http'.`,
165 | );
166 | }
167 |
168 | /**
169 | * Main application entry point. Initializes and starts the MCP server.
170 | * Orchestrates server startup, transport selection, and top-level error handling.
171 | *
172 | * @returns For 'stdio', resolves with `McpServer`.
173 | * Rejects on critical failure, leading to process exit.
174 | */
175 | export async function initializeAndStartServer(): Promise<void | McpServer> {
176 | const context = requestContextService.createRequestContext({
177 | operation: "initializeAndStartServer",
178 | });
179 | logger.info("Filesystem MCP Server initialization sequence started.", context);
180 | try {
181 | const result = await startTransport();
182 | logger.info(
183 | "Filesystem MCP Server initialization sequence completed successfully.",
184 | context,
185 | );
186 | return result;
187 | } catch (err) {
188 | logger.fatal("Critical error during Filesystem MCP server initialization.", {
189 | ...context,
190 | error: err instanceof Error ? err.message : String(err),
191 | stack: err instanceof Error ? err.stack : undefined,
192 | });
193 | ErrorHandler.handleError(err, {
194 | operation: "initializeAndStartServer",
195 | context: context,
196 | critical: true,
197 | });
198 | logger.info(
199 | "Exiting process due to critical initialization error.",
200 | context,
201 | );
202 | process.exit(1); // Exit with a non-zero code to indicate failure.
203 | }
204 | }
205 |
```