diff --git a/src/tools/file-operations.ts b/src/tools/file-operations.ts index 4b49f60..c24c45e 100644 --- a/src/tools/file-operations.ts +++ b/src/tools/file-operations.ts @@ -40,6 +40,30 @@ function isBinaryContent(buf: Buffer): boolean { return nonPrintable / sample.length > 0.1; } +/** Map common file extensions to MIME types */ +const MIME_TYPES: Record = { + pdf: 'application/pdf', + zip: 'application/zip', + gz: 'application/gzip', + tar: 'application/x-tar', + png: 'image/png', + jpg: 'image/jpeg', + jpeg: 'image/jpeg', + gif: 'image/gif', + webp: 'image/webp', + svg: 'image/svg+xml', + mp3: 'audio/mpeg', + mp4: 'video/mp4', + docx: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', + xlsx: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', + pptx: 'application/vnd.openxmlformats-officedocument.presentationml.presentation', +}; + +function getMimeType(filePath: string): string { + const ext = filePath.split('.').pop()?.toLowerCase() ?? ''; + return MIME_TYPES[ext] ?? 'application/octet-stream'; +} + /** * Register all file operation tools */ @@ -151,7 +175,7 @@ export async function registerFileOperationTools(server: ObsidianMCPServer): Pro // T031: Read note tool server.registerTool( 'obsidian_read_note', - 'Read the content of a note from the Obsidian vault. Specify either the note name (file) or full path (path). For large files (e.g. PDFs), use max_chars and offset to read in chunks and avoid exceeding context limits. Binary files (ZIP, images, compiled files, etc.) are automatically detected and returned as a base64-encoded string prefixed with "BASE64:" — the client must base64-decode the value to recover the original binary content.', + 'Read the content of a note from the Obsidian vault. Specify either the note name (file) or full path (path). For large files (e.g. PDFs), use max_chars and offset to read in chunks and avoid exceeding context limits. Binary files (ZIP, images, PDFs, etc.) are automatically detected and returned as an MCP embedded resource with a uri, mimeType, and base64-encoded blob field instead of plain text.', { type: 'object', properties: { @@ -174,7 +198,7 @@ export async function registerFileOperationTools(server: ObsidianMCPServer): Pro }, }, createToolHandler( - 'Read the content of a note. Binary files are returned as a base64-encoded string prefixed with "BASE64:" — decode it to recover the original binary content.', + 'Read the content of a note. Binary files are returned as an MCP embedded resource (type: "resource") with uri, mimeType, and a base64-encoded blob field.', { type: 'object', properties: { @@ -209,13 +233,21 @@ export async function registerFileOperationTools(server: ObsidianMCPServer): Pro const result = await executeObsidianCommandBinary('read', cmdArgs); handleCLIResult(result, { operation: 'read_note', identifier: sanitized.file || sanitized.path }); - // Detect binary content from the raw buffer and return as base64 + // Detect binary content from the raw buffer and return as MCP resource if (result.stdoutBuffer && isBinaryContent(result.stdoutBuffer)) { + const identifier = sanitized.file || sanitized.path as string; + const vaultName = process.env.OBSIDIAN_VAULT ?? 'vault'; + const uri = `obsidian://${encodeURIComponent(vaultName)}/${encodeURIComponent(identifier)}`; + const mimeType = getMimeType(identifier); return { content: [ { - type: 'text', - text: `BASE64:${result.stdoutBuffer.toString('base64')}`, + type: 'resource' as const, + resource: { + uri, + mimeType, + blob: result.stdoutBuffer.toString('base64'), + }, }, ], }; diff --git a/src/utils/types.ts b/src/utils/types.ts index 888eff6..4258b3a 100644 --- a/src/utils/types.ts +++ b/src/utils/types.ts @@ -16,10 +16,10 @@ export interface ToolInput { * Successful tool output */ export interface ToolOutput { - content: Array<{ - type: 'text'; - text: string; - }>; + content: Array< + | { type: 'text'; text: string } + | { type: 'resource'; resource: { uri: string; mimeType?: string; blob: string } } + >; isError?: false; }