file-organizer-dev
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 sc
Install
mkdir -p .claude/skills/file-organizer-dev && curl -L -o skill.zip "https://agentskills.codes/api/skills/download/15414" && unzip -o skill.zip -d .claude/skills/file-organizer-dev && rm skill.zipInstalls to .claude/skills/file-organizer-dev
Activation
This is the description your AI agent reads to decide when to run this skill — the better it matches your request, the more reliably it fires.
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.About this skill
File Organizer MCP - Development Guide
This skill helps developers work on the File Organizer MCP server codebase.
Quick Reference
Essential Commands
# Build
npm run build # Compile TypeScript to dist/
npm run build:watch # Watch mode
npm run clean # Remove dist/
# Test
npm test # Run all tests
npm test -- tests/unit/services/organizer.test.ts # Single test
npm run test:coverage # With coverage
npm run test:security # Security test suite
# Lint/Format
npm run lint # ESLint
npm run lint:fix # Auto-fix
npm run format # Prettier
# Dev
npm run dev # Build + start
npm run setup # TUI setup wizard
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:
/**
* File Organizer MCP Server
* my_feature Tool
*/
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';
// ==================== Schema ====================
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>;
// ==================== Tool Definition ====================
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, // true if doesn't modify files
destructiveHint: false, // true if deletes/modifies files
idempotentHint: true, // true if running twice = same result
openWorldHint: true, // true if accesses filesystem
},
};
// ==================== Handler ====================
export async function handleMyFeature(args: Record<string, unknown>): Promise<ToolResponse> {
try {
// 1. Validate input
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;
// 2. Validate path (SECURITY CRITICAL)
const validatedPath = await validateStrictPath(directory);
// 3. Call service layer
// const result = await myService.doSomething(validatedPath, some_param);
// 4. Format response
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) {
// 5. Centralized error handling
return createErrorResponse(error);
}
}
Step 2: Register in Tool Index
Edit src/tools/index.ts:
// Add export
export {
myFeatureToolDefinition,
handleMyFeature,
MyFeatureInputSchema,
} from './my-feature.js';
export type { MyFeatureInput } from './my-feature.js';
// Add to TOOLS array
export const TOOLS: ToolDefinition[] = [
// ...existing tools
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) {
// ...existing cases
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:
/**
* My Service - Business logic for X
*/
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 });
// Implementation
return result;
}
}
Security Guidelines (CRITICAL)
8-Layer Path Validation
Every file path MUST go through validation:
import { validateStrictPath } from '../services/path-validator.service.js';
// Basic validation
const validatedPath = await validateStrictPath(userInput);
// With options
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';
// Validate via file descriptor (TOCTOU protection)
const validator = new PathValidatorService();
const handle = await validator.openAndValidateFile(path);
// ... use handle ...
await handle.close();
// Atomic copy (prevents race conditions)
await fs.copyFile(source, dest, constants.COPYFILE_EXCL);
// Safe overwrite (backup first)
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)); // Windows cleanup
await fs.rm(testDir, { recursive: true, force: true });
});
it('should do something correctly', async () => {
// Arrange
const input = 'test';
// Act
const result = await service.doSomething(input);
// Assert
expect(result).toBe(expected);
});
it('should handle errors gracefully', async () => {
await expect(service.doSomething(invalidInput))
.rejects.toThrow(ExpectedError);
});
});
Security Test Pattern
describe('Securi
---
*Content truncated.*