Skip to content

Commit c9f41d2

Browse files
committed
fix(filesystem): use valid MCP content type for read_media_file
read_media_file returned content with type: "blob", which is not a valid MCP content type per the 2025-11-25 spec (only text, image, audio, resource_link, and resource are allowed). Strict clients rejected the response at the transport layer with "MCP error -32602: Invalid tools/call result". Map the resolved MIME type to a valid content type: - image/* -> { type: "image", data, mimeType } - audio/* -> { type: "audio", data, mimeType } - other -> { type: "resource", resource: { uri, mimeType, blob } } Update the tool's outputSchema to match the new union shape and add integration tests covering both the image and arbitrary-binary paths. Closes #4029.
1 parent 4503e2d commit c9f41d2

2 files changed

Lines changed: 74 additions & 11 deletions

File tree

src/filesystem/__tests__/structured-content.test.ts

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,4 +155,49 @@ describe('structuredContent schema compliance', () => {
155155
expect(Array.isArray(structuredContent.content)).toBe(false);
156156
});
157157
});
158+
159+
describe('read_media_file (issue #4029)', () => {
160+
it('returns type: "image" for image files (valid MCP content type)', async () => {
161+
// 1x1 transparent PNG (base64)
162+
const pngBase64 =
163+
'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII=';
164+
const pngPath = path.join(testDir, 'pixel.png');
165+
await fs.writeFile(pngPath, Buffer.from(pngBase64, 'base64'));
166+
167+
const result = await client.callTool({
168+
name: 'read_media_file',
169+
arguments: { path: pngPath }
170+
});
171+
172+
const content = result.content as Array<{ type: string }>;
173+
expect(Array.isArray(content)).toBe(true);
174+
expect(content[0].type).toBe('image');
175+
// 'blob' is NOT a valid MCP content type per the spec.
176+
expect(content[0].type).not.toBe('blob');
177+
});
178+
179+
it('returns type: "resource" for non-image/audio binaries (valid MCP content type)', async () => {
180+
const binPath = path.join(testDir, 'data.bin');
181+
await fs.writeFile(binPath, Buffer.from([0x00, 0x01, 0x02, 0x03]));
182+
183+
const result = await client.callTool({
184+
name: 'read_media_file',
185+
arguments: { path: binPath }
186+
});
187+
188+
const content = result.content as Array<{
189+
type: string;
190+
resource?: { uri: string; mimeType: string; blob: string };
191+
}>;
192+
expect(Array.isArray(content)).toBe(true);
193+
// Must be a valid MCP content type (text | image | audio | resource_link | resource).
194+
expect(['image', 'audio', 'resource']).toContain(content[0].type);
195+
expect(content[0].type).not.toBe('blob');
196+
if (content[0].type === 'resource') {
197+
expect(content[0].resource).toBeDefined();
198+
expect(typeof content[0].resource!.uri).toBe('string');
199+
expect(typeof content[0].resource!.blob).toBe('string');
200+
}
201+
});
202+
});
158203
});

src/filesystem/index.ts

Lines changed: 29 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -256,11 +256,21 @@ server.registerTool(
256256
path: z.string()
257257
},
258258
outputSchema: {
259-
content: z.array(z.object({
260-
type: z.enum(["image", "audio", "blob"]),
261-
data: z.string(),
262-
mimeType: z.string()
263-
}))
259+
content: z.array(z.union([
260+
z.object({
261+
type: z.enum(["image", "audio"]),
262+
data: z.string(),
263+
mimeType: z.string()
264+
}),
265+
z.object({
266+
type: z.literal("resource"),
267+
resource: z.object({
268+
uri: z.string(),
269+
mimeType: z.string(),
270+
blob: z.string()
271+
})
272+
})
273+
]))
264274
},
265275
annotations: { readOnlyHint: true }
266276
},
@@ -283,13 +293,21 @@ server.registerTool(
283293
const mimeType = mimeTypes[extension] || "application/octet-stream";
284294
const data = await readFileAsBase64Stream(validPath);
285295

286-
const type = mimeType.startsWith("image/")
287-
? "image"
296+
// Map MIME type to a valid MCP content type. The spec only allows
297+
// text, image, audio, resource_link, and resource. Non-image/audio
298+
// binaries are returned as an embedded resource so clients accept them.
299+
const contentItem = mimeType.startsWith("image/")
300+
? { type: "image" as const, data, mimeType }
288301
: mimeType.startsWith("audio/")
289-
? "audio"
290-
// Fallback for other binary types, not officially supported by the spec but has been used for some time
291-
: "blob";
292-
const contentItem = { type: type as 'image' | 'audio' | 'blob', data, mimeType };
302+
? { type: "audio" as const, data, mimeType }
303+
: {
304+
type: "resource" as const,
305+
resource: {
306+
uri: `file://${validPath}`,
307+
mimeType,
308+
blob: data
309+
}
310+
};
293311
return {
294312
content: [contentItem],
295313
structuredContent: { content: [contentItem] }

0 commit comments

Comments
 (0)