Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 39 additions & 0 deletions src/filesystem/__tests__/structured-content.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ describe('structuredContent schema compliance', () => {

// Create test files
await fs.writeFile(path.join(testDir, 'test.txt'), 'test content');
await fs.writeFile(path.join(testDir, 'image.png'), Buffer.from('test image data'));
await fs.writeFile(path.join(testDir, 'archive.bin'), Buffer.from('test binary data'));
await fs.mkdir(path.join(testDir, 'subdir'));
await fs.writeFile(path.join(testDir, 'subdir', 'nested.txt'), 'nested content');

Expand Down Expand Up @@ -155,4 +157,41 @@ describe('structuredContent schema compliance', () => {
expect(Array.isArray(structuredContent.content)).toBe(false);
});
});

describe('read_media_file', () => {
it('should return spec-compliant image content for image files', async () => {
const result = await client.callTool({
name: 'read_media_file',
arguments: { path: path.join(testDir, 'image.png') }
});

expect(result.isError).not.toBe(true);
expect(result.content).toHaveLength(1);
expect(result.content[0]).toMatchObject({
type: 'image',
data: Buffer.from('test image data').toString('base64'),
mimeType: 'image/png'
});

const structuredContent = result.structuredContent as { content: unknown[] };
expect(structuredContent.content[0]).toMatchObject({
type: 'image',
mimeType: 'image/png'
});
});

it('should reject non-media files instead of returning an invalid blob content type', async () => {
const result = await client.callTool({
name: 'read_media_file',
arguments: { path: path.join(testDir, 'archive.bin') }
});

expect(result.isError).toBe(true);
expect(result.content).toHaveLength(1);
expect(result.content[0]).toMatchObject({
type: 'text'
});
expect(JSON.stringify(result)).not.toContain('"blob"');
});
});
});
25 changes: 19 additions & 6 deletions src/filesystem/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -257,7 +257,7 @@ server.registerTool(
},
outputSchema: {
content: z.array(z.object({
type: z.enum(["image", "audio", "blob"]),
type: z.enum(["image", "audio"]),
data: z.string(),
mimeType: z.string()
}))
Expand All @@ -280,16 +280,29 @@ server.registerTool(
".ogg": "audio/ogg",
".flac": "audio/flac",
};
const mimeType = mimeTypes[extension] || "application/octet-stream";
const data = await readFileAsBase64Stream(validPath);
const mimeType = mimeTypes[extension];
if (!mimeType) {
return {
isError: true,
content: [{ type: "text" as const, text: `Unsupported media type: ${extension || "unknown extension"}. read_media_file only supports image and audio files.` }]
};
}

const type = mimeType.startsWith("image/")
? "image"
: mimeType.startsWith("audio/")
? "audio"
// Fallback for other binary types, not officially supported by the spec but has been used for some time
: "blob";
const contentItem = { type: type as 'image' | 'audio' | 'blob', data, mimeType };
: undefined;

if (!type) {
return {
isError: true,
content: [{ type: "text" as const, text: `Unsupported media MIME type: ${mimeType}. read_media_file only supports image and audio files.` }]
};
}

const data = await readFileAsBase64Stream(validPath);
const contentItem = { type, data, mimeType };
return {
content: [contentItem],
structuredContent: { content: [contentItem] }
Expand Down
Loading