Kraft102's picture
Initial deployment - WidgeTDC Cortex Backend v2.1.0
529090e
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
}
];