Spaces:
Paused
Paused
| 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<void> { | |
| 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<FileInfo[]> { | |
| 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 | |
| } | |
| ]; | |