mirror of
https://github.com/modelcontextprotocol/servers.git
synced 2026-04-27 00:05:14 +02:00
Merge pull request #2382 from cliffhall/main
Add media file reading in filesystem server
This commit is contained in:
9
package-lock.json
generated
9
package-lock.json
generated
@@ -6159,7 +6159,7 @@
|
|||||||
"version": "0.6.2",
|
"version": "0.6.2",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@modelcontextprotocol/sdk": "^1.12.3",
|
"@modelcontextprotocol/sdk": "^1.17.0",
|
||||||
"diff": "^5.1.0",
|
"diff": "^5.1.0",
|
||||||
"glob": "^10.3.10",
|
"glob": "^10.3.10",
|
||||||
"minimatch": "^10.0.1",
|
"minimatch": "^10.0.1",
|
||||||
@@ -6182,9 +6182,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"src/filesystem/node_modules/@modelcontextprotocol/sdk": {
|
"src/filesystem/node_modules/@modelcontextprotocol/sdk": {
|
||||||
"version": "1.12.3",
|
"version": "1.17.0",
|
||||||
"resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.12.3.tgz",
|
"resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.17.0.tgz",
|
||||||
"integrity": "sha512-DyVYSOafBvk3/j1Oka4z5BWT8o4AFmoNyZY9pALOm7Lh3GZglR71Co4r4dEUoqDWdDazIZQHBe7J2Nwkg6gHgQ==",
|
"integrity": "sha512-qFfbWFA7r1Sd8D697L7GkTd36yqDuTkvz0KfOGkgXR8EUhQn3/EDNIR/qUdQNMT8IjmasBvHWuXeisxtXTQT2g==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"ajv": "^6.12.6",
|
"ajv": "^6.12.6",
|
||||||
@@ -6192,6 +6192,7 @@
|
|||||||
"cors": "^2.8.5",
|
"cors": "^2.8.5",
|
||||||
"cross-spawn": "^7.0.5",
|
"cross-spawn": "^7.0.5",
|
||||||
"eventsource": "^3.0.2",
|
"eventsource": "^3.0.2",
|
||||||
|
"eventsource-parser": "^3.0.0",
|
||||||
"express": "^5.0.1",
|
"express": "^5.0.1",
|
||||||
"express-rate-limit": "^7.5.0",
|
"express-rate-limit": "^7.5.0",
|
||||||
"pkce-challenge": "^5.0.0",
|
"pkce-challenge": "^5.0.0",
|
||||||
|
|||||||
@@ -70,10 +70,19 @@ The server's directory access control follows this flow:
|
|||||||
|
|
||||||
### Tools
|
### Tools
|
||||||
|
|
||||||
- **read_file**
|
- **read_text_file**
|
||||||
- Read complete contents of a file
|
- Read complete contents of a file as text
|
||||||
- Input: `path` (string)
|
- Inputs:
|
||||||
- Reads complete file contents with UTF-8 encoding
|
- `path` (string)
|
||||||
|
- `head` (number, optional): First N lines
|
||||||
|
- `tail` (number, optional): Last N lines
|
||||||
|
- Always treats the file as UTF-8 text regardless of extension
|
||||||
|
|
||||||
|
- **read_media_file**
|
||||||
|
- Read an image or audio file
|
||||||
|
- Inputs:
|
||||||
|
- `path` (string)
|
||||||
|
- Streams the file and returns base64 data with the corresponding MIME type
|
||||||
|
|
||||||
- **read_multiple_files**
|
- **read_multiple_files**
|
||||||
- Read multiple files simultaneously
|
- Read multiple files simultaneously
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import {
|
|||||||
type Root,
|
type Root,
|
||||||
} from "@modelcontextprotocol/sdk/types.js";
|
} from "@modelcontextprotocol/sdk/types.js";
|
||||||
import fs from "fs/promises";
|
import fs from "fs/promises";
|
||||||
|
import { createReadStream } from "fs";
|
||||||
import path from "path";
|
import path from "path";
|
||||||
import os from 'os';
|
import os from 'os';
|
||||||
import { randomBytes } from 'crypto';
|
import { randomBytes } from 'crypto';
|
||||||
@@ -116,12 +117,16 @@ async function validatePath(requestedPath: string): Promise<string> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Schema definitions
|
// Schema definitions
|
||||||
const ReadFileArgsSchema = z.object({
|
const ReadTextFileArgsSchema = z.object({
|
||||||
path: z.string(),
|
path: z.string(),
|
||||||
tail: z.number().optional().describe('If provided, returns only the last N lines of the file'),
|
tail: z.number().optional().describe('If provided, returns only the last N lines of the file'),
|
||||||
head: z.number().optional().describe('If provided, returns only the first N lines of the file')
|
head: z.number().optional().describe('If provided, returns only the first N lines of the file')
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const ReadMediaFileArgsSchema = z.object({
|
||||||
|
path: z.string()
|
||||||
|
});
|
||||||
|
|
||||||
const ReadMultipleFilesArgsSchema = z.object({
|
const ReadMultipleFilesArgsSchema = z.object({
|
||||||
paths: z.array(z.string()),
|
paths: z.array(z.string()),
|
||||||
});
|
});
|
||||||
@@ -471,20 +476,51 @@ async function headFile(filePath: string, numLines: number): Promise<string> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Reads a file as a stream of buffers, concatenates them, and then encodes
|
||||||
|
// the result to a Base64 string. This is a memory-efficient way to handle
|
||||||
|
// binary data from a stream before the final encoding.
|
||||||
|
async function readFileAsBase64Stream(filePath: string): Promise<string> {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const stream = createReadStream(filePath);
|
||||||
|
const chunks: Buffer[] = [];
|
||||||
|
stream.on('data', (chunk) => {
|
||||||
|
chunks.push(chunk as Buffer);
|
||||||
|
});
|
||||||
|
stream.on('end', () => {
|
||||||
|
const finalBuffer = Buffer.concat(chunks);
|
||||||
|
resolve(finalBuffer.toString('base64'));
|
||||||
|
});
|
||||||
|
stream.on('error', (err) => reject(err));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Tool handlers
|
// Tool handlers
|
||||||
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
||||||
return {
|
return {
|
||||||
tools: [
|
tools: [
|
||||||
{
|
{
|
||||||
name: "read_file",
|
name: "read_file",
|
||||||
|
description: "Read the complete contents of a file as text. DEPRECATED: Use read_text_file instead.",
|
||||||
|
inputSchema: zodToJsonSchema(ReadTextFileArgsSchema) as ToolInput,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "read_text_file",
|
||||||
description:
|
description:
|
||||||
"Read the complete contents of a file from the file system. " +
|
"Read the complete contents of a file from the file system as text. " +
|
||||||
"Handles various text encodings and provides detailed error messages " +
|
"Handles various text encodings and provides detailed error messages " +
|
||||||
"if the file cannot be read. Use this tool when you need to examine " +
|
"if the file cannot be read. Use this tool when you need to examine " +
|
||||||
"the contents of a single file. Use the 'head' parameter to read only " +
|
"the contents of a single file. Use the 'head' parameter to read only " +
|
||||||
"the first N lines of a file, or the 'tail' parameter to read only " +
|
"the first N lines of a file, or the 'tail' parameter to read only " +
|
||||||
"the last N lines of a file. Only works within allowed directories.",
|
"the last N lines of a file. Operates on the file as text regardless of extension. " +
|
||||||
inputSchema: zodToJsonSchema(ReadFileArgsSchema) as ToolInput,
|
"Only works within allowed directories.",
|
||||||
|
inputSchema: zodToJsonSchema(ReadTextFileArgsSchema) as ToolInput,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "read_media_file",
|
||||||
|
description:
|
||||||
|
"Read an image or audio file. Returns the base64 encoded data and MIME type. " +
|
||||||
|
"Only works within allowed directories.",
|
||||||
|
inputSchema: zodToJsonSchema(ReadMediaFileArgsSchema) as ToolInput,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "read_multiple_files",
|
name: "read_multiple_files",
|
||||||
@@ -597,10 +633,11 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|||||||
const { name, arguments: args } = request.params;
|
const { name, arguments: args } = request.params;
|
||||||
|
|
||||||
switch (name) {
|
switch (name) {
|
||||||
case "read_file": {
|
case "read_file":
|
||||||
const parsed = ReadFileArgsSchema.safeParse(args);
|
case "read_text_file": {
|
||||||
|
const parsed = ReadTextFileArgsSchema.safeParse(args);
|
||||||
if (!parsed.success) {
|
if (!parsed.success) {
|
||||||
throw new Error(`Invalid arguments for read_file: ${parsed.error}`);
|
throw new Error(`Invalid arguments for read_text_file: ${parsed.error}`);
|
||||||
}
|
}
|
||||||
const validPath = await validatePath(parsed.data.path);
|
const validPath = await validatePath(parsed.data.path);
|
||||||
|
|
||||||
@@ -630,6 +667,38 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
case "read_media_file": {
|
||||||
|
const parsed = ReadMediaFileArgsSchema.safeParse(args);
|
||||||
|
if (!parsed.success) {
|
||||||
|
throw new Error(`Invalid arguments for read_media_file: ${parsed.error}`);
|
||||||
|
}
|
||||||
|
const validPath = await validatePath(parsed.data.path);
|
||||||
|
const extension = path.extname(validPath).toLowerCase();
|
||||||
|
const mimeTypes: Record<string, string> = {
|
||||||
|
".png": "image/png",
|
||||||
|
".jpg": "image/jpeg",
|
||||||
|
".jpeg": "image/jpeg",
|
||||||
|
".gif": "image/gif",
|
||||||
|
".webp": "image/webp",
|
||||||
|
".bmp": "image/bmp",
|
||||||
|
".svg": "image/svg+xml",
|
||||||
|
".mp3": "audio/mpeg",
|
||||||
|
".wav": "audio/wav",
|
||||||
|
".ogg": "audio/ogg",
|
||||||
|
".flac": "audio/flac",
|
||||||
|
};
|
||||||
|
const mimeType = mimeTypes[extension] || "application/octet-stream";
|
||||||
|
const data = await readFileAsBase64Stream(validPath);
|
||||||
|
const type = mimeType.startsWith("image/")
|
||||||
|
? "image"
|
||||||
|
: mimeType.startsWith("audio/")
|
||||||
|
? "audio"
|
||||||
|
: "blob";
|
||||||
|
return {
|
||||||
|
content: [{ type, data, mimeType }],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
case "read_multiple_files": {
|
case "read_multiple_files": {
|
||||||
const parsed = ReadMultipleFilesArgsSchema.safeParse(args);
|
const parsed = ReadMultipleFilesArgsSchema.safeParse(args);
|
||||||
if (!parsed.success) {
|
if (!parsed.success) {
|
||||||
|
|||||||
@@ -20,7 +20,7 @@
|
|||||||
"test": "jest --config=jest.config.cjs --coverage"
|
"test": "jest --config=jest.config.cjs --coverage"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@modelcontextprotocol/sdk": "^1.12.3",
|
"@modelcontextprotocol/sdk": "^1.17.0",
|
||||||
"diff": "^5.1.0",
|
"diff": "^5.1.0",
|
||||||
"glob": "^10.3.10",
|
"glob": "^10.3.10",
|
||||||
"minimatch": "^10.0.1",
|
"minimatch": "^10.0.1",
|
||||||
|
|||||||
Reference in New Issue
Block a user