fix: return binary files as MCP embedded resource instead of BASE64 prefix fixes #9

The previous implementation prefixed base64 with "BASE64:" in a text
response. This updates the response to use the proper MCP embedded
resource format:

  { type: "resource", resource: { uri, mimeType, blob } }

Changes:
- types.ts: extend ToolOutput content union to allow resource items
- file-operations.ts:
  - getMimeType() maps common extensions to MIME types, falling back
    to application/octet-stream
  - MIME_TYPES table covers PDF, ZIP, images, Office formats, audio/video
  - Binary files are now returned as an EmbeddedResource with:
      uri:      obsidian://<vault>/<path>
      mimeType: detected from file extension
      blob:     base64-encoded raw bytes from the Buffer
  - Tool descriptions updated to document the resource response shape

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
2026-04-30 18:52:57 -05:00
parent 825ad133a0
commit 6ee26f1ad8
2 changed files with 41 additions and 9 deletions

View File

@@ -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<string, string> = {
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'),
},
},
],
};

View File

@@ -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;
}