Skip to content

Commit 33e029f

Browse files
authored
Merge pull request #3033 from modelcontextprotocol/adamj/simplify-output-schemas
fix(filesystem): simplify output schemas and fix structuredContent
2 parents 55c3a31 + 3f2ddb0 commit 33e029f

File tree

1 file changed

+57
-110
lines changed

1 file changed

+57
-110
lines changed

src/filesystem/index.ts

Lines changed: 57 additions & 110 deletions
Original file line numberDiff line numberDiff line change
@@ -176,22 +176,18 @@ const readTextFileHandler = async (args: z.infer<typeof ReadTextFileArgsSchema>)
176176
throw new Error("Cannot specify both head and tail parameters simultaneously");
177177
}
178178

179+
let content: string;
179180
if (args.tail) {
180-
const tailContent = await tailFile(validPath, args.tail);
181-
return {
182-
content: [{ type: "text" as const, text: tailContent }],
183-
};
181+
content = await tailFile(validPath, args.tail);
182+
} else if (args.head) {
183+
content = await headFile(validPath, args.head);
184+
} else {
185+
content = await readFileContent(validPath);
184186
}
185187

186-
if (args.head) {
187-
const headContent = await headFile(validPath, args.head);
188-
return {
189-
content: [{ type: "text" as const, text: headContent }],
190-
};
191-
}
192-
const content = await readFileContent(validPath);
193188
return {
194189
content: [{ type: "text" as const, text: content }],
190+
structuredContent: { content }
195191
};
196192
};
197193

