diff --git a/src/filesystem/README.md b/src/filesystem/README.md index 2ec65400..5552e398 100644 --- a/src/filesystem/README.md +++ b/src/filesystem/README.md @@ -81,7 +81,7 @@ The server's directory access control follows this flow: - **read_media_file** - Read an image or audio file - Input: `path` (string) - - Returns base64 data and MIME type based on the file extension + - Streams the file and returns base64 data with the corresponding MIME type - **read_multiple_files** - Read multiple files simultaneously diff --git a/src/filesystem/index.ts b/src/filesystem/index.ts index 3fc39fec..f3254bc9 100644 --- a/src/filesystem/index.ts +++ b/src/filesystem/index.ts @@ -10,6 +10,7 @@ import { type Root, } from "@modelcontextprotocol/sdk/types.js"; import fs from "fs/promises"; +import { createReadStream } from "fs"; import path from "path"; import os from 'os'; import { randomBytes } from 'crypto'; @@ -475,6 +476,21 @@ async function headFile(filePath: string, numLines: number): Promise { } } +// Stream a file and return its Base64 representation without loading the +// entire file into memory at once. Chunks are encoded individually and +// concatenated into the final string. +async function readFileAsBase64Stream(filePath: string): Promise { + return new Promise((resolve, reject) => { + const stream = createReadStream(filePath, { encoding: 'base64' }); + let data = ''; + stream.on('data', (chunk) => { + data += chunk; + }); + stream.on('end', () => resolve(data)); + stream.on('error', (err) => reject(err)); + }); +} + // Tool handlers server.setRequestHandler(ListToolsRequestSchema, async () => { return { @@ -663,7 +679,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => { ".flac": "audio/flac", }; const mimeType = mimeTypes[extension] || "application/octet-stream"; - const data = (await fs.readFile(validPath)).toString("base64"); + const data = await readFileAsBase64Stream(validPath); const type = mimeType.startsWith("image/") ? "image" : mimeType.startsWith("audio/")