Skip to main content
Glama

Decision Tree MCP Server

by psikosen
index.js10.9 kB
// index.js (Simplified for Stdio Transport) import path from 'path'; import { fileURLToPath } from 'url'; import { createClient } from 'redis'; import { z } from 'zod'; // MCP Server goodies import { Server } from '@modelcontextprotocol/sdk/server/index.js'; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import { ListToolsRequestSchema, CallToolRequestSchema } from '@modelcontextprotocol/sdk/types.js'; import { parseRTDQFile, getNodeById } from './dt_mcp.js'; // --- Configuration --- const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); const ALLOWED_RTDQ_DIRECTORY = path.resolve(process.env.RTDQ_DIR || path.join(__dirname, 'rtdq_files')); const REDIS_URL = process.env.REDIS_URL || 'redis://localhost:6379'; const REDIS_KEY_PREFIX = 'rtdq:'; const TODO_LIST_KEY = 'mcp:todos'; // --- Zod Schemas (Keep these) --- const LoadRtdqArgsSchema = z.object({ relativePath: z.string().min(1) }); const GetNodeArgsSchema = z.object({ rtdqIdentifier: z.string().min(1), taskName: z.string().min(1), nodeId: z.string().min(1) }); const AddTodoArgsSchema = z.object({ taskDescription: z.string().min(1), relatedRtdqId: z.string().optional() }); const ListTodosArgsSchema = z.object({}); const MarkTodoDoneArgsSchema = z.object({ taskId: z.string().min(1) }); // --- End Zod Schemas --- const redisClient = createClient({ url: REDIS_URL }); redisClient.on('error', (err) => console.error('Redis Client Error:', err)); async function main() { try { await redisClient.connect(); } catch (err) { console.error(`FATAL: Could not connect to Redis at ${REDIS_URL}`, err); process.exit(1); } const mcpServer = new Server( { name: 'dt_mcp_server_full', version: '1.0.0' }, { capabilities: { tools: {} } } ); mcpServer.setRequestHandler(ListToolsRequestSchema, async () => { return { tools: [ { name: 'load_rtdq_to_redis', description: 'Load/reload .rtdq file from disk into Redis cache.', inputSchema: { type: 'object', properties: { relativePath: { type: 'string', description: "Relative path within allowed dir (e.g., 'planning/trip.rtdq')" } }, required: ['relativePath'] } }, { name: 'get_dt_node_from_redis', description: 'Get node data from loaded .rtdq in Redis.', inputSchema: { type: 'object', properties: { rtdqIdentifier: { type: 'string' }, taskName: { type: 'string' }, nodeId: { type: 'string' } }, required: ['rtdqIdentifier', 'taskName', 'nodeId'] } }, { name: 'add_todo', description: 'Add task to to-do list.', inputSchema: { type: 'object', properties: { taskDescription: { type: 'string' }, relatedRtdqId: { type: 'string' } }, required: ['taskDescription'] } }, { name: 'list_todos', description: 'List active to-do items.', inputSchema: { type: 'object', properties: {} } }, { name: 'mark_todo_done', description: 'Mark a todo item as completed by ID.', inputSchema: { type: 'object', properties: { taskId: { type: 'string' } }, required: ['taskId'] } }, ], }; }); // --- Implement Tool Handlers --- mcpServer.setRequestHandler(CallToolRequestSchema, async (req) => { const { name: toolName, arguments: rawArgs } = req.params; let args; const createErrorResponse = (message, code = 'TOOL_EXECUTION_ERROR') => { console.error(`Error in tool "${toolName}": ${message}`); return { isError: true, content: [{ type: 'json', json: { error: code, message: message } }] }; }; try { // Validate Input switch (toolName) { case 'load_rtdq_to_redis': args = LoadRtdqArgsSchema.parse(rawArgs); break; case 'get_dt_node_from_redis': args = GetNodeArgsSchema.parse(rawArgs); break; case 'add_todo': args = AddTodoArgsSchema.parse(rawArgs); break; case 'list_todos': args = ListTodosArgsSchema.parse(rawArgs); break; case 'mark_todo_done': args = MarkTodoDoneArgsSchema.parse(rawArgs); break; default: throw new Error(`Tool not found: ${toolName}`); } // Execute Tool Logic if (toolName === 'load_rtdq_to_redis') { const { relativePath } = args; const absolutePath = path.resolve(ALLOWED_RTDQ_DIRECTORY, relativePath); if (!absolutePath.startsWith(ALLOWED_RTDQ_DIRECTORY) || !absolutePath.endsWith('.rtdq')) { throw new Error('Access denied or invalid file type.'); } // console.error(`Loading RTDQ: ${absolutePath}`); const parsedData = await parseRTDQFile(absolutePath); const redisKey = `${REDIS_KEY_PREFIX}${relativePath}`; await redisClient.set(redisKey, JSON.stringify(parsedData)); // console.error(`Loaded RTDQ to Redis: ${parsedData.rtdq_name} -> ${redisKey}`); return { content: [{ type: 'text', text: `Loaded .rtdq to cache: ${parsedData.rtdq_name} (ID: ${relativePath})` }] }; } else if (toolName === 'get_dt_node_from_redis') { const { rtdqIdentifier, taskName, nodeId } = args; const redisKey = `${REDIS_KEY_PREFIX}${rtdqIdentifier}`; const jsonData = await redisClient.get(redisKey); if (!jsonData) throw new Error(`RTDQ data not found in cache for ID: ${rtdqIdentifier}`); const rtdqData = JSON.parse(jsonData); const node = getNodeById(rtdqData, taskName, nodeId); if (!node) throw new Error(`Node ID "${nodeId}"/"${taskName}" not found for RTDQ "${rtdqIdentifier}".`); return { content: [{ type: 'json', json: { nodeId: node.id, prompt: node.prompt, children: node.children || [] } }] }; } else if (toolName === 'add_todo') { const { taskDescription, relatedRtdqId } = args; const taskId = `task_${Date.now()}`; const todoItem = JSON.stringify({ id: taskId, description: taskDescription, relatedRtdqId: relatedRtdqId || null, done: false }); await redisClient.lPush(TODO_LIST_KEY, todoItem); return { content: [{ type: 'text', text: `Added todo: "${taskDescription}" (ID: ${taskId})` }] }; } else if (toolName === 'list_todos') { const todoStrings = await redisClient.lRange(TODO_LIST_KEY, 0, -1); const todos = todoStrings.map(s => JSON.parse(s)).filter(t => !t.done); if (todos.length === 0) return { content: [{ type: 'text', text: "No active to-do items." }] }; const todoListText = todos.map(t => `- ${t.description} (ID: ${t.id})`).join('\n'); return { content: [{ type: 'text', text: `Active To-Dos:\n${todoListText}` }] }; } else if (toolName === 'mark_todo_done') { const { taskId } = args; const todoStrings = await redisClient.lRange(TODO_LIST_KEY, 0, -1); let updated = false; const updatedTodos = []; for (const s of todoStrings) { let todo = JSON.parse(s); if (todo.id === taskId && !todo.done) { todo.done = true; updated = true; } updatedTodos.push(JSON.stringify(todo)); } if (!updated) throw new Error(`Could not find active todo with ID: ${taskId}`); await redisClient.del(TODO_LIST_KEY); if (updatedTodos.length > 0) await redisClient.lPush(TODO_LIST_KEY, updatedTodos); return { content: [{ type: 'text', text: `Marked todo ID ${taskId} as done.` }] }; } return createErrorResponse(`Handler logic missing for tool: ${toolName}`, 'NOT_IMPLEMENTED'); } catch (error) { let errorCode = 'TOOL_EXECUTION_ERROR'; if (error instanceof z.ZodError) { errorCode = 'INVALID_INPUT'; return createErrorResponse(`Invalid input: ${error.errors.map(e=>`${e.path.join('.')}: ${e.message}`).join(', ')}`, errorCode); } else if (error.message.includes('Tool not found')) { errorCode = 'TOOL_NOT_FOUND'; } else if (error.message.includes('not found in cache')) { errorCode = 'DATA_NOT_FOUND'; } else if (error.message.includes('Node ID') && error.message.includes('not found')) { errorCode = 'NODE_NOT_FOUND'; } else if (error.message.includes('active todo') && error.message.includes('not find')) { errorCode = 'TODO_NOT_FOUND'; } else if (error instanceof SyntaxError) { errorCode = 'DATA_CORRUPTION'; error.message = `Failed to parse stored data: ${error.message}`; } else if (error.code === 'ENOENT') { errorCode = 'FILE_NOT_FOUND'; error.message = `Required file not found: ${error.message}`; } else if (error.message.includes('Access denied')) { errorCode = 'ACCESS_DENIED'; } return createErrorResponse(error.message || 'An unexpected error occurred.', errorCode); } }); // --- Connect Stdio Transport --- // Remove app.get('/mcp', ...) and app.listen(...) // Explicitly create and connect the StdioServerTransport const stdioTransport = new StdioServerTransport(); await mcpServer.connect(stdioTransport); console.error("MCP Server connected via Stdio. Awaiting client initialization."); // Log readiness to stderr // --- Keep Process Alive (or handle shutdown) --- // The process needs to stay running to listen on stdio. // The graceful shutdown handler remains important. const shutdown = async (signal) => { console.error(`Received ${signal}. Shutting down...`); try { // Optional: Add any specific cleanup for MCP server/transport if needed // stdioTransport.close(); // If transport has a close method if (redisClient.isOpen) { await redisClient.quit(); } } catch (err) { console.error("Error during shutdown:", err); } finally { process.exit(0); } }; process.on('SIGINT', () => shutdown('SIGINT')); process.on('SIGTERM', () => shutdown('SIGTERM')); // Keep Node running until signal // This basic approach keeps the process alive; more robust methods exist await new Promise(() => {}); // Keep running indefinitely until shutdown signal } main().catch(async (err) => { console.error("Fatal error during server startup:", err); if (redisClient?.isOpen) { try { await redisClient.quit(); } catch (e) { console.error("Error quitting Redis during fatal startup error:", e); } } process.exit(1); });

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/psikosen/dt_mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server