@@ -201,12 +197,7 @@ server.registerTool(
201197
title: "Read File (Deprecated)",
202198
description: "Read the complete contents of a file as text. DEPRECATED: Use read_text_file instead.",
203199
inputSchema: ReadTextFileArgsSchema.shape,
204-
outputSchema: {
205-
content: z.array(z.object({
206-
type: z.literal("text"),
207-
text: z.string()
208-
}))
209-
}
200+
outputSchema: { content: z.string() }
210201
},
211202
readTextFileHandler
212203
);
@@ -228,12 +219,7 @@ server.registerTool(
228219
tail: z.number().optional().describe("If provided, returns only the last N lines of the file"),
229220
head: z.number().optional().describe("If provided, returns only the first N lines of the file")
230221
},
231-
outputSchema: {
232-
content: z.array(z.object({
233-
type: z.literal("text"),
234-
text: z.string()
235-
}))
236-
}
222+
outputSchema: { content: z.string() }
237223
},
238224
readTextFileHandler
239225
);
@@ -281,8 +267,10 @@ server.registerTool(
281267
? "audio"
282268
// Fallback for other binary types, not officially supported by the spec but has been used for some time
283269
: "blob";
270+
const contentItem = { type: type as 'image' | 'audio' | 'blob', data, mimeType };
284271
return {
285-
content: [{ type, data, mimeType }],
272+
content: [contentItem],
273+
structuredContent: { content: [contentItem] }
286274
} as unknown as CallToolResult;
287275
}
288276
);
@@ -302,12 +290,7 @@ server.registerTool(
302290
.min(1)
303291
.describe("Array of file paths to read. Each path must be a string pointing to a valid file within allowed directories.")
304292
},
305-
outputSchema: {
306-
content: z.array(z.object({
307-
type: z.literal("text"),
308-
text: z.string()
309-
}))
310-
}
293+
outputSchema: { content: z.string() }
311294
},
312295
async (args: z.infer<typeof ReadMultipleFilesArgsSchema>) => {
313296
const results = await Promise.all(
@@ -322,8 +305,10 @@ server.registerTool(
322305
}
323306
}),
324307
);
308+
const text = results.join("\n---\n");
325309
return {
326-
content: [{ type: "text" as const, text: results.join("\n---\n") }],
310+
content: [{ type: "text" as const, text }],
311+
structuredContent: { content: text }
327312
};
328313
}
329314
);
@@ -340,18 +325,15 @@ server.registerTool(
340325
path: z.string(),
341326
content: z.string()
342327
},
343-
outputSchema: {
344-
content: z.array(z.object({
345-
type: z.literal("text"),
346-
text: z.string()
347-
}))
348-
}
328+
outputSchema: { content: z.string() }
349329
},
350330
async (args: z.infer<typeof WriteFileArgsSchema>) => {
351331
const validPath = await validatePath(args.path);
352332
await writeFileContent(validPath, args.content);
333+
const text = `Successfully wrote to ${args.path}`;
353334
return {
354-
content: [{ type: "text" as const, text: `Successfully wrote to ${args.path}` }],
335+
content: [{ type: "text" as const, text }],
336+
structuredContent: { content: text }
355337
};
356338
}
357339
);
@@ -372,18 +354,14 @@ server.registerTool(
372354
})),
373355
dryRun: z.boolean().default(false).describe("Preview changes using git-style diff format")
374356
},
375-
outputSchema: {
376-
content: z.array(z.object({
377-
type: z.literal("text"),
378-
text: z.string()
379-
}))
380-
}
357+
outputSchema: { content: z.string() }
381358
},
382359
async (args: z.infer<typeof EditFileArgsSchema>) => {
383360
const validPath = await validatePath(args.path);
384361
const result = await applyFileEdits(validPath, args.edits, args.dryRun);
385362
return {
386363
content: [{ type: "text" as const, text: result }],
364+
structuredContent: { content: result }
387365
};
388366
}
389367
);
@@ -400,18 +378,15 @@ server.registerTool(
400378
inputSchema: {
401379
path: z.string()
402380
},
403-
outputSchema: {
404-
content: z.array(z.object({
405-
type: z.literal("text"),
406-
text: z.string()
407-
}))
408-
}
381+
outputSchema: { content: z.string() }
409382
},
410383
async (args: z.infer<typeof CreateDirectoryArgsSchema>) => {
411384
const validPath = await validatePath(args.path);
412385
await fs.mkdir(validPath, { recursive: true });
386+
const text = `Successfully created directory ${args.path}`;
413387
return {
414-
content: [{ type: "text" as const, text: `Successfully created directory ${args.path}` }],
388+
content: [{ type: "text" as const, text }],
389+
structuredContent: { content: text }
415390
};
416391
}
417392
);
@@ -428,12 +403,7 @@ server.registerTool(
428403
inputSchema: {
429404
path: z.string()
430405
},
431-
outputSchema: {
432-
content: z.array(z.object({
433-
type: z.literal("text"),
434-
text: z.string()
435-
}))
436-
}
406+
outputSchema: { content: z.string() }
437407
},
438408
async (args: z.infer<typeof ListDirectoryArgsSchema>) => {
439409
const validPath = await validatePath(args.path);
@@ -443,6 +413,7 @@ server.registerTool(
443413
.join("\n");
444414
return {
445415
content: [{ type: "text" as const, text: formatted }],
416+
structuredContent: { content: formatted }
446417
};
447418
}
448419
);
@@ -460,12 +431,7 @@ server.registerTool(
460431
path: z.string(),
461432
sortBy: z.enum(["name", "size"]).optional().default("name").describe("Sort entries by name or size")
462433
},
463-
outputSchema: {
464-
content: z.array(z.object({
465-
type: z.literal("text"),
466-
text: z.string()
467-
}))
468-
}
434+
outputSchema: { content: z.string() }
469435
},
470436
async (args: z.infer<typeof ListDirectoryWithSizesArgsSchema>) => {
471437
const validPath = await validatePath(args.path);
@@ -521,11 +487,11 @@ server.registerTool(
521487
`Combined size: ${formatSize(totalSize)}`
522488
];
523489

490+
const text = [...formattedEntries, ...summary].join("\n");
491+
const contentBlock = { type: "text" as const, text };
524492
return {
525-
content: [{
526-
type: "text" as const,
527-
text: [...formattedEntries, ...summary].join("\n")
528-
}],
493+
content: [contentBlock],
494+
structuredContent: { content: [contentBlock] }
529495
};
530496
}
531497
);
@@ -543,12 +509,7 @@ server.registerTool(
543509
path: z.string(),
544510
excludePatterns: z.array(z.string()).optional().default([])
545511
},
546-
outputSchema: {
547-
content: z.array(z.object({
548-
type: z.literal("text"),
549-
text: z.string()
550-
}))
551-
}
512+
outputSchema: { content: z.string() }
552513
},
553514
async (args: z.infer<typeof DirectoryTreeArgsSchema>) => {
554515
interface TreeEntry {
@@ -595,11 +556,11 @@ server.registerTool(
595556
}
596557

597558
const treeData = await buildTree(rootPath, args.excludePatterns);
559+
const text = JSON.stringify(treeData, null, 2);
560+
const contentBlock = { type: "text" as const, text };
598561
return {
599-
content: [{
600-
type: "text" as const,
601-
text: JSON.stringify(treeData, null, 2)
602-
}],
562+
content: [contentBlock],
563+
structuredContent: { content: [contentBlock] }
603564
};
604565
}
605566
);
@@ -617,19 +578,17 @@ server.registerTool(
617578
source: z.string(),
618579
destination: z.string()
619580
},
620-
outputSchema: {
621-
content: z.array(z.object({
622-
type: z.literal("text"),
623-
text: z.string()
624-
}))
625-
}
581+
outputSchema: { content: z.string() }
626582
},
627583
async (args: z.infer<typeof MoveFileArgsSchema>) => {
628584
const validSourcePath = await validatePath(args.source);
629585
const validDestPath = await validatePath(args.destination);
630586
await fs.rename(validSourcePath, validDestPath);
587+
const text = `Successfully moved ${args.source} to ${args.destination}`;
588+
const contentBlock = { type: "text" as const, text };
631589
return {
632-
content: [{ type: "text" as const, text: `Successfully moved ${args.source} to ${args.destination}` }],
590+
content: [contentBlock],
591+
structuredContent: { content: [contentBlock] }
633592
};
634593
}
635594
);
@@ -649,18 +608,15 @@ server.registerTool(
649608
pattern: z.string(),
650609
excludePatterns: z.array(z.string()).optional().default([])
651610
},
652-
outputSchema: {
653-
content: z.array(z.object({
654-
type: z.literal("text"),
655-
text: z.string()
656-
}))
657-
}
611+
outputSchema: { content: z.string() }
658612
},
659613
async (args: z.infer<typeof SearchFilesArgsSchema>) => {
660614
const validPath = await validatePath(args.path);
661615
const results = await searchFilesWithValidation(validPath, args.pattern, allowedDirectories, { excludePatterns: args.excludePatterns });
616+
const text = results.length > 0 ? results.join("\n") : "No matches found";
662617
return {
663-
content: [{ type: "text" as const, text: results.length > 0 ? results.join("\n") : "No matches found" }],
618+
content: [{ type: "text" as const, text }],
619+
structuredContent: { content: text }
664620
};
665621
}
666622
);
@@ -677,20 +633,17 @@ server.registerTool(
677633
inputSchema: {
678634
path: z.string()
679635
},
680-
outputSchema: {
681-
content: z.array(z.object({
682-
type: z.literal("text"),
683-
text: z.string()
684-
}))
685-
}
636+
outputSchema: { content: z.string() }
686637
},
687638
async (args: z.infer<typeof GetFileInfoArgsSchema>) => {
688639
const validPath = await validatePath(args.path);
689640
const info = await getFileStats(validPath);
641+
const text = Object.entries(info)
642+
.map(([key, value]) => `${key}: ${value}`)
643+
.join("\n");
690644
return {
691-
content: [{ type: "text" as const, text: Object.entries(info)
692-
.map(([key, value]) => `${key}: ${value}`)
693-
.join("\n") }],
645+
content: [{ type: "text" as const, text }],
646+
structuredContent: { content: text }
694647
};
695648
}
696649
);
@@ -705,19 +658,13 @@ server.registerTool(
705658
"Use this to understand which directories and their nested paths are available " +
706659
"before trying to access files.",
707660
inputSchema: {},
708-
outputSchema: {
709-
content: z.array(z.object({
710-
type: z.literal("text"),
711-
text: z.string()
712-
}))
713-
}
661+
outputSchema: { content: z.string() }
714662
},
715663
async () => {
664+
const text = `Allowed directories:\n${allowedDirectories.join('\n')}`;
716665
return {
717-
content: [{
718-
type: "text" as const,
719-
text: `Allowed directories:\n${allowedDirectories.join('\n')}`
720-
}],
666+
content: [{ type: "text" as const, text }],
667+
structuredContent: { content: text }
721668
};
722669
}
723670
);

0 commit comments

Comments
 (0)