/** * IPC-based MCP Server for NanoClaw * Writes messages and tasks to files for the host process to pick up */ import { createSdkMcpServer, tool } from '@anthropic-ai/claude-agent-sdk'; import { z } from 'zod'; import fs from 'fs'; import path from 'path'; const IPC_DIR = '/workspace/ipc'; const MESSAGES_DIR = path.join(IPC_DIR, 'messages'); const TASKS_DIR = path.join(IPC_DIR, 'tasks'); export interface IpcMcpContext { chatJid: string; groupFolder: string; isMain: boolean; } function writeIpcFile(dir: string, data: object): string { // Ensure directory exists fs.mkdirSync(dir, { recursive: true }); // Use timestamp + random suffix for unique filename const filename = `${Date.now()}-${Math.random().toString(36).slice(2, 8)}.json`; const filepath = path.join(dir, filename); // Write atomically: write to temp file, then rename const tempPath = `${filepath}.tmp`; fs.writeFileSync(tempPath, JSON.stringify(data, null, 2)); fs.renameSync(tempPath, filepath); return filename; } export function createIpcMcp(ctx: IpcMcpContext) { const { chatJid, groupFolder, isMain } = ctx; return createSdkMcpServer({ name: 'nanoclaw', version: '1.0.0', tools: [ // Send a message to the WhatsApp group tool( 'send_message', 'Send a message to the current WhatsApp group. Use this to proactively share information or updates.', { text: z.string().describe('The message text to send') }, async (args) => { const data = { type: 'message', chatJid, text: args.text, groupFolder, timestamp: new Date().toISOString() }; const filename = writeIpcFile(MESSAGES_DIR, data); return { content: [{ type: 'text', text: `Message queued for delivery (${filename})` }] }; } ), // Schedule a new task tool( 'schedule_task', 'Schedule a recurring or one-time task. The task will run as a full agent with access to all tools.', { prompt: z.string().describe('What the agent should do when the task runs'), schedule_type: z.enum(['cron', 'interval', 'once']).describe('Type of schedule'), schedule_value: z.string().describe('Cron expression, interval in ms, or ISO timestamp'), target_group: z.string().optional().describe('Target group folder (main only, defaults to current group)') }, async (args) => { // Non-main groups can only schedule for themselves const targetGroup = isMain && args.target_group ? args.target_group : groupFolder; const data = { type: 'schedule_task', prompt: args.prompt, schedule_type: args.schedule_type, schedule_value: args.schedule_value, groupFolder: targetGroup, chatJid, createdBy: groupFolder, timestamp: new Date().toISOString() }; const filename = writeIpcFile(TASKS_DIR, data); return { content: [{ type: 'text', text: `Task scheduled (${filename}): ${args.schedule_type} - ${args.schedule_value}` }] }; } ), // List tasks (reads from a mounted file that host keeps updated) tool( 'list_tasks', 'List all scheduled tasks. From main: shows all tasks. From other groups: shows only that group\'s tasks.', {}, async () => { // Host process writes current tasks to this file const tasksFile = path.join(IPC_DIR, 'current_tasks.json'); try { if (!fs.existsSync(tasksFile)) { return { content: [{ type: 'text', text: 'No scheduled tasks found.' }] }; } const allTasks = JSON.parse(fs.readFileSync(tasksFile, 'utf-8')); // Filter to current group unless main const tasks = isMain ? allTasks : allTasks.filter((t: { groupFolder: string }) => t.groupFolder === groupFolder); if (tasks.length === 0) { return { content: [{ type: 'text', text: 'No scheduled tasks found.' }] }; } const formatted = tasks.map((t: { id: string; prompt: string; schedule_type: string; schedule_value: string; status: string; next_run: string }) => `- [${t.id}] ${t.prompt.slice(0, 50)}... (${t.schedule_type}: ${t.schedule_value}) - ${t.status}, next: ${t.next_run || 'N/A'}` ).join('\n'); return { content: [{ type: 'text', text: `Scheduled tasks:\n${formatted}` }] }; } catch (err) { return { content: [{ type: 'text', text: `Error reading tasks: ${err instanceof Error ? err.message : String(err)}` }] }; } } ), // Pause a task tool( 'pause_task', 'Pause a scheduled task. It will not run until resumed.', { task_id: z.string().describe('The task ID to pause') }, async (args) => { const data = { type: 'pause_task', taskId: args.task_id, groupFolder, isMain, timestamp: new Date().toISOString() }; writeIpcFile(TASKS_DIR, data); return { content: [{ type: 'text', text: `Task ${args.task_id} pause requested.` }] }; } ), // Resume a task tool( 'resume_task', 'Resume a paused task.', { task_id: z.string().describe('The task ID to resume') }, async (args) => { const data = { type: 'resume_task', taskId: args.task_id, groupFolder, isMain, timestamp: new Date().toISOString() }; writeIpcFile(TASKS_DIR, data); return { content: [{ type: 'text', text: `Task ${args.task_id} resume requested.` }] }; } ), // Cancel a task tool( 'cancel_task', 'Cancel and delete a scheduled task.', { task_id: z.string().describe('The task ID to cancel') }, async (args) => { const data = { type: 'cancel_task', taskId: args.task_id, groupFolder, isMain, timestamp: new Date().toISOString() }; writeIpcFile(TASKS_DIR, data); return { content: [{ type: 'text', text: `Task ${args.task_id} cancellation requested.` }] }; } ) ] }); }