Slowtime MCP Server

by bmorphism
Verified
#!/usr/bin/env node import { Server } from '@modelcontextprotocol/sdk/server/index.js'; import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; import { CallToolRequestSchema, ErrorCode, ListToolsRequestSchema, McpError, } from '@modelcontextprotocol/sdk/types.js'; import { timekeeper } from './timekeeper.js'; import { timeLockManager } from './timelock.js'; import { timeVault } from './timevault.js'; const server = new Server( { name: 'slowtime-server', version: '1.0.0', }, { capabilities: { tools: {}, }, } ); // List available tools server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: [ { name: 'list_vault_history', description: 'List historical timevault entries with filtering options', inputSchema: { type: 'object', properties: { interval_id: { type: 'string', description: 'Filter by interval ID', }, decrypted_only: { type: 'boolean', description: 'Show only decrypted entries', }, limit: { type: 'number', description: 'Maximum number of entries to return', }, offset: { type: 'number', description: 'Number of entries to skip', }, }, }, }, { name: 'get_vault_stats', description: 'Get statistics about timevault usage', inputSchema: { type: 'object', properties: {}, }, }, { name: 'encrypt_with_timelock', description: 'Encrypt data that can only be decrypted after a specified interval', inputSchema: { type: 'object', properties: { data: { type: 'string', description: 'Data to encrypt', }, interval_id: { type: 'string', description: 'ID of the interval to use for encryption duration', }, }, required: ['data', 'interval_id'], }, }, { name: 'decrypt_timelock', description: 'Attempt to decrypt time-locked data', inputSchema: { type: 'object', properties: { timelock_id: { type: 'string', description: 'ID of the timelock to decrypt', }, }, required: ['timelock_id'], }, }, { name: 'list_timelocks', description: 'List all timelocks and their status', inputSchema: { type: 'object', properties: {}, }, }, { name: 'start_interval', description: 'Start a new slowtime interval', inputSchema: { type: 'object', properties: { label: { type: 'string', description: 'Label for the interval', }, duration: { type: 'number', description: 'Duration in minutes', }, }, required: ['label', 'duration'], }, }, { name: 'check_interval', description: 'Check the status of an interval', inputSchema: { type: 'object', properties: { id: { type: 'string', description: 'Interval ID', }, }, required: ['id'], }, }, { name: 'list_intervals', description: 'List all intervals', inputSchema: { type: 'object', properties: { status: { type: 'string', enum: ['all', 'active', 'completed'], description: 'Filter intervals by status', }, }, }, }, { name: 'pause_interval', description: 'Pause an active interval', inputSchema: { type: 'object', properties: { id: { type: 'string', description: 'Interval ID', }, }, required: ['id'], }, }, { name: 'resume_interval', description: 'Resume a paused interval', inputSchema: { type: 'object', properties: { id: { type: 'string', description: 'Interval ID', }, }, required: ['id'], }, }, ], })); // Handle tool execution server.setRequestHandler(CallToolRequestSchema, async (request) => { const { name, arguments: args = {} } = request.params; try { switch (name) { case 'start_interval': { const label = args.label as string | undefined; const duration = args.duration as number | undefined; if (!label || typeof duration !== 'number') { throw new McpError(ErrorCode.InvalidParams, 'Invalid parameters'); } const interval = timekeeper.createInterval({ label, duration: duration * 60 * 1000, // Convert minutes to milliseconds }); const progress = await timekeeper.getIntervalProgress(interval); return { content: [ { type: 'text', text: `Started interval "${progress.label}" (ID: ${progress.id})\nDuration: ${args.duration} minutes\nStatus: ${progress.status}`, }, ], }; } case 'check_interval': { const id = args.id as string | undefined; if (!id) { throw new McpError(ErrorCode.InvalidParams, 'Missing interval ID'); } const interval = timekeeper.getInterval(id); if (!interval) { throw new McpError(ErrorCode.InvalidParams, 'Interval not found'); } const progress = await timekeeper.getIntervalProgress(interval); const remainingMinutes = Math.ceil(progress.remainingTime / (60 * 1000)); const progressPercent = Math.round(progress.progress * 100); return { content: [ { type: 'text', text: `Interval "${progress.label}" (ID: ${progress.id})\nStatus: ${progress.status}\nProgress: ${progressPercent}%\nRemaining time: ${remainingMinutes} minutes`, }, ], }; } case 'list_intervals': { const status = (args.status as string | undefined) || 'all'; let intervals; switch (status) { case 'active': intervals = await timekeeper.listActiveIntervals(); break; case 'completed': intervals = await timekeeper.listCompletedIntervals(); break; default: intervals = await timekeeper.listIntervals(); } if (intervals.length === 0) { return { content: [ { type: 'text', text: 'No intervals found', }, ], }; } const intervalList = intervals.map(interval => { const remainingMinutes = Math.ceil(interval.remainingTime / (60 * 1000)); const progressPercent = Math.round(interval.progress * 100); return `- "${interval.label}" (ID: ${interval.id})\n Status: ${interval.status}\n Progress: ${progressPercent}%\n Remaining: ${remainingMinutes} minutes`; }).join('\n\n'); return { content: [ { type: 'text', text: intervalList, }, ], }; } case 'pause_interval': { const id = args.id as string | undefined; if (!id) { throw new McpError(ErrorCode.InvalidParams, 'Missing interval ID'); } const success = await timekeeper.pauseInterval(id); if (!success) { throw new McpError(ErrorCode.InvalidParams, 'Failed to pause interval'); } const interval = timekeeper.getInterval(id); if (!interval) { throw new McpError(ErrorCode.InvalidParams, 'Interval not found'); } const progress = await timekeeper.getIntervalProgress(interval); return { content: [ { type: 'text', text: `Paused interval "${progress.label}" (ID: ${progress.id})`, }, ], }; } case 'resume_interval': { const id = args.id as string | undefined; if (!id) { throw new McpError(ErrorCode.InvalidParams, 'Missing interval ID'); } const success = await timekeeper.resumeInterval(id); if (!success) { throw new McpError(ErrorCode.InvalidParams, 'Failed to resume interval'); } const interval = timekeeper.getInterval(id); if (!interval) { throw new McpError(ErrorCode.InvalidParams, 'Interval not found'); } const progress = await timekeeper.getIntervalProgress(interval); return { content: [ { type: 'text', text: `Resumed interval "${progress.label}" (ID: ${progress.id})`, }, ], }; } case 'encrypt_with_timelock': { const data = args.data as string | undefined; const intervalId = args.interval_id as string | undefined; if (!data || !intervalId) { throw new McpError(ErrorCode.InvalidParams, 'Missing required parameters'); } const interval = timekeeper.getInterval(intervalId); if (!interval) { throw new McpError(ErrorCode.InvalidParams, 'Interval not found'); } const progress = await timekeeper.getIntervalProgress(interval); const remainingTime = progress.remainingTime; try { const timeLock = await timeLockManager.encryptForInterval(data, remainingTime); return { content: [ { type: 'text', text: `Data encrypted with timelock (ID: ${timeLock.id})\nWill be decryptable when interval "${interval.label}" completes`, }, ], }; } catch (error) { throw new McpError(ErrorCode.InternalError, `Encryption failed: ${error}`); } } case 'decrypt_timelock': { const timelockId = args.timelock_id as string | undefined; if (!timelockId) { throw new McpError(ErrorCode.InvalidParams, 'Missing timelock ID'); } try { const decrypted = await timeLockManager.attemptDecryption(timelockId); if (decrypted === null) { return { content: [ { type: 'text', text: 'The data is not yet decryptable. Please wait until the associated interval completes.', }, ], }; } return { content: [ { type: 'text', text: `Decrypted data: ${decrypted}`, }, ], }; } catch (error) { if (error instanceof Error && error.message === 'TimeLock not found') { throw new McpError(ErrorCode.InvalidParams, 'TimeLock not found'); } throw new McpError(ErrorCode.InternalError, `Decryption failed: ${error}`); } } case 'list_timelocks': { const timelocks = timeLockManager.listTimeLocks(); if (timelocks.length === 0) { return { content: [ { type: 'text', text: 'No timelocks found', }, ], }; } const timelockList = timelocks.map(timelock => { const status = timelock.decryptedData ? 'Decrypted' : 'Encrypted'; return `- Timelock ID: ${timelock.id}\n Status: ${status}\n Round Number: ${timelock.roundNumber}`; }).join('\n\n'); return { content: [ { type: 'text', text: timelockList, }, ], }; } case 'list_vault_history': { const intervalId = args.interval_id as string | undefined; const decryptedOnly = args.decrypted_only as boolean | undefined; const limit = args.limit as number | undefined; const offset = args.offset as number | undefined; const vaults = await timeVault.listVaults({ intervalId, decryptedOnly, limit, offset, }); if (vaults.length === 0) { return { content: [ { type: 'text', text: 'No vault entries found', }, ], }; } const vaultList = vaults.map(vault => { const status = vault.decryptedAt ? 'Decrypted' : 'Encrypted'; const decryptedTime = vault.decryptedAt ? `\n Decrypted at: ${new Date(vault.decryptedAt).toISOString()}` : ''; return `- Vault ID: ${vault.id} Status: ${status} Created at: ${new Date(vault.createdAt).toISOString()}${decryptedTime} Interval ID: ${vault.intervalId} Round Number: ${vault.roundNumber}`; }).join('\n\n'); return { content: [ { type: 'text', text: vaultList, }, ], }; } case 'get_vault_stats': { const stats = await timeVault.getStats(); const avgTime = stats.avgDecryptionTime ? `\nAverage decryption time: ${Math.round(stats.avgDecryptionTime)} seconds` : ''; return { content: [ { type: 'text', text: `Total vaults: ${stats.totalVaults} Decrypted vaults: ${stats.decryptedVaults}${avgTime}`, }, ], }; } default: throw new McpError(ErrorCode.MethodNotFound, `Unknown tool: ${name}`); } } catch (error) { if (error instanceof McpError) { throw error; } throw new McpError(ErrorCode.InternalError, `Internal error: ${error}`); } }); // Clean up old data every hour setInterval(async () => { const maxAge = 24 * 60 * 60 * 1000; // 24 hours await timekeeper.cleanupCompletedIntervals(maxAge); await timeLockManager.cleanupDecrypted(maxAge); }, 60 * 60 * 1000); // Start the server const transport = new StdioServerTransport(); await server.connect(transport); console.error('Slowtime MCP server running on stdio');