import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import type { z } from 'zod';
import { formatBytes } from '../config.js';
import type { FileInfo } from '../config.js';
import { DEFAULT_SEARCH_TIMEOUT_MS } from '../lib/constants.js';
import { ErrorCode } from '../lib/errors.js';
import { getMultipleFileInfo } from '../lib/file-operations/file-info.js';
import { createTimedAbortSignal } from '../lib/fs-helpers.js';
import { withToolDiagnostics } from '../lib/observability.js';
import {
GetMultipleFileInfoInputSchema,
GetMultipleFileInfoOutputSchema,
} from '../schemas.js';
import {
buildToolErrorResponse,
buildToolResponse,
type ToolExtra,
type ToolRegistrationOptions,
type ToolResponse,
type ToolResult,
withToolErrorHandling,
wrapToolHandler,
} from './shared.js';
const GET_MULTIPLE_FILE_INFO_TOOL = {
title: 'Get Multiple File Info',
description: 'Get metadata for multiple files or directories in one request.',
inputSchema: GetMultipleFileInfoInputSchema,
outputSchema: GetMultipleFileInfoOutputSchema,
annotations: {
readOnlyHint: true,
idempotentHint: true,
openWorldHint: false,
},
} as const;
interface FileInfoPayload {
name: string;
path: string;
type: FileInfo['type'];
size: number;
tokenEstimate?: number;
created: string;
modified: string;
accessed: string;
permissions: string;
isHidden: boolean;
mimeType?: string;
symlinkTarget?: string;
}
function buildFileInfoPayload(info: FileInfo): FileInfoPayload {
return {
name: info.name,
path: info.path,
type: info.type,
size: info.size,
...(info.tokenEstimate !== undefined
? { tokenEstimate: info.tokenEstimate }
: {}),
created: info.created.toISOString(),
modified: info.modified.toISOString(),
accessed: info.accessed.toISOString(),
permissions: info.permissions,
isHidden: info.isHidden,
...(info.mimeType !== undefined ? { mimeType: info.mimeType } : {}),
...(info.symlinkTarget !== undefined
? { symlinkTarget: info.symlinkTarget }
: {}),
};
}
function formatFileInfoSummary(pathValue: string, info: FileInfo): string {
return `${pathValue} (${info.type}, ${formatBytes(info.size)})`;
}
async function handleGetMultipleFileInfo(
args: z.infer<typeof GetMultipleFileInfoInputSchema>,
signal?: AbortSignal
): Promise<ToolResponse<z.infer<typeof GetMultipleFileInfoOutputSchema>>> {
const result = await getMultipleFileInfo(args.paths, {
includeMimeType: true,
...(signal ? { signal } : {}),
});
const structured: z.infer<typeof GetMultipleFileInfoOutputSchema> = {
ok: true,
results: result.results.map((entry) => ({
path: entry.path,
info: entry.info ? buildFileInfoPayload(entry.info) : undefined,
error: entry.error,
})),
summary: {
total: result.summary.total,
succeeded: result.summary.succeeded,
failed: result.summary.failed,
},
};
const text = result.results
.map((entry) => {
if (entry.error) {
return `${entry.path}: ${entry.error}`;
}
if (entry.info) {
return formatFileInfoSummary(entry.path, entry.info);
}
return entry.path;
})
.join('\n');
return buildToolResponse(text, structured);
}
export function registerGetMultipleFileInfoTool(
server: McpServer,
options: ToolRegistrationOptions = {}
): void {
const handler = (
args: z.infer<typeof GetMultipleFileInfoInputSchema>,
extra: ToolExtra
): Promise<ToolResult<z.infer<typeof GetMultipleFileInfoOutputSchema>>> => {
const primaryPath = args.paths[0] ?? '';
return withToolDiagnostics(
'stat_many',
() =>
withToolErrorHandling(
async () => {
const { signal, cleanup } = createTimedAbortSignal(
extra.signal,
DEFAULT_SEARCH_TIMEOUT_MS
);
try {
return await handleGetMultipleFileInfo(args, signal);
} finally {
cleanup();
}
},
(error) =>
buildToolErrorResponse(error, ErrorCode.E_NOT_FOUND, primaryPath)
),
{ path: primaryPath }
);
};
server.registerTool(
'stat_many',
{
...GET_MULTIPLE_FILE_INFO_TOOL,
...(options.serverIcon
? {
icons: [
{
src: options.serverIcon,
mimeType: 'image/svg+xml',
sizes: ['any'],
},
],
}
: {}),
},
wrapToolHandler(handler, { guard: options.isInitialized })
);
}