@geanatz/cortex-mcp 5.0.1 → 5.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +96 -19
- package/dist/features/task-management/tools/artifacts/index.js +8 -8
- package/dist/features/task-management/tools/base/schemas.d.ts +33 -1
- package/dist/features/task-management/tools/base/schemas.js +75 -2
- package/dist/features/task-management/tools/tasks/index.js +2 -2
- package/dist/utils/index.d.ts +1 -0
- package/dist/utils/index.js +1 -0
- package/dist/utils/path-security.d.ts +29 -0
- package/dist/utils/path-security.js +74 -0
- package/dist/utils/validation.d.ts +19 -0
- package/dist/utils/validation.js +41 -0
- package/dist/utils/version.js +2 -2
- package/package.json +12 -4
package/README.md
CHANGED
|
@@ -9,6 +9,7 @@ An MCP (Model Context Protocol) server for managing task-based workflows with ar
|
|
|
9
9
|
- **File-Based Storage** - All data stored in `.cortex/` directory with no database required
|
|
10
10
|
- **In-Memory Caching** - High-performance caching layer with TTL for frequently accessed tasks
|
|
11
11
|
- **Structured Logging** - Comprehensive logging with configurable levels
|
|
12
|
+
- **Production Ready** - Path traversal protection, input validation, size limits, error handling
|
|
12
13
|
|
|
13
14
|
## Installation
|
|
14
15
|
|
|
@@ -84,10 +85,10 @@ Or for global mode:
|
|
|
84
85
|
```
|
|
85
86
|
Tool: create_task
|
|
86
87
|
Parameters:
|
|
87
|
-
- workingDirectory: path where tasks are stored
|
|
88
|
-
- details: task description (generates task ID)
|
|
88
|
+
- workingDirectory: path where tasks are stored (absolute path, required)
|
|
89
|
+
- details: task description (generates task ID), max 2000 characters
|
|
89
90
|
- status (optional): pending | in_progress | done
|
|
90
|
-
- tags (optional): categorization tags
|
|
91
|
+
- tags (optional): categorization tags, max 20 tags, max 50 chars each
|
|
91
92
|
```
|
|
92
93
|
|
|
93
94
|
### Managing Subtasks
|
|
@@ -101,6 +102,7 @@ Parameters:
|
|
|
101
102
|
- addSubtask: { details: "Subtask description", status: "pending" }
|
|
102
103
|
- updateSubtask: { id: "1", status: "done" }
|
|
103
104
|
- removeSubtaskId: "1"
|
|
105
|
+
- actualHours (optional): number, max 10000 hours
|
|
104
106
|
```
|
|
105
107
|
|
|
106
108
|
### Managing Artifacts
|
|
@@ -115,11 +117,12 @@ Each task can have artifacts for 5 phases:
|
|
|
115
117
|
```
|
|
116
118
|
Tools: create_{phase}, update_{phase}, delete_{phase}
|
|
117
119
|
Parameters:
|
|
118
|
-
- workingDirectory: project directory
|
|
120
|
+
- workingDirectory: project directory (absolute path)
|
|
119
121
|
- taskId: which task to attach artifact to
|
|
120
|
-
- content: markdown content
|
|
122
|
+
- content: markdown content, max 10MB
|
|
121
123
|
- status: pending | in-progress | completed | failed | skipped
|
|
122
|
-
- retries
|
|
124
|
+
- retries (optional): integer, max 100
|
|
125
|
+
- error (optional): error message, max 10,000 characters
|
|
123
126
|
```
|
|
124
127
|
|
|
125
128
|
## Storage Format
|
|
@@ -166,12 +169,12 @@ Tasks are stored in `.cortex/tasks/{number}-{slug}/` directories:
|
|
|
166
169
|
```typescript
|
|
167
170
|
interface Task {
|
|
168
171
|
id: string; // Task ID (e.g., "001-implement-auth")
|
|
169
|
-
details: string; // Full task description
|
|
172
|
+
details: string; // Full task description (max 2000 chars)
|
|
170
173
|
status: 'pending' | 'in_progress' | 'done';
|
|
171
174
|
createdAt: string; // ISO 8601 timestamp
|
|
172
175
|
updatedAt: string; // Last modification timestamp
|
|
173
|
-
tags?: string[]; // Categorization tags
|
|
174
|
-
actualHours?: number; // Time tracking
|
|
176
|
+
tags?: string[]; // Categorization tags (max 20, max 50 chars each)
|
|
177
|
+
actualHours?: number; // Time tracking (max 10000 hours)
|
|
175
178
|
subtasks: Subtask[]; // Array of subtasks
|
|
176
179
|
}
|
|
177
180
|
```
|
|
@@ -180,7 +183,7 @@ interface Task {
|
|
|
180
183
|
```typescript
|
|
181
184
|
interface Subtask {
|
|
182
185
|
id: string; // Simple ID ("1", "2", etc.)
|
|
183
|
-
details: string; // Subtask description
|
|
186
|
+
details: string; // Subtask description (max 1000 chars)
|
|
184
187
|
status: 'pending' | 'in_progress' | 'done';
|
|
185
188
|
}
|
|
186
189
|
```
|
|
@@ -193,10 +196,10 @@ interface Artifact {
|
|
|
193
196
|
status: 'pending' | 'in-progress' | 'completed' | 'failed' | 'skipped';
|
|
194
197
|
createdAt: string; // ISO 8601
|
|
195
198
|
updatedAt: string; // ISO 8601
|
|
196
|
-
retries?: number; // Attempt count
|
|
197
|
-
error?: string; // Error message
|
|
199
|
+
retries?: number; // Attempt count (max 100)
|
|
200
|
+
error?: string; // Error message (max 10000 chars)
|
|
198
201
|
}
|
|
199
|
-
content: string; // Markdown content
|
|
202
|
+
content: string; // Markdown content (max 10MB)
|
|
200
203
|
}
|
|
201
204
|
```
|
|
202
205
|
|
|
@@ -234,9 +237,41 @@ interface Artifact {
|
|
|
234
237
|
### Command-Line Flags
|
|
235
238
|
- `--claude` - Use global directory mode (~/.cortex/)
|
|
236
239
|
|
|
240
|
+
### Security Considerations
|
|
241
|
+
|
|
242
|
+
This server implements several security measures:
|
|
243
|
+
|
|
244
|
+
1. **Path Traversal Protection**: All paths are validated to prevent `../` sequences and directory escape
|
|
245
|
+
2. **Input Validation**: All inputs are validated using Zod schemas with strict limits
|
|
246
|
+
3. **Size Limits**:
|
|
247
|
+
- Task details: max 2000 characters
|
|
248
|
+
- Artifact content: max 10MB
|
|
249
|
+
- Tags: max 20 tags, max 50 characters each
|
|
250
|
+
- Error messages: max 10,000 characters
|
|
251
|
+
4. **Working Directory Validation**: Must be absolute paths without traversal sequences
|
|
252
|
+
5. **Atomic File Writes**: Uses temp files and atomic rename to prevent data corruption
|
|
253
|
+
6. **Safe Error Messages**: Internal paths are not exposed in error messages
|
|
254
|
+
|
|
255
|
+
### Validation Limits
|
|
256
|
+
|
|
257
|
+
| Field | Limit |
|
|
258
|
+
|-------|-------|
|
|
259
|
+
| Task ID | 100 characters |
|
|
260
|
+
| Task details | 2000 characters |
|
|
261
|
+
| Subtask details | 1000 characters |
|
|
262
|
+
| Tags | 20 tags max, 50 chars each |
|
|
263
|
+
| Actual hours | Max 10,000 |
|
|
264
|
+
| Artifact content | 10MB max |
|
|
265
|
+
| Error messages | 10,000 characters max |
|
|
266
|
+
| Retries | Max 100 |
|
|
267
|
+
| Working directory path | 4096 characters max |
|
|
268
|
+
|
|
237
269
|
## Development
|
|
238
270
|
|
|
239
271
|
```bash
|
|
272
|
+
# Install dependencies
|
|
273
|
+
npm install
|
|
274
|
+
|
|
240
275
|
# Build TypeScript
|
|
241
276
|
npm run build
|
|
242
277
|
|
|
@@ -258,15 +293,45 @@ npm start
|
|
|
258
293
|
- **No circular dependencies** - Easy to understand data flow
|
|
259
294
|
- **Abstract storage** - File-based implementation, easily extensible
|
|
260
295
|
- **MCP-compliant** - Full compliance with Model Context Protocol specification
|
|
296
|
+
- **Security-first** - Path validation, input sanitization, size limits
|
|
297
|
+
|
|
298
|
+
## Error Handling
|
|
299
|
+
|
|
300
|
+
The server uses a comprehensive error handling strategy:
|
|
301
|
+
|
|
302
|
+
- **AppError hierarchy** - Typed errors with context
|
|
303
|
+
- **Zod validation** - Schema validation at the boundary
|
|
304
|
+
- **Graceful degradation** - Continues operating on non-fatal errors
|
|
305
|
+
- **Structured logging** - All errors logged with context to stderr
|
|
306
|
+
- **Safe shutdown** - SIGINT/SIGTERM handlers for graceful exit
|
|
307
|
+
|
|
308
|
+
## Troubleshooting
|
|
309
|
+
|
|
310
|
+
### Connection closed errors
|
|
311
|
+
- Check that the working directory exists and is accessible
|
|
312
|
+
- Verify the working directory is an absolute path
|
|
313
|
+
- Ensure no path traversal sequences (../) in workingDirectory
|
|
314
|
+
|
|
315
|
+
### Permission denied errors
|
|
316
|
+
- Verify write permissions to the working directory
|
|
317
|
+
- Check if the .cortex directory is owned by the current user
|
|
318
|
+
|
|
319
|
+
### Storage not initializing
|
|
320
|
+
- Ensure the path is an absolute path (not relative)
|
|
321
|
+
- Check that parent directories exist and are writable
|
|
261
322
|
|
|
262
323
|
## Version
|
|
263
324
|
|
|
264
|
-
Current version: **5.0.
|
|
325
|
+
Current version: **5.0.2**
|
|
265
326
|
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
-
|
|
269
|
-
-
|
|
327
|
+
### Changelog
|
|
328
|
+
|
|
329
|
+
- **v5.0.2**: Security hardening - path traversal protection, input validation, size limits
|
|
330
|
+
- **v5.0.1**: Added error handlers for connection stability
|
|
331
|
+
- **v5.0.0**: Simplified model - subtasks stored inline, removed dependencies, removed move_task
|
|
332
|
+
- **v4.0.0**: Complete refactor with artifact support and optimized build
|
|
333
|
+
- **v3.x**: Legacy memory features (deprecated)
|
|
334
|
+
- **v1.x**: Initial implementation
|
|
270
335
|
|
|
271
336
|
## License
|
|
272
337
|
|
|
@@ -278,4 +343,16 @@ Geanatz
|
|
|
278
343
|
|
|
279
344
|
## Contributing
|
|
280
345
|
|
|
281
|
-
Contributions welcome!
|
|
346
|
+
Contributions welcome! Please ensure:
|
|
347
|
+
- Code follows existing patterns
|
|
348
|
+
- All inputs are validated
|
|
349
|
+
- Security considerations are addressed
|
|
350
|
+
- Tests pass (when test suite is added)
|
|
351
|
+
|
|
352
|
+
## Security
|
|
353
|
+
|
|
354
|
+
For security issues, please email directly rather than opening a public issue.
|
|
355
|
+
|
|
356
|
+
### Reporting Vulnerabilities
|
|
357
|
+
|
|
358
|
+
If you discover a security vulnerability, please report it responsibly by contacting the maintainer directly.
|
|
@@ -4,7 +4,7 @@ import { createErrorResponse } from '../../../../utils/response-builder.js';
|
|
|
4
4
|
import { createLogger } from '../../../../utils/logger.js';
|
|
5
5
|
import { ARTIFACT_PHASES, OPERATION_DESCRIPTIONS, PHASE_DESCRIPTIONS } from '../../models/artifact.js';
|
|
6
6
|
import { withErrorHandling } from '../../tools/base/handlers.js';
|
|
7
|
-
import {
|
|
7
|
+
import { createWorkingDirectorySchema, taskIdSchema, artifactContentSchema, artifactErrorSchema, retriesSchema } from '../../tools/base/schemas.js';
|
|
8
8
|
const logger = createLogger('artifact-tools');
|
|
9
9
|
function createCreateHandler(phase, config, createStorage) {
|
|
10
10
|
return async (params) => {
|
|
@@ -131,7 +131,7 @@ function createDeleteHandler(phase, config, createStorage) {
|
|
|
131
131
|
}
|
|
132
132
|
export function createArtifactTools(config, createStorage) {
|
|
133
133
|
const tools = [];
|
|
134
|
-
const wdSchema =
|
|
134
|
+
const wdSchema = createWorkingDirectorySchema(getWorkingDirectoryDescription(config));
|
|
135
135
|
for (const phase of ARTIFACT_PHASES) {
|
|
136
136
|
tools.push({
|
|
137
137
|
name: `create_${phase}`,
|
|
@@ -139,10 +139,10 @@ export function createArtifactTools(config, createStorage) {
|
|
|
139
139
|
parameters: {
|
|
140
140
|
workingDirectory: wdSchema,
|
|
141
141
|
taskId: taskIdSchema.describe('The ID of the task to create the artifact for'),
|
|
142
|
-
content:
|
|
142
|
+
content: artifactContentSchema.describe(`Markdown content for the ${phase} artifact. ${PHASE_DESCRIPTIONS[phase]}`),
|
|
143
143
|
status: z.enum(['pending', 'in-progress', 'completed', 'failed', 'skipped']).optional().describe('Status of this phase (defaults to "completed")'),
|
|
144
|
-
retries:
|
|
145
|
-
error:
|
|
144
|
+
retries: retriesSchema.optional().describe('Number of retry attempts for this phase'),
|
|
145
|
+
error: artifactErrorSchema.optional().describe('Error message if status is "failed"')
|
|
146
146
|
},
|
|
147
147
|
handler: withErrorHandling(createCreateHandler(phase, config, createStorage))
|
|
148
148
|
});
|
|
@@ -152,10 +152,10 @@ export function createArtifactTools(config, createStorage) {
|
|
|
152
152
|
parameters: {
|
|
153
153
|
workingDirectory: wdSchema,
|
|
154
154
|
taskId: taskIdSchema.describe('The ID of the task to update the artifact for'),
|
|
155
|
-
content:
|
|
155
|
+
content: artifactContentSchema.optional().describe(`Updated markdown content for the ${phase} artifact`),
|
|
156
156
|
status: z.enum(['pending', 'in-progress', 'completed', 'failed', 'skipped']).optional().describe('Updated status of this phase'),
|
|
157
|
-
retries:
|
|
158
|
-
error:
|
|
157
|
+
retries: retriesSchema.optional().describe('Updated number of retry attempts'),
|
|
158
|
+
error: artifactErrorSchema.optional().describe('Updated error message')
|
|
159
159
|
},
|
|
160
160
|
handler: withErrorHandling(createUpdateHandler(phase, config, createStorage))
|
|
161
161
|
});
|
|
@@ -1,3 +1,35 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
-
export declare
|
|
2
|
+
export declare function createWorkingDirectorySchema(description?: string): z.ZodEffects<z.ZodEffects<z.ZodString, string, string>, string, string>;
|
|
3
|
+
export declare const workingDirectorySchema: z.ZodEffects<z.ZodEffects<z.ZodString, string, string>, string, string>;
|
|
4
|
+
/**
|
|
5
|
+
* Task ID schema
|
|
6
|
+
*/
|
|
3
7
|
export declare const taskIdSchema: z.ZodString;
|
|
8
|
+
/**
|
|
9
|
+
* Task details schema
|
|
10
|
+
*/
|
|
11
|
+
export declare const taskDetailsSchema: z.ZodString;
|
|
12
|
+
/**
|
|
13
|
+
* Subtask details schema
|
|
14
|
+
*/
|
|
15
|
+
export declare const subtaskDetailsSchema: z.ZodString;
|
|
16
|
+
/**
|
|
17
|
+
* Tags schema
|
|
18
|
+
*/
|
|
19
|
+
export declare const tagsSchema: z.ZodArray<z.ZodString, "many">;
|
|
20
|
+
/**
|
|
21
|
+
* Actual hours schema
|
|
22
|
+
*/
|
|
23
|
+
export declare const actualHoursSchema: z.ZodNumber;
|
|
24
|
+
/**
|
|
25
|
+
* Artifact content schema
|
|
26
|
+
*/
|
|
27
|
+
export declare const artifactContentSchema: z.ZodEffects<z.ZodString, string, string>;
|
|
28
|
+
/**
|
|
29
|
+
* Artifact error schema
|
|
30
|
+
*/
|
|
31
|
+
export declare const artifactErrorSchema: z.ZodString;
|
|
32
|
+
/**
|
|
33
|
+
* Retries schema
|
|
34
|
+
*/
|
|
35
|
+
export declare const retriesSchema: z.ZodNumber;
|
|
@@ -1,6 +1,79 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
import { ValidationLimits } from '../../../../utils/validation.js';
|
|
3
|
-
|
|
3
|
+
import { validateWorkingDirectory, containsPathTraversal } from '../../../../utils/path-security.js';
|
|
4
|
+
// Base schemas without descriptions (for internal use)
|
|
5
|
+
const baseWorkingDirectorySchema = z.string()
|
|
6
|
+
.min(1, 'Working directory is required')
|
|
7
|
+
.max(ValidationLimits.MAX_WORKING_DIRECTORY_LENGTH, 'Working directory path is too long')
|
|
8
|
+
.refine((path) => !containsPathTraversal(path), 'Working directory cannot contain path traversal sequences (..)')
|
|
9
|
+
.refine((path) => {
|
|
10
|
+
try {
|
|
11
|
+
validateWorkingDirectory(path);
|
|
12
|
+
return true;
|
|
13
|
+
}
|
|
14
|
+
catch {
|
|
15
|
+
return false;
|
|
16
|
+
}
|
|
17
|
+
}, 'Working directory must be an absolute path without traversal sequences');
|
|
18
|
+
// Export schema builder function
|
|
19
|
+
export function createWorkingDirectorySchema(description) {
|
|
20
|
+
if (description) {
|
|
21
|
+
return baseWorkingDirectorySchema.describe(description);
|
|
22
|
+
}
|
|
23
|
+
return baseWorkingDirectorySchema;
|
|
24
|
+
}
|
|
25
|
+
// For backward compatibility
|
|
26
|
+
export const workingDirectorySchema = baseWorkingDirectorySchema;
|
|
27
|
+
/**
|
|
28
|
+
* Task ID schema
|
|
29
|
+
*/
|
|
4
30
|
export const taskIdSchema = z.string()
|
|
5
31
|
.min(1, 'Task ID is required')
|
|
6
|
-
.max(ValidationLimits.TASK_ID_MAX_LENGTH);
|
|
32
|
+
.max(ValidationLimits.TASK_ID_MAX_LENGTH, `Task ID must be ${ValidationLimits.TASK_ID_MAX_LENGTH} characters or less`);
|
|
33
|
+
/**
|
|
34
|
+
* Task details schema
|
|
35
|
+
*/
|
|
36
|
+
export const taskDetailsSchema = z.string()
|
|
37
|
+
.min(ValidationLimits.TASK_DETAILS_MIN_LENGTH, 'Task details cannot be empty')
|
|
38
|
+
.max(ValidationLimits.TASK_DETAILS_MAX_LENGTH, `Task details must be ${ValidationLimits.TASK_DETAILS_MAX_LENGTH} characters or less`);
|
|
39
|
+
/**
|
|
40
|
+
* Subtask details schema
|
|
41
|
+
*/
|
|
42
|
+
export const subtaskDetailsSchema = z.string()
|
|
43
|
+
.min(1, 'Subtask details cannot be empty')
|
|
44
|
+
.max(ValidationLimits.SUBTASK_DETAILS_MAX_LENGTH, `Subtask details must be ${ValidationLimits.SUBTASK_DETAILS_MAX_LENGTH} characters or less`);
|
|
45
|
+
/**
|
|
46
|
+
* Tags schema
|
|
47
|
+
*/
|
|
48
|
+
export const tagsSchema = z.array(z.string()
|
|
49
|
+
.min(ValidationLimits.TAG_MIN_LENGTH, 'Tag cannot be empty')
|
|
50
|
+
.max(ValidationLimits.TAG_MAX_LENGTH, `Tag must be ${ValidationLimits.TAG_MAX_LENGTH} characters or less`))
|
|
51
|
+
.max(ValidationLimits.MAX_TAGS, `Cannot have more than ${ValidationLimits.MAX_TAGS} tags`);
|
|
52
|
+
/**
|
|
53
|
+
* Actual hours schema
|
|
54
|
+
*/
|
|
55
|
+
export const actualHoursSchema = z.number()
|
|
56
|
+
.min(0, 'Actual hours cannot be negative')
|
|
57
|
+
.max(ValidationLimits.MAX_ACTUAL_HOURS, `Actual hours cannot exceed ${ValidationLimits.MAX_ACTUAL_HOURS}`)
|
|
58
|
+
.finite('Actual hours must be a finite number');
|
|
59
|
+
/**
|
|
60
|
+
* Artifact content schema
|
|
61
|
+
*/
|
|
62
|
+
export const artifactContentSchema = z.string()
|
|
63
|
+
.min(1, 'Content cannot be empty')
|
|
64
|
+
.refine((content) => {
|
|
65
|
+
const byteLength = Buffer.byteLength(content, 'utf-8');
|
|
66
|
+
return byteLength <= ValidationLimits.ARTIFACT_CONTENT_MAX_LENGTH;
|
|
67
|
+
}, `Content exceeds maximum size of ${ValidationLimits.ARTIFACT_CONTENT_MAX_LENGTH} bytes (10MB)`);
|
|
68
|
+
/**
|
|
69
|
+
* Artifact error schema
|
|
70
|
+
*/
|
|
71
|
+
export const artifactErrorSchema = z.string()
|
|
72
|
+
.max(ValidationLimits.ARTIFACT_ERROR_MAX_LENGTH, `Error message cannot exceed ${ValidationLimits.ARTIFACT_ERROR_MAX_LENGTH} characters`);
|
|
73
|
+
/**
|
|
74
|
+
* Retries schema
|
|
75
|
+
*/
|
|
76
|
+
export const retriesSchema = z.number()
|
|
77
|
+
.int('Retries must be an integer')
|
|
78
|
+
.min(0, 'Retries cannot be negative')
|
|
79
|
+
.max(ValidationLimits.MAX_RETRIES, `Retries cannot exceed ${ValidationLimits.MAX_RETRIES}`);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
import { ARTIFACT_PHASES } from '../../models/artifact.js';
|
|
3
3
|
import { withErrorHandling } from '../base/handlers.js';
|
|
4
|
-
import {
|
|
4
|
+
import { createWorkingDirectorySchema } from '../base/schemas.js';
|
|
5
5
|
import { getWorkingDirectoryDescription } from '../../../../utils/storage-config.js';
|
|
6
6
|
import { createLogger } from '../../../../utils/logger.js';
|
|
7
7
|
const logger = createLogger('task-tools');
|
|
@@ -43,7 +43,7 @@ function countDoneSubtasks(task) {
|
|
|
43
43
|
* Each tool creates its own storage instance per-call using the provided factory.
|
|
44
44
|
*/
|
|
45
45
|
export function createTaskTools(config, createStorage) {
|
|
46
|
-
const wdSchema =
|
|
46
|
+
const wdSchema = createWorkingDirectorySchema(getWorkingDirectoryDescription(config));
|
|
47
47
|
return [
|
|
48
48
|
createListTasksTool(wdSchema, config, createStorage),
|
|
49
49
|
createCreateTaskTool(wdSchema, config, createStorage),
|
package/dist/utils/index.d.ts
CHANGED
package/dist/utils/index.js
CHANGED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Secure path utilities to prevent path traversal attacks
|
|
3
|
+
*/
|
|
4
|
+
/**
|
|
5
|
+
* Validate that a path doesn't contain traversal sequences
|
|
6
|
+
* Blocks paths containing .. or null bytes
|
|
7
|
+
*/
|
|
8
|
+
export declare function containsPathTraversal(filePath: string): boolean;
|
|
9
|
+
/**
|
|
10
|
+
* Securely resolve a path within a base directory
|
|
11
|
+
* Throws if the resolved path escapes the base directory
|
|
12
|
+
*
|
|
13
|
+
* @param baseDir - The allowed base directory
|
|
14
|
+
* @param targetPath - The path to resolve (can be relative)
|
|
15
|
+
* @returns The resolved path (guaranteed to be within baseDir)
|
|
16
|
+
* @throws Error if path escapes baseDir
|
|
17
|
+
*/
|
|
18
|
+
export declare function resolveSecurePath(baseDir: string, targetPath: string): string;
|
|
19
|
+
/**
|
|
20
|
+
* Validate that a working directory path is safe
|
|
21
|
+
* - Must be absolute
|
|
22
|
+
* - Must not contain traversal sequences
|
|
23
|
+
* - Must not be empty
|
|
24
|
+
*
|
|
25
|
+
* @param workingDirectory - The path to validate
|
|
26
|
+
* @returns Normalized absolute path
|
|
27
|
+
* @throws Error if path is invalid
|
|
28
|
+
*/
|
|
29
|
+
export declare function validateWorkingDirectory(workingDirectory: string): string;
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Secure path utilities to prevent path traversal attacks
|
|
3
|
+
*/
|
|
4
|
+
import { resolve, normalize, sep } from 'path';
|
|
5
|
+
/**
|
|
6
|
+
* Validate that a path doesn't contain traversal sequences
|
|
7
|
+
* Blocks paths containing .. or null bytes
|
|
8
|
+
*/
|
|
9
|
+
export function containsPathTraversal(filePath) {
|
|
10
|
+
// Check for null bytes (null byte injection)
|
|
11
|
+
if (filePath.includes('\0')) {
|
|
12
|
+
return true;
|
|
13
|
+
}
|
|
14
|
+
// Normalize the path and check if it contains parent directory references
|
|
15
|
+
const normalized = normalize(filePath);
|
|
16
|
+
// Check for .. components in the path
|
|
17
|
+
const parts = normalized.split(sep);
|
|
18
|
+
return parts.some(part => part === '..');
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Securely resolve a path within a base directory
|
|
22
|
+
* Throws if the resolved path escapes the base directory
|
|
23
|
+
*
|
|
24
|
+
* @param baseDir - The allowed base directory
|
|
25
|
+
* @param targetPath - The path to resolve (can be relative)
|
|
26
|
+
* @returns The resolved path (guaranteed to be within baseDir)
|
|
27
|
+
* @throws Error if path escapes baseDir
|
|
28
|
+
*/
|
|
29
|
+
export function resolveSecurePath(baseDir, targetPath) {
|
|
30
|
+
// Reject paths with traversal attempts
|
|
31
|
+
if (containsPathTraversal(targetPath)) {
|
|
32
|
+
throw new Error(`Path traversal detected: ${targetPath}`);
|
|
33
|
+
}
|
|
34
|
+
// Resolve the path
|
|
35
|
+
const resolvedPath = resolve(baseDir, targetPath);
|
|
36
|
+
const resolvedBase = resolve(baseDir);
|
|
37
|
+
// Ensure the resolved path is within the base directory
|
|
38
|
+
// Add trailing separator to base to prevent partial matches
|
|
39
|
+
const baseWithSep = resolvedBase.endsWith(sep) ? resolvedBase : resolvedBase + sep;
|
|
40
|
+
if (!resolvedPath.startsWith(baseWithSep) && resolvedPath !== resolvedBase) {
|
|
41
|
+
throw new Error(`Path escapes base directory: ${targetPath}`);
|
|
42
|
+
}
|
|
43
|
+
return resolvedPath;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Validate that a working directory path is safe
|
|
47
|
+
* - Must be absolute
|
|
48
|
+
* - Must not contain traversal sequences
|
|
49
|
+
* - Must not be empty
|
|
50
|
+
*
|
|
51
|
+
* @param workingDirectory - The path to validate
|
|
52
|
+
* @returns Normalized absolute path
|
|
53
|
+
* @throws Error if path is invalid
|
|
54
|
+
*/
|
|
55
|
+
export function validateWorkingDirectory(workingDirectory) {
|
|
56
|
+
if (!workingDirectory || workingDirectory.trim().length === 0) {
|
|
57
|
+
throw new Error('Working directory is required');
|
|
58
|
+
}
|
|
59
|
+
// Check for traversal sequences
|
|
60
|
+
if (containsPathTraversal(workingDirectory)) {
|
|
61
|
+
throw new Error('Working directory cannot contain path traversal sequences (..)');
|
|
62
|
+
}
|
|
63
|
+
const normalized = normalize(workingDirectory);
|
|
64
|
+
// On Windows, check for absolute path (starts with drive letter or \\)
|
|
65
|
+
// On Unix, check for absolute path (starts with /)
|
|
66
|
+
const isWindows = process.platform === 'win32';
|
|
67
|
+
const isAbsolute = isWindows
|
|
68
|
+
? /^[a-zA-Z]:[\\/]|^\\\\/.test(normalized)
|
|
69
|
+
: normalized.startsWith('/');
|
|
70
|
+
if (!isAbsolute) {
|
|
71
|
+
throw new Error('Working directory must be an absolute path');
|
|
72
|
+
}
|
|
73
|
+
return normalized;
|
|
74
|
+
}
|
|
@@ -6,4 +6,23 @@ export declare const ValidationLimits: {
|
|
|
6
6
|
readonly MAX_TAGS: 20;
|
|
7
7
|
readonly MAX_DEPENDENCIES: 50;
|
|
8
8
|
readonly TASK_ID_MAX_LENGTH: 100;
|
|
9
|
+
readonly ARTIFACT_CONTENT_MAX_LENGTH: number;
|
|
10
|
+
readonly ARTIFACT_ERROR_MAX_LENGTH: 10000;
|
|
11
|
+
readonly SUBTASK_DETAILS_MAX_LENGTH: 1000;
|
|
12
|
+
readonly MAX_ACTUAL_HOURS: 10000;
|
|
13
|
+
readonly MAX_RETRIES: 100;
|
|
14
|
+
readonly MAX_WORKING_DIRECTORY_LENGTH: 4096;
|
|
15
|
+
readonly MAX_CACHE_ENTRY_SIZE: number;
|
|
9
16
|
};
|
|
17
|
+
/**
|
|
18
|
+
* Validate that a number is within safe integer range
|
|
19
|
+
*/
|
|
20
|
+
export declare function isSafeNumber(value: number): boolean;
|
|
21
|
+
/**
|
|
22
|
+
* Validate hours value
|
|
23
|
+
*/
|
|
24
|
+
export declare function validateHours(value: number): boolean;
|
|
25
|
+
/**
|
|
26
|
+
* Validate content size
|
|
27
|
+
*/
|
|
28
|
+
export declare function validateContentSize(content: string, maxSize: number): boolean;
|
package/dist/utils/validation.js
CHANGED
|
@@ -1,9 +1,50 @@
|
|
|
1
1
|
export const ValidationLimits = {
|
|
2
|
+
// Task fields
|
|
2
3
|
TASK_DETAILS_MAX_LENGTH: 2000,
|
|
3
4
|
TASK_DETAILS_MIN_LENGTH: 1,
|
|
5
|
+
// Tags
|
|
4
6
|
TAG_MAX_LENGTH: 50,
|
|
5
7
|
TAG_MIN_LENGTH: 1,
|
|
6
8
|
MAX_TAGS: 20,
|
|
9
|
+
// Dependencies (not used in simplified model but kept for compatibility)
|
|
7
10
|
MAX_DEPENDENCIES: 50,
|
|
11
|
+
// Task ID
|
|
8
12
|
TASK_ID_MAX_LENGTH: 100,
|
|
13
|
+
// Artifact content - prevent DoS via huge content
|
|
14
|
+
ARTIFACT_CONTENT_MAX_LENGTH: 10 * 1024 * 1024, // 10MB max
|
|
15
|
+
ARTIFACT_ERROR_MAX_LENGTH: 10000, // 10KB max for error messages
|
|
16
|
+
// Subtask details
|
|
17
|
+
SUBTASK_DETAILS_MAX_LENGTH: 1000,
|
|
18
|
+
// Time tracking
|
|
19
|
+
MAX_ACTUAL_HOURS: 10000, // Max 10,000 hours (reasonable limit)
|
|
20
|
+
// Retry attempts
|
|
21
|
+
MAX_RETRIES: 100, // Reasonable max for retry attempts
|
|
22
|
+
// Working directory
|
|
23
|
+
MAX_WORKING_DIRECTORY_LENGTH: 4096, // Max path length on most systems
|
|
24
|
+
// Cache
|
|
25
|
+
MAX_CACHE_ENTRY_SIZE: 1024 * 1024, // 1MB max per cache entry
|
|
9
26
|
};
|
|
27
|
+
/**
|
|
28
|
+
* Validate that a number is within safe integer range
|
|
29
|
+
*/
|
|
30
|
+
export function isSafeNumber(value) {
|
|
31
|
+
return Number.isFinite(value) &&
|
|
32
|
+
value >= Number.MIN_SAFE_INTEGER &&
|
|
33
|
+
value <= Number.MAX_SAFE_INTEGER;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Validate hours value
|
|
37
|
+
*/
|
|
38
|
+
export function validateHours(value) {
|
|
39
|
+
return isSafeNumber(value) &&
|
|
40
|
+
value >= 0 &&
|
|
41
|
+
value <= ValidationLimits.MAX_ACTUAL_HOURS;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Validate content size
|
|
45
|
+
*/
|
|
46
|
+
export function validateContentSize(content, maxSize) {
|
|
47
|
+
// Use Buffer to get accurate byte length for unicode
|
|
48
|
+
const byteLength = Buffer.byteLength(content, 'utf-8');
|
|
49
|
+
return byteLength <= maxSize;
|
|
50
|
+
}
|
package/dist/utils/version.js
CHANGED
|
@@ -26,10 +26,10 @@ export function getVersion() {
|
|
|
26
26
|
continue;
|
|
27
27
|
}
|
|
28
28
|
}
|
|
29
|
-
return '5.0.
|
|
29
|
+
return '5.0.2'; // Fallback version — keep in sync with package.json
|
|
30
30
|
}
|
|
31
31
|
catch {
|
|
32
|
-
return '5.0.
|
|
32
|
+
return '5.0.2'; // Fallback version — keep in sync with package.json
|
|
33
33
|
}
|
|
34
34
|
}
|
|
35
35
|
/**
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@geanatz/cortex-mcp",
|
|
3
|
-
"version": "5.0.
|
|
3
|
+
"version": "5.0.3",
|
|
4
4
|
"description": "An MCP server for task-based orchestration workflows with phase artifacts for agentic development",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"type": "module",
|
|
@@ -11,7 +11,9 @@
|
|
|
11
11
|
"build": "tsc",
|
|
12
12
|
"dev": "tsc --watch",
|
|
13
13
|
"start": "node dist/index.js",
|
|
14
|
-
"prepublishOnly": "npm run build"
|
|
14
|
+
"prepublishOnly": "npm run build",
|
|
15
|
+
"test": "echo \"No tests specified\" && exit 0",
|
|
16
|
+
"lint": "echo \"No linter configured\" && exit 0"
|
|
15
17
|
},
|
|
16
18
|
"keywords": [
|
|
17
19
|
"mcp",
|
|
@@ -22,7 +24,9 @@
|
|
|
22
24
|
"file-storage",
|
|
23
25
|
"productivity",
|
|
24
26
|
"ai-tools",
|
|
25
|
-
"opencode"
|
|
27
|
+
"opencode",
|
|
28
|
+
"claude",
|
|
29
|
+
"cursor"
|
|
26
30
|
],
|
|
27
31
|
"author": "Geanatz",
|
|
28
32
|
"license": "MIT",
|
|
@@ -39,8 +43,12 @@
|
|
|
39
43
|
},
|
|
40
44
|
"repository": {
|
|
41
45
|
"type": "git",
|
|
42
|
-
"url": "https://github.com/geanatz/cortex-mcp.git"
|
|
46
|
+
"url": "git+https://github.com/geanatz/cortex-mcp.git"
|
|
43
47
|
},
|
|
48
|
+
"bugs": {
|
|
49
|
+
"url": "https://github.com/geanatz/cortex-mcp/issues"
|
|
50
|
+
},
|
|
51
|
+
"homepage": "https://github.com/geanatz/cortex-mcp#readme",
|
|
44
52
|
"files": [
|
|
45
53
|
"dist/**/*",
|
|
46
54
|
"!dist/.tsbuildinfo",
|