ultimate-mcp-server
Version:
The definitive all-in-one Model Context Protocol server for AI-assisted coding across 30+ platforms
282 lines • 9.51 kB
JavaScript
import fs from 'fs/promises';
import path from 'path';
import crypto from 'crypto';
import { Logger } from '../utils/logger.js';
export class FileServer {
logger;
fileCache = new Map();
filePathMap = new Map(); // path -> id mapping
maxFileSize;
cacheDuration;
constructor(options) {
this.logger = new Logger('FileServer');
this.maxFileSize = options?.maxFileSize || 10 * 1024 * 1024; // 10MB default
this.cacheDuration = options?.cacheDuration || 3600000; // 1 hour default
// Start cleanup interval
this.startCleanup();
}
/**
* Register a file for AI model access
* Returns a reference ID that AI models can use
*/
async registerFile(filePath) {
try {
// Resolve absolute path
const absolutePath = path.resolve(filePath);
// Check if already cached
if (this.filePathMap.has(absolutePath)) {
const existingId = this.filePathMap.get(absolutePath);
const existing = this.fileCache.get(existingId);
if (existing && new Date(existing.expires) > new Date()) {
this.logger.debug(`File already cached: ${absolutePath}`);
return existingId;
}
}
// Read file
const stats = await fs.stat(absolutePath);
// Check file size
if (stats.size > this.maxFileSize) {
throw new Error(`File too large: ${stats.size} bytes (max: ${this.maxFileSize})`);
}
// Read content
const content = await fs.readFile(absolutePath, 'utf-8');
// Generate ID and hash
const id = this.generateId();
const hash = this.generateHash(content);
// Determine MIME type
const mimeType = this.getMimeType(absolutePath);
// Create reference
const reference = {
id,
path: absolutePath,
content,
hash,
size: stats.size,
mimeType,
created: new Date().toISOString(),
expires: new Date(Date.now() + this.cacheDuration).toISOString()
};
// Cache the reference
this.fileCache.set(id, reference);
this.filePathMap.set(absolutePath, id);
this.logger.info(`File registered: ${absolutePath} -> ${id}`);
return id;
}
catch (error) {
this.logger.error(`Failed to register file ${filePath}:`, error);
throw error;
}
}
/**
* Register multiple files at once
*/
async registerFiles(filePaths) {
const results = new Map();
for (const filePath of filePaths) {
try {
const id = await this.registerFile(filePath);
results.set(filePath, id);
}
catch (error) {
this.logger.warn(`Skipping file ${filePath}:`, error);
}
}
return results;
}
/**
* Get file content by reference ID
*/
async getFile(referenceId) {
const reference = this.fileCache.get(referenceId);
if (!reference) {
return null;
}
// Check if expired
if (new Date(reference.expires) < new Date()) {
this.fileCache.delete(referenceId);
this.filePathMap.delete(reference.path);
return null;
}
// Refresh expiration
reference.expires = new Date(Date.now() + this.cacheDuration).toISOString();
return reference;
}
/**
* Get file by path
*/
async getFileByPath(filePath) {
const absolutePath = path.resolve(filePath);
const id = this.filePathMap.get(absolutePath);
if (!id) {
// Try to register it
try {
const newId = await this.registerFile(filePath);
return this.getFile(newId);
}
catch (error) {
return null;
}
}
return this.getFile(id);
}
/**
* Generate a shareable URL for OpenRouter models
* This creates a data URL that can be passed to models
*/
async generateDataUrl(referenceId) {
const reference = await this.getFile(referenceId);
if (!reference) {
return null;
}
// For text files, create a data URL
if (reference.mimeType.startsWith('text/') ||
reference.mimeType === 'application/json' ||
reference.mimeType === 'application/javascript') {
const base64 = Buffer.from(reference.content).toString('base64');
return `data:${reference.mimeType};base64,${base64}`;
}
// For other files, return the content directly
return reference.content;
}
/**
* Create a file manifest for AI models
* This provides a structured way for models to understand available files
*/
async createManifest() {
const files = [];
for (const [id, reference] of this.fileCache.entries()) {
// Skip expired files
if (new Date(reference.expires) < new Date()) {
continue;
}
files.push({
id,
path: reference.path,
name: path.basename(reference.path),
size: reference.size,
mimeType: reference.mimeType,
hash: reference.hash,
created: reference.created,
expires: reference.expires
});
}
return {
version: '1.0',
generated: new Date().toISOString(),
fileCount: files.length,
totalSize: files.reduce((sum, f) => sum + f.size, 0),
files
};
}
/**
* Clear expired files from cache
*/
cleanupExpired() {
const now = new Date();
const toDelete = [];
for (const [id, reference] of this.fileCache.entries()) {
if (new Date(reference.expires) < now) {
toDelete.push(id);
this.filePathMap.delete(reference.path);
}
}
for (const id of toDelete) {
this.fileCache.delete(id);
}
if (toDelete.length > 0) {
this.logger.debug(`Cleaned up ${toDelete.length} expired files`);
}
}
/**
* Start cleanup interval
*/
startCleanup() {
setInterval(() => {
this.cleanupExpired();
}, 60000); // Clean up every minute
}
/**
* Generate unique ID for file reference
*/
generateId() {
return `file-${Date.now()}-${crypto.randomBytes(8).toString('hex')}`;
}
/**
* Generate hash of file content
*/
generateHash(content) {
return crypto.createHash('sha256').update(content).digest('hex');
}
/**
* Determine MIME type from file extension
*/
getMimeType(filePath) {
const ext = path.extname(filePath).toLowerCase();
const mimeTypes = {
'.txt': 'text/plain',
'.md': 'text/markdown',
'.json': 'application/json',
'.js': 'application/javascript',
'.ts': 'application/typescript',
'.jsx': 'application/javascript',
'.tsx': 'application/typescript',
'.py': 'text/x-python',
'.java': 'text/x-java',
'.c': 'text/x-c',
'.cpp': 'text/x-c++',
'.cs': 'text/x-csharp',
'.go': 'text/x-go',
'.rs': 'text/x-rust',
'.rb': 'text/x-ruby',
'.php': 'text/x-php',
'.swift': 'text/x-swift',
'.kt': 'text/x-kotlin',
'.scala': 'text/x-scala',
'.r': 'text/x-r',
'.sql': 'text/x-sql',
'.html': 'text/html',
'.css': 'text/css',
'.xml': 'text/xml',
'.yaml': 'text/yaml',
'.yml': 'text/yaml',
'.toml': 'text/toml',
'.ini': 'text/ini',
'.env': 'text/plain',
'.sh': 'text/x-shellscript',
'.bash': 'text/x-shellscript',
'.zsh': 'text/x-shellscript',
'.fish': 'text/x-shellscript',
'.ps1': 'text/x-powershell',
'.bat': 'text/x-batch',
'.cmd': 'text/x-batch'
};
return mimeTypes[ext] || 'text/plain';
}
/**
* Get statistics about cached files
*/
getStats() {
const files = Array.from(this.fileCache.values());
const active = files.filter(f => new Date(f.expires) > new Date());
return {
totalFiles: this.fileCache.size,
activeFiles: active.length,
totalSize: active.reduce((sum, f) => sum + f.size, 0),
averageSize: active.length > 0 ?
active.reduce((sum, f) => sum + f.size, 0) / active.length : 0,
mimeTypes: active.reduce((acc, f) => {
acc[f.mimeType] = (acc[f.mimeType] || 0) + 1;
return acc;
}, {})
};
}
/**
* Clear all cached files
*/
clearCache() {
this.fileCache.clear();
this.filePathMap.clear();
this.logger.info('File cache cleared');
}
}
//# sourceMappingURL=file-server.js.map