How to Build Custom File Handlers for Unsupported File Types in Desktop Commander MCP
You can extend Desktop Commander MCP to support any file type by implementing the FileHandler interface defined in src/utils/files/base.ts, registering your handler in the factory at src/utils/files/factory.ts, and optionally adding a UI preview component in src/ui/file-preview/src/file-type-handlers.ts.
Desktop Commander MCP processes files through a plugin-style handler system that abstracts all file-type-specific logic behind a common interface. This architecture means that adding support for new formats—whether proprietary data files, custom configuration formats, or specialized media types—requires no changes to the core filesystem tools in src/tools/filesystem.ts or editing logic in src/tools/edit.ts. Instead, you implement a single class and hook it into the factory pattern.
Understanding the FileHandler Interface
All file handlers must implement the FileHandler interface exported from src/utils/files/base.ts. This contract ensures that every handler can read, write, and provide metadata for its supported file types.
export interface FileHandler {
read(path: string, options?: ReadOptions): Promise<FileResult>;
write(path: string, content: any, mode?: 'rewrite' | 'append'): Promise<void>;
getInfo(path: string): Promise<FileInfo>;
canHandle(path: string): boolean | Promise<boolean>;
editRange?(path: string, range: string, content: any, options?: Record<string, any>): Promise<EditResult>;
}
The canHandle() method determines whether a handler accepts a given file path, typically by checking file extensions or performing content-based detection. The read() and write() methods handle the actual I/O operations, while getInfo() returns filesystem metadata. The optional editRange() method enables partial file editing capabilities for supported formats.
The Factory Pattern and Handler Priority
The src/utils/files/factory.ts file contains a factory function getFileHandler() that lazily instantiates handler singletons and selects the appropriate one for each file operation. Handlers are checked in a fixed priority order, allowing specific handlers to take precedence over generic fallbacks like the text handler.
When you add a custom handler, you must insert it into this priority chain. The factory uses lazy initialization functions (e.g., getImageHandler(), getExcelHandler()) to avoid instantiating handlers until they are first needed, improving startup performance.
Step-by-Step: Creating a Custom Handler
Follow these four steps to add support for a new file type. The example below demonstrates adding a Markdown handler for .md files.
Step 1: Implement the Handler Class
Create a new file at src/utils/files/markdown.ts that implements the FileHandler interface:
import fs from "fs/promises";
import { FileHandler, ReadOptions, FileResult, FileInfo } from './base.js';
export class MarkdownFileHandler implements FileHandler {
private static readonly EXT = ['.md'];
private static readonly MIME = 'text/markdown';
canHandle(path: string): boolean {
const lower = path.toLowerCase();
return MarkdownFileHandler.EXT.some(ext => lower.endsWith(ext));
}
async read(path: string, options?: ReadOptions): Promise<FileResult> {
const content = await fs.readFile(path, { encoding: 'utf8', signal: options?.signal });
return { content, mimeType: MarkdownFileHandler.MIME, metadata: {} };
}
async write(path: string, content: string, mode: 'rewrite' | 'append' = 'rewrite'): Promise<void> {
if (mode === 'append') await fs.appendFile(path, content);
else await fs.writeFile(path, content);
}
async getInfo(path: string): Promise<FileInfo> {
const stats = await fs.stat(path);
return {
size: stats.size,
created: stats.birthtime,
modified: stats.mtime,
accessed: stats.atime,
isDirectory: stats.isDirectory(),
isFile: stats.isFile(),
permissions: stats.mode.toString(8).slice(-3),
fileType: 'text',
metadata: {}
};
}
}
Step 2: Export from the Index
Expose your handler from src/utils/files/index.ts so the factory can import it:
export { getFileHandler, isExcelFile, isImageFile } from './factory.js';
export { MarkdownFileHandler } from './markdown.js';
Step 3: Register in the Factory
Modify src/utils/files/factory.ts to add lazy initialization and insert your handler into the priority chain:
import { MarkdownFileHandler } from './markdown.js';
let markdownHandler: MarkdownFileHandler | null = null;
function getMarkdownHandler(): MarkdownFileHandler {
if (!markdownHandler) markdownHandler = new MarkdownFileHandler();
return markdownHandler;
}
// Inside getFileHandler() - insert before generic text/binary handlers
if (getMarkdownHandler().canHandle(filePath)) {
return getMarkdownHandler();
}
Step 4: Add UI Preview Support (Optional)
To enable custom preview panels for your file type, register a handler in src/ui/file-preview/src/file-type-handlers.ts:
markdown: {
getCapabilities: (payload) => buildPreviewCapabilities(payload, false),
renderBody: ({ payload }) => {
return renderHtmlPreview(payload.content, 'full');
},
},
Complete Example: YAML File Handler
Here is a minimal, production-ready handler for YAML files that follows the same pattern:
File: src/utils/files/yaml.ts
import fs from "fs/promises";
import { FileHandler, ReadOptions, FileResult, FileInfo } from "./base.js";
export class YamlFileHandler implements FileHandler {
private static readonly EXT = [".yaml", ".yml"];
private static readonly MIME = "application/x-yaml";
canHandle(p: string): boolean {
const low = p.toLowerCase();
return YamlFileHandler.EXT.some(e => low.endsWith(e));
}
async read(p: string, opts?: ReadOptions): Promise<FileResult> {
const txt = await fs.readFile(p, { encoding: "utf8", signal: opts?.signal });
return { content: txt, mimeType: YamlFileHandler.MIME, metadata: {} };
}
async write(p: string, cnt: string, mode: "rewrite" | "append" = "rewrite"): Promise<void> {
if (mode === "append") await fs.appendFile(p, cnt);
else await fs.writeFile(p, cnt);
}
async getInfo(p: string): Promise<FileInfo> {
const s = await fs.stat(p);
return {
size: s.size,
created: s.birthtime,
modified: s.mtime,
accessed: s.atime,
isDirectory: s.isDirectory(),
isFile: s.isFile(),
permissions: s.mode.toString(8).slice(-3),
fileType: "text",
metadata: {}
};
}
}
Registration in src/utils/files/factory.ts:
import { YamlFileHandler } from "./yaml.js";
let yamlHandler: YamlFileHandler | null = null;
function getYamlHandler(): YamlFileHandler {
if (!yamlHandler) yamlHandler = new YamlFileHandler();
return yamlHandler;
}
// Inside getFileHandler()
if (getYamlHandler().canHandle(filePath)) {
return getYamlHandler();
}
UI Preview in src/ui/file-preview/src/file-type-handlers.ts:
yaml: {
getCapabilities: (p) => buildPreviewCapabilities(p, true),
renderBody: ({ payload }) => ({
notice: "",
html: `<pre class="code-viewer"><code class="hljs language-yaml">${escapeHtml(payload.content)}</code></pre>`
})
},
Summary
- Interface-driven design: All handlers implement
FileHandlerfromsrc/utils/files/base.ts, ensuring type safety and consistent behavior across read, write, and metadata operations. - Factory registration: Add your handler to
src/utils/files/factory.tsusing lazy singleton initialization and insert it into the priority chain before generic fallbacks. - Zero core changes: Once registered, existing tools in
src/tools/filesystem.tsandsrc/tools/edit.tsautomatically work with your new file type through the abstract interface. - Optional UI support: Extend
src/ui/file-preview/src/file-type-handlers.tsto provide custom preview renderers for your file type.
Frequently Asked Questions
What methods are required when implementing a custom file handler?
You must implement read(), write(), getInfo(), and canHandle() from the FileHandler interface in src/utils/files/base.ts. The editRange() method is optional and only needed if your file type supports partial content editing. All methods must handle their own error conditions and return properly typed promises according to the FileResult and FileInfo interfaces.
How does Desktop Commander decide which handler to use for a file?
The getFileHandler() function in src/utils/files/factory.ts checks handlers in a fixed priority order by calling each handler's canHandle() method until one returns true. You should register specific handlers (like those for .yaml or .md) before generic handlers (like the text or binary fallbacks) to ensure proper type detection.
Can I add support for file types that don't use standard extensions?
Yes. While the examples show extension-based detection using path.toLowerCase().endsWith(), you can implement any logic inside canHandle(). For content-based detection, make canHandle() asynchronous and read the file header or perform content sniffing to determine compatibility. The factory supports both synchronous and asynchronous canHandle() implementations.
Do I need to modify the MCP tool definitions to use a new handler?
No. The filesystem tools in src/tools/filesystem.ts and editing tools in src/tools/edit.ts interact exclusively with the FileHandler abstraction. Once your handler is registered in the factory, these tools automatically route file operations to your implementation without requiring changes to their code or the MCP server configuration.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →