Skip to main content
Glama

TriliumNext Notes' MCP Server

fileManager.ts13.6 kB
/** * File Manager for TriliumNext MCP * Handles file upload workflow and integration with Trilium ETAPI */ import axios, { AxiosInstance } from 'axios'; import * as fs from 'fs'; import { validateFile, generateFileTitle, ValidationResult, SupportedMimeType, parseFileDataSource, ParsedFileData } from '../utils/fileUtils.js'; export interface CreateFileNoteParams { parentNoteId: string; filePath: string; // Can be file path, base64, or data URI title?: string; mimeType?: string; attributes?: any[]; maxSize?: number; noteType?: 'file' | 'image'; // Specify 'file' or 'image' type } export interface FileUploadOptions { title?: string; mimeType?: string; attributes?: any[]; maxSize?: number; } export interface FileInfo { noteId: string; title: string; type: 'file'; mimeType: string; contentSize: number; attachmentId?: string; fileUrl?: string; dateModified: string; blobId: string; } export interface CreateNoteResult { note: { noteId: string; title: string; type: string; mime?: string; blobId: string; }; } export class FileManager { private axiosClient: AxiosInstance; private readonly maxFileSize: number; constructor(axiosClient: AxiosInstance, maxFileSize: number = 50 * 1024 * 1024) { this.axiosClient = axiosClient; this.maxFileSize = maxFileSize; } /** * Create a file or image note following Trilium's two-step process: * 1. Create note metadata with type="file" or type="image" * 2. Upload binary content to the created note */ async createFileNote(params: CreateFileNoteParams): Promise<CreateNoteResult> { // Parse and validate file data source let fileData: ParsedFileData; try { fileData = parseFileDataSource(params.filePath); } catch (error) { throw new Error(`File data parsing failed: ${error instanceof Error ? error.message : 'Unknown error'}`); } // Validate MIME type if specified if (params.mimeType && !this.isCompatibleMimeType(params.mimeType, fileData.mimeType)) { throw new Error(`MIME type mismatch: specified ${params.mimeType}, detected ${fileData.mimeType}`); } const mimeType = params.mimeType || fileData.mimeType; const title = params.title || this.generateTitleFromFileData(fileData); // Auto-detect note type if not specified let noteType = params.noteType; if (!noteType) { const { detectNoteTypeFromMime } = await import('../utils/fileUtils.js'); const detectedType = detectNoteTypeFromMime(mimeType); if (!detectedType) { throw new Error(`Unsupported file type: ${mimeType}`); } noteType = detectedType; } // Validate file size if (fileData.size > (params.maxSize || this.maxFileSize)) { throw new Error(`File too large: ${(fileData.size / 1024 / 1024).toFixed(2)}MB. Maximum allowed: ${((params.maxSize || this.maxFileSize) / 1024 / 1024).toFixed(2)}MB`); } try { // Step 1: Create file or image note with metadata const noteData: any = { parentNoteId: params.parentNoteId, title: title, type: noteType, mime: mimeType, content: '' // Empty content initially, will be uploaded separately }; // Only include attributes if they exist and have content if (params.attributes && params.attributes.length > 0) { noteData.attributes = params.attributes; } const noteResponse = await this.axiosClient.post('/create-note', noteData); const noteResult: CreateNoteResult = noteResponse.data; // Step 2: Upload binary content await this.uploadFileContentFromData(noteResult.note.noteId, fileData, mimeType); return noteResult; } catch (error) { if (axios.isAxiosError(error)) { const status = error.response?.status; const message = error.response?.data?.message || error.message; throw new Error(`Failed to create file note (${status}): ${message}`); } throw new Error(`Failed to create file note: ${error instanceof Error ? error.message : 'Unknown error'}`); } } /** * Upload binary file content to an existing note from parsed file data * Supports file paths, base64 strings, and data URIs */ async uploadFileContentFromData(noteId: string, fileData: ParsedFileData, mimeType: string): Promise<void> { try { let fileStream: NodeJS.ReadableStream; if (fileData.type === 'path') { // Create file stream from file path if (!fs.existsSync(fileData.data as string)) { throw new Error(`File not found: ${fileData.data}`); } fileStream = fs.createReadStream(fileData.data as string); } else { // Create readable stream from buffer (base64 or data URI) const buffer = fileData.data instanceof Buffer ? fileData.data : Buffer.from(fileData.data as string, 'base64'); fileStream = this.createReadableStreamFromBuffer(buffer); } // Upload with exact headers from PDF upload guide await this.axiosClient.put(`/notes/${noteId}/content`, fileStream, { headers: { 'Content-Type': 'application/octet-stream', 'Content-Transfer-Encoding': 'binary' }, maxContentLength: Infinity, maxBodyLength: Infinity }); } catch (error) { if (axios.isAxiosError(error)) { const status = error.response?.status; const message = error.response?.data?.message || error.message; throw new Error(`Failed to upload file content (${status}): ${message}`); } throw new Error(`Failed to upload file content: ${error instanceof Error ? error.message : 'Unknown error'}`); } } /** * Upload binary file content to an existing note * Follows the exact pattern from PDF upload guide */ async uploadFileContent(noteId: string, filePath: string, mimeType: string): Promise<void> { try { // Validate file exists before upload if (!fs.existsSync(filePath)) { throw new Error(`File not found: ${filePath}`); } // Create file stream for upload const fileStream = fs.createReadStream(filePath); // Upload with exact headers from PDF upload guide await this.axiosClient.put(`/notes/${noteId}/content`, fileStream, { headers: { 'Content-Type': 'application/octet-stream', 'Content-Transfer-Encoding': 'binary' }, maxContentLength: Infinity, maxBodyLength: Infinity }); } catch (error) { if (axios.isAxiosError(error)) { const status = error.response?.status; const message = error.response?.data?.message || error.message; throw new Error(`Failed to upload file content (${status}): ${message}`); } throw new Error(`Failed to upload file content: ${error instanceof Error ? error.message : 'Unknown error'}`); } } /** * Get enhanced file information for a file note */ async getFileInfo(noteId: string): Promise<FileInfo> { try { // Get basic note information const noteResponse = await this.axiosClient.get(`/notes/${noteId}`); const note = noteResponse.data; if (note.type !== 'file') { throw new Error(`Note ${noteId} is not a file note (type: ${note.type})`); } // Try to get attachment information let attachmentInfo = null; try { // Get content to extract attachment information const contentResponse = await this.axiosClient.get(`/notes/${noteId}/content`, { headers: { 'Accept': 'text/html' } }); // Parse HTML content to extract attachment information const htmlContent = contentResponse.data; const attachmentMatch = htmlContent.match(/src="api\/attachments\/([^"]+)\/[^"]+\/([^"]+)"/); if (attachmentMatch) { const attachmentId = attachmentMatch[1]; // Get detailed attachment information const attachmentResponse = await this.axiosClient.get(`/attachments/${attachmentId}`); attachmentInfo = attachmentResponse.data; } } catch { // Attachment information might not be available immediately after upload // This is not a critical error } return { noteId: note.noteId, title: note.title, type: 'file', mimeType: note.mime || 'application/octet-stream', contentSize: attachmentInfo?.contentLength || 0, attachmentId: attachmentInfo?.attachmentId, fileUrl: attachmentInfo ? `api/attachments/${attachmentInfo.attachmentId}/document/${encodeURIComponent(note.title)}` : undefined, dateModified: note.dateModified || new Date().toISOString(), blobId: note.blobId }; } catch (error) { if (axios.isAxiosError(error)) { const status = error.response?.status; const message = error.response?.data?.message || error.message; throw new Error(`Failed to get file info (${status}): ${message}`); } throw new Error(`Failed to get file info: ${error instanceof Error ? error.message : 'Unknown error'}`); } } /** * Process complete file upload workflow * Convenience method that combines validation, creation, and upload */ async processFileUpload(parentNoteId: string, filePath: string, options?: FileUploadOptions): Promise<FileInfo> { try { // Create the file note const createResult = await this.createFileNote({ parentNoteId, filePath, title: options?.title, mimeType: options?.mimeType, attributes: options?.attributes, maxSize: options?.maxSize || this.maxFileSize }); // Get enhanced file information const fileInfo = await this.getFileInfo(createResult.note.noteId); return fileInfo; } catch (error) { throw new Error(`File upload workflow failed: ${error instanceof Error ? error.message : 'Unknown error'}`); } } /** * Validate file for upload */ private validateFileForUpload(filePath: string, maxSize: number): ValidationResult { // First validate the file exists and is accessible const validation = validateFile(filePath, maxSize); if (!validation.valid) { return validation; } return validation; } /** * Check if a note is a file note */ async isFileNote(noteId: string): Promise<boolean> { try { const noteResponse = await this.axiosClient.get(`/notes/${noteId}`); const note = noteResponse.data; return note.type === 'file'; } catch { return false; } } /** * Download file content from a file note * Note: This would require additional implementation based on Trilium's file download API */ async downloadFile(noteId: string, savePath?: string): Promise<Buffer> { try { // Get file info first const fileInfo = await this.getFileInfo(noteId); if (!fileInfo.fileUrl) { throw new Error('File URL not available for download'); } // Download the file content const downloadResponse = await this.axiosClient.get(`/${fileInfo.fileUrl}`, { responseType: 'arraybuffer' }); const fileBuffer = Buffer.from(downloadResponse.data); // Save to file if path provided if (savePath) { fs.writeFileSync(savePath, fileBuffer); } return fileBuffer; } catch (error) { if (axios.isAxiosError(error)) { const status = error.response?.status; const message = error.response?.data?.message || error.message; throw new Error(`Failed to download file (${status}): ${message}`); } throw new Error(`Failed to download file: ${error instanceof Error ? error.message : 'Unknown error'}`); } } /** * Create a readable stream from a Buffer */ private createReadableStreamFromBuffer(buffer: Buffer): NodeJS.ReadableStream { const { Readable } = require('stream'); return Readable.from(buffer); } /** * Generate title from file data */ private generateTitleFromFileData(fileData: ParsedFileData): string { if (fileData.fileName) { return generateFileTitle(fileData.fileName); } // For base64/data URI without filename, generate a generic title const extension = this.getExtensionFromMimeType(fileData.mimeType); return `Untitled File${extension ? '.' + extension : ''}`; } /** * Check MIME type compatibility */ private isCompatibleMimeType(specified: string, detected: string): boolean { // Allow exact matches if (specified === detected) { return true; } // Allow some common MIME type aliases const compatibleTypes: { [key: string]: string[] } = { 'image/jpeg': ['image/jpg'], 'audio/mpeg': ['audio/mp3'], 'application/msword': ['application/vnd.openxmlformats-officedocument.wordprocessingml.document'] }; return compatibleTypes[specified]?.includes(detected) || compatibleTypes[detected]?.includes(specified) || false; } /** * Get file extension from MIME type */ private getExtensionFromMimeType(mimeType: string): string | null { const extensions: { [key: string]: string } = { 'application/pdf': 'pdf', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document': 'docx', 'audio/mpeg': 'mp3', 'audio/wav': 'wav', 'audio/mp4': 'm4a', 'image/jpg': 'jpg', 'image/png': 'png', 'image/webp': 'webp' }; return extensions[mimeType] || null; } }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/tan-yong-sheng/triliumnext-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server