import { Server } from '@modelcontextprotocol/sdk/server/index.js'; import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js'; import * as fs from 'fs/promises'; import * as path from 'path'; import { DROPZONE_PATH, VIDENSARKIV_PATH as CONFIG_VIDENSARKIV_PATH } from '../../config.js'; // Constants - use config paths for Docker/HF compatibility export const SAFE_DESKTOP_PATH = DROPZONE_PATH; export const SNAPSHOTS_PATH = path.join(SAFE_DESKTOP_PATH, 'snapshots'); export const VIDENSARKIV_PATH = CONFIG_VIDENSARKIV_PATH; export const ALLOWED_EXTENSIONS = ['.txt', '.md', '.json', '.csv', '.yaml', '.yml', '.xml', '.log']; interface FileInfo { name: string; path: string; size: number; modified: string; type: string; } // Helpers export async function ensureSafeZoneExists(): Promise { try { await fs.access(SAFE_DESKTOP_PATH); } catch { await fs.mkdir(SAFE_DESKTOP_PATH, { recursive: true }); console.error(`📁 Created DropZone at: ${SAFE_DESKTOP_PATH}`); } } export async function listSafeFiles(dirPath: string, recursive: boolean = false): Promise { const files: FileInfo[] = []; try { const entries = await fs.readdir(dirPath, { withFileTypes: true }); for (const entry of entries) { const fullPath = path.join(dirPath, entry.name); if (entry.isFile()) { const ext = path.extname(entry.name).toLowerCase(); if (ALLOWED_EXTENSIONS.includes(ext)) { const stats = await fs.stat(fullPath); files.push({ name: entry.name, path: fullPath.replace(VIDENSARKIV_PATH, '').replace(SAFE_DESKTOP_PATH, ''), size: stats.size, modified: stats.mtime.toISOString(), type: ext }); } } else if (entry.isDirectory() && recursive) { const subFiles = await listSafeFiles(fullPath, true); files.push(...subFiles); } } } catch (error: any) { if (error.code !== 'ENOENT') { throw error; } } return files; } // Handlers async function handleListDropzoneFiles(args: any) { const filter = args?.filter; await ensureSafeZoneExists(); const files = await listSafeFiles(SAFE_DESKTOP_PATH); const filtered = filter ? files.filter(f => f.name.endsWith(filter)) : files; return { content: [{ type: 'text', text: JSON.stringify({ path: SAFE_DESKTOP_PATH, files: filtered, count: filtered.length }, null, 2) }] }; } async function handleReadDropzoneFile(args: any) { const { filename } = args; if (!filename) { throw new Error('Filename is required'); } const safePath = path.join(SAFE_DESKTOP_PATH, path.basename(filename)); if (!safePath.startsWith(SAFE_DESKTOP_PATH)) { throw new Error('Access denied: File outside safe zone'); } const ext = path.extname(filename).toLowerCase(); if (!ALLOWED_EXTENSIONS.includes(ext)) { throw new Error(`File type not allowed: ${ext}`); } try { const content = await fs.readFile(safePath, 'utf-8'); return { content: [{ type: 'text', text: content }] }; } catch (error: any) { if (error.code === 'ENOENT') { throw new Error(`File not found: ${filename}`); } throw error; } } async function handleListVidensarkiv(args: any) { const subfolder = args?.subfolder || ''; const recursive = args?.recursive ?? false; const targetPath = path.join(VIDENSARKIV_PATH, subfolder); if (!targetPath.startsWith(VIDENSARKIV_PATH)) { throw new Error('Access denied: Path outside vidensarkiv'); } const files = await listSafeFiles(targetPath, recursive); return { content: [{ type: 'text', text: JSON.stringify({ path: targetPath, files: files, count: files.length }, null, 2) }] }; } async function handleReadVidensarkivFile(args: any) { const { filepath } = args; if (!filepath) { throw new Error('Filepath is required'); } const safePath = path.join(VIDENSARKIV_PATH, filepath); if (!safePath.startsWith(VIDENSARKIV_PATH)) { throw new Error('Access denied: Path outside vidensarkiv'); } const ext = path.extname(filepath).toLowerCase(); if (!ALLOWED_EXTENSIONS.includes(ext)) { throw new Error(`File type not allowed: ${ext}`); } try { const content = await fs.readFile(safePath, 'utf-8'); return { content: [{ type: 'text', text: content }] }; } catch (error: any) { if (error.code === 'ENOENT') { throw new Error(`File not found: ${filepath}`); } throw error; } } export function registerFileSystemTools(server: Server) { server.setRequestHandler(ListToolsRequestSchema, async (request) => { // Since we are appending to an existing list in the main server, // we can't easily "append" here if the main server handles the schema. // Instead, the main server should aggregate definitions. // But MCP SDK allows adding handlers? No, `setRequestHandler` overwrites. // Strategy: The main server handles `ListToolsRequestSchema` and returns a combined list. // We export the tool definitions here. return { tools: [] }; // Dummy return, unused by aggregator }); } export const FILE_SYSTEM_TOOLS = [ { name: 'list_dropzone_files', description: 'List files in the WidgeTDC DropZone (safe zone for file access)', inputSchema: { type: 'object', properties: { filter: { type: 'string', description: 'File extension filter (e.g., ".txt", ".json")' } } }, handler: handleListDropzoneFiles }, { name: 'read_dropzone_file', description: 'Read a file from the WidgeTDC DropZone', inputSchema: { type: 'object', properties: { filename: { type: 'string', description: 'Name of the file to read' } }, required: ['filename'] }, handler: handleReadDropzoneFile }, { name: 'list_vidensarkiv', description: 'List files in the vidensarkiv (knowledge archive)', inputSchema: { type: 'object', properties: { subfolder: { type: 'string', description: 'Subfolder path within vidensarkiv' }, recursive: { type: 'boolean', description: 'List files recursively' } } }, handler: handleListVidensarkiv }, { name: 'read_vidensarkiv_file', description: 'Read a file from the vidensarkiv (knowledge archive)', inputSchema: { type: 'object', properties: { filepath: { type: 'string', description: 'Relative path within vidensarkiv' } }, required: ['filepath'] }, handler: handleReadVidensarkivFile } ];