| name | file-organizer-dev |
| description | Development guide for the File Organizer MCP server codebase. Use when (1) adding new MCP tools, (2) adding new services, (3) modifying existing tools or services, (4) writing tests, (5) fixing security issues, (6) refactoring code, or (7) understanding the architecture. Provides patterns for Zod schemas, path validation, error handling, and security-hardened file operations. |
File Organizer MCP - Development Guide
This skill helps developers work on the File Organizer MCP server codebase.
Quick Reference
Essential Commands
npm run build
npm run build:watch
npm run clean
npm test
npm test -- tests/unit/services/organizer.test.ts
npm run test:coverage
npm run test:security
npm run lint
npm run lint:fix
npm run format
npm run dev
npm run setup
Project Structure
src/
├── server.ts # MCP server entry
├── index.ts # Main entry
├── types.ts # TypeScript types
├── constants.ts # App constants
├── config.ts # Config loader
├── errors.ts # Error classes
├── services/ # Business logic
│ ├── path-validator.service.ts # Security-critical
│ ├── organizer.service.ts # File organization
│ ├── file-scanner.service.ts # Directory scanning
│ ├── categorizer.service.ts # File categorization
│ ├── hash-calculator.service.ts # Duplicate detection
│ ├── rollback.service.ts # Undo operations
│ └── metadata.service.ts # EXIF/ID3 extraction
├── tools/ # MCP tool implementations
│ ├── index.ts # Tool registry
│ ├── file-organization.ts
│ ├── file-scanning.ts
│ ├── file-duplicates.ts
│ └── ...
├── schemas/ # Zod validation schemas
│ ├── common.schemas.ts
│ ├── security.schemas.ts
│ └── ...
└── utils/ # Utilities
├── logger.ts
├── error-handler.ts
├── file-utils.ts
└── formatters.ts
tests/
├── unit/services/ # Service unit tests
├── unit/tools/ # Tool unit tests
└── unit/utils/ # Utility tests
Adding a New MCP Tool
Step 1: Create Tool File
Create src/tools/my-feature.ts:
import { z } from 'zod';
import type { ToolDefinition, ToolResponse } from '../types.js';
import { validateStrictPath } from '../services/path-validator.service.js';
import { createErrorResponse } from '../utils/error-handler.js';
import { CommonParamsSchema } from '../schemas/common.schemas.js';
export const MyFeatureInputSchema = z
.object({
directory: z
.string()
.min(1, 'Directory path cannot be empty')
.describe('Full path to the directory'),
some_param: z
.boolean()
.optional()
.default(false)
.describe('Description of param'),
})
.merge(CommonParamsSchema);
export type MyFeatureInput = z.infer<typeof MyFeatureInputSchema>;
export const myFeatureToolDefinition: ToolDefinition = {
name: 'file_organizer_my_feature',
title: 'My Feature',
description: 'What this tool does. Be descriptive for LLM understanding.',
inputSchema: {
type: 'object',
properties: {
directory: { type: 'string', description: 'Full path to the directory' },
some_param: { type: 'boolean', description: 'What it does', default: false },
response_format: { type: 'string', enum: ['json', 'markdown'], default: 'markdown' },
},
required: ['directory'],
},
annotations: {
readOnlyHint: true,
destructiveHint: false,
idempotentHint: true,
openWorldHint: true,
},
};
export async function handleMyFeature(args: Record<string, unknown>): Promise<ToolResponse> {
try {
const parsed = MyFeatureInputSchema.safeParse(args);
if (!parsed.success) {
return {
content: [
{ type: 'text', text: `Error: ${parsed.error.issues.map((i) => i.message).join(', ')}` },
],
};
}
const { directory, some_param, response_format } = parsed.data;
const validatedPath = await validateStrictPath(directory);
if (response_format === 'json') {
return {
content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
structuredContent: result as unknown as Record<string, unknown>,
};
}
const markdown = `### My Feature Result
**Directory:** ${validatedPath}
**Param:** ${some_param}
Results here...`;
return {
content: [{ type: 'text', text: markdown }],
};
} catch (error) {
return createErrorResponse(error);
}
}
Step 2: Register in Tool Index
Edit src/tools/index.ts:
export {
myFeatureToolDefinition,
handleMyFeature,
MyFeatureInputSchema,
} from './my-feature.js';
export type { MyFeatureInput } from './my-feature.js';
export const TOOLS: ToolDefinition[] = [
myFeatureToolDefinition,
];
Step 3: Add to Server Router
Edit src/server.ts - add import and route:
import { handleMyFeature } from './tools/index.js';
async function handleToolCall(name: string, args: Record<string, unknown>) {
switch (name) {
case 'file_organizer_my_feature':
return handleMyFeature(args);
}
}
Step 4: Write Tests
Create tests/unit/tools/my-feature.test.ts:
import fs from 'fs/promises';
import path from 'path';
import os from 'os';
import { handleMyFeature } from '../../../src/tools/my-feature.js';
describe('handleMyFeature', () => {
let testDir: string;
beforeEach(async () => {
testDir = await fs.mkdtemp(path.join(os.tmpdir(), 'test-myfeature-'));
});
afterEach(async () => {
await fs.rm(testDir, { recursive: true, force: true });
});
it('should process directory successfully', async () => {
const result = await handleMyFeature({
directory: testDir,
some_param: true,
response_format: 'json',
});
expect(result.content[0].text).toContain('expected content');
});
it('should reject invalid paths', async () => {
const result = await handleMyFeature({
directory: '/invalid/path',
response_format: 'json',
});
expect(result.content[0].text).toContain('Error');
});
});
Adding a New Service
Create src/services/my-service.service.ts:
import type { SomeType } from '../types.js';
import { logger } from '../utils/logger.js';
export interface MyServiceOptions {
option1?: boolean;
option2?: number;
}
export class MyService {
constructor(private options: MyServiceOptions = {}) {}
async doSomething(input: string): Promise<SomeType> {
logger.debug('Doing something', { input });
return result;
}
}
Security Guidelines (CRITICAL)
8-Layer Path Validation
Every file path MUST go through validation:
import { validateStrictPath } from '../services/path-validator.service.js';
const validatedPath = await validateStrictPath(userInput);
import { validatePathBase } from '../services/path-validator.service.js';
const path = await validatePathBase(input, {
basePath: '/base',
allowedPaths: ['/allowed'],
requireExists: true,
checkWrite: true,
allowSymlinks: false,
});
Security Rules
- Never trust user paths - Always validate
- Use O_NOFOLLOW - Prevent symlink attacks
- Atomic operations - Use COPYFILE_EXCL for race condition safety
- No path traversal - Block
../ sequences
- Windows reserved names - Block CON, PRN, AUX, NUL, COM1-9, LPT1-9
- Sanitize errors - Never expose internal paths in error messages
Safe File Operations
import { constants } from 'fs';
const validator = new PathValidatorService();
const handle = await validator.openAndValidateFile(path);
await handle.close();
await fs.copyFile(source, dest, constants.COPYFILE_EXCL);
if (await fileExists(targetPath)) {
const backupPath = path.join('.file-organizer-backups', `${Date.now()}_${basename}`);
await fs.rename(targetPath, backupPath);
}
Testing Patterns
Unit Test Template
import { jest } from '@jest/globals';
import fs from 'fs/promises';
import path from 'path';
import os from 'os';
import { MyService } from '../../../src/services/my-service.service.js';
describe('MyService', () => {
let service: MyService;
let testDir: string;
beforeEach(async () => {
testDir = await fs.mkdtemp(path.join(os.tmpdir(), 'test-'));
service = new MyService();
});
afterEach(async () => {
await new Promise((resolve) => setTimeout(resolve, 100));
await fs.rm(testDir, { recursive: true, force: true });
});
it('should do something correctly', async () => {
const input = 'test';
const result = await service.doSomething(input);
expect(result).toBe(expected);
});
it('should handle errors gracefully', async () => {
await expect(service.doSomething(invalidInput))
.rejects.toThrow(ExpectedError);
});
});
Security Test Pattern
describe('Security', () => {
it('should block path traversal', async () => {
const result = await handleTool({
directory: '/allowed/../etc/passwd',
});
expect(result.content[0].text).toContain('Error');
});
it('should block symlinks outside allowed paths', async () => {
});
});
Code Style
Naming Conventions
| Type | Convention | Example |
|---|
| Files | kebab-case | path-validator.service.ts |
| Classes | PascalCase | PathValidatorService |
| Functions | camelCase | validatePath() |
| Constants | SCREAMING_SNAKE | MAX_FILE_SIZE |
| Interfaces | PascalCase | ToolResponse |
TypeScript Imports
import fs from 'fs/promises';
import { constants } from 'fs';
import path from 'path';
import { z } from 'zod';
import { validateStrictPath } from '../services/path-validator.service.js';
import type { ToolResponse } from '../types.js';
Zod Schema Pattern
export const MyInputSchema = z
.object({
directory: z.string().min(1, 'Directory cannot be empty'),
dry_run: z.boolean().optional().default(false),
limit: z.number().int().positive().max(1000).default(100),
})
.merge(CommonParamsSchema);
export type MyInput = z.infer<typeof MyInputSchema>;
Error Handling Pattern
import { createErrorResponse } from '../utils/error-handler.js';
import { ValidationError } from '../types.js';
try {
} catch (error) {
if (error instanceof ValidationError) {
}
return createErrorResponse(error);
}
Common Utilities
Logger
import { logger } from '../utils/logger.js';
logger.debug('Debug message', { context: 'value' });
logger.info('Info message');
logger.warn('Warning', { detail: 'value' });
logger.error('Error message', error, { context: 'value' });
File Utilities
import { fileExists, formatBytes } from '../utils/file-utils.js';
if (await fileExists(path)) { }
const readable = formatBytes(1024);
Formatters
import { formatDate, pluralize } from '../utils/formatters.js';
formatDate(new Date());
pluralize(5, 'file');
Key Types Reference
File Types
interface FileWithSize {
name: string;
path: string;
size: number;
modified?: Date;
}
type CategoryName =
| 'Executables' | 'Videos' | 'Documents' | 'Presentations'
| 'Spreadsheets' | 'Images' | 'Audio' | 'Archives'
| 'Code' | 'Installers' | 'Ebooks' | 'Fonts' | 'Others';
Tool Types
interface ToolDefinition {
name: string;
description: string;
inputSchema: {
type: 'object';
properties: Record<string, unknown>;
required: string[];
};
annotations?: {
readOnlyHint?: boolean;
destructiveHint?: boolean;
idempotentHint?: boolean;
openWorldHint?: boolean;
};
}
type ToolResponse = {
content: Array<{ type: 'text'; text: string }>;
[key: string]: unknown;
};
Debugging Tips
Enable Debug Logging
Set environment variable:
$env:LOG_LEVEL = "debug"
Common Issues
| Issue | Solution |
|---|
| Path validation fails | Check config.json allowed directories |
| Windows file locks | Add 100ms delay before cleanup in tests |
| ESM import errors | Use .js extension in imports |
| Type errors | Run npm run build to check |
| Test timeouts | Check for unclosed file handles |
Development Workflow
- Create feature branch
- Implement changes following patterns above
- Add tests for new functionality
- Run full test suite:
npm test
- Run lint:
npm run lint
- Build:
npm run build
- Test manually:
npm run dev
- Security review (for path handling changes)