v2 phase 4+5: Discord via Chat SDK, expanded MCP tools, message seq IDs

- Chat SDK bridge + Discord adapter (gateway listener, message routing)
- MCP tools refactored into modular structure: core (send_message, send_file,
  edit_message, add_reaction), scheduling (schedule/list/cancel/pause/resume
  tasks), interactive (ask_user_question, send_card), agents (send_to_agent)
- Message seq IDs: shared integer sequence across messages_in/out so agents
  see small numeric IDs instead of platform snowflakes
- busy_timeout=5000 for session DB (poll loop + MCP server concurrent access)
- Always copy agent-runner source to fix stale cache when non-index files change
- Seed script for Discord testing, e2e test script

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
gavrielc
2026-04-09 02:53:39 +03:00
parent b36f127acc
commit afbc20a6c4
21 changed files with 2702 additions and 37 deletions

View File

@@ -0,0 +1,190 @@
/**
* Core MCP tools: send_message, send_file, edit_message, add_reaction.
*/
import fs from 'fs';
import path from 'path';
import { writeMessageOut, getMessageIdBySeq } from '../db/messages-out.js';
import type { McpToolDefinition } from './types.js';
function log(msg: string): void {
console.error(`[mcp-tools] ${msg}`);
}
function generateId(): string {
return `msg-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
}
function routing() {
return {
platform_id: process.env.NANOCLAW_PLATFORM_ID || null,
channel_type: process.env.NANOCLAW_CHANNEL_TYPE || null,
thread_id: process.env.NANOCLAW_THREAD_ID || null,
};
}
function ok(text: string) {
return { content: [{ type: 'text' as const, text }] };
}
function err(text: string) {
return { content: [{ type: 'text' as const, text: `Error: ${text}` }], isError: true };
}
export const sendMessage: McpToolDefinition = {
tool: {
name: 'send_message',
description: 'Send a chat message to the current conversation or a specified destination.',
inputSchema: {
type: 'object' as const,
properties: {
text: { type: 'string', description: 'Message content' },
channel: { type: 'string', description: 'Target channel type (default: reply to origin)' },
platformId: { type: 'string', description: 'Target platform ID' },
threadId: { type: 'string', description: 'Target thread ID' },
},
required: ['text'],
},
},
async handler(args) {
const text = args.text as string;
if (!text) return err('text is required');
const id = generateId();
const r = routing();
const seq = writeMessageOut({
id,
kind: 'chat',
platform_id: (args.platformId as string) || r.platform_id,
channel_type: (args.channel as string) || r.channel_type,
thread_id: (args.threadId as string) || r.thread_id,
content: JSON.stringify({ text }),
});
log(`send_message: #${seq} ${id}${r.channel_type || 'default'}/${r.platform_id || 'default'}`);
return ok(`Message sent (id: ${seq})`);
},
};
export const sendFile: McpToolDefinition = {
tool: {
name: 'send_file',
description: 'Send a file to the current conversation.',
inputSchema: {
type: 'object' as const,
properties: {
path: { type: 'string', description: 'File path (relative to /workspace/agent/ or absolute)' },
text: { type: 'string', description: 'Optional accompanying message' },
filename: { type: 'string', description: 'Display name (default: basename of path)' },
},
required: ['path'],
},
},
async handler(args) {
const filePath = args.path as string;
if (!filePath) return err('path is required');
const resolvedPath = path.isAbsolute(filePath) ? filePath : path.resolve('/workspace/agent', filePath);
if (!fs.existsSync(resolvedPath)) return err(`File not found: ${filePath}`);
const id = generateId();
const filename = (args.filename as string) || path.basename(resolvedPath);
const r = routing();
// Copy file to outbox
const outboxDir = path.join('/workspace/outbox', id);
fs.mkdirSync(outboxDir, { recursive: true });
fs.copyFileSync(resolvedPath, path.join(outboxDir, filename));
writeMessageOut({
id,
kind: 'chat',
platform_id: r.platform_id,
channel_type: r.channel_type,
thread_id: r.thread_id,
content: JSON.stringify({ text: (args.text as string) || '', files: [filename] }),
});
log(`send_file: ${id}${filename}`);
return ok(`File sent (id: ${id}, filename: ${filename})`);
},
};
export const editMessage: McpToolDefinition = {
tool: {
name: 'edit_message',
description: 'Edit a previously sent message.',
inputSchema: {
type: 'object' as const,
properties: {
messageId: { type: 'integer', description: 'Message ID (the numeric id shown in messages)' },
text: { type: 'string', description: 'New message content' },
},
required: ['messageId', 'text'],
},
},
async handler(args) {
const seq = Number(args.messageId);
const text = args.text as string;
if (!seq || !text) return err('messageId and text are required');
const platformId = getMessageIdBySeq(seq);
if (!platformId) return err(`Message #${seq} not found`);
const id = generateId();
const r = routing();
writeMessageOut({
id,
kind: 'chat',
platform_id: r.platform_id,
channel_type: r.channel_type,
thread_id: r.thread_id,
content: JSON.stringify({ operation: 'edit', messageId: platformId, text }),
});
log(`edit_message: #${seq}${platformId}`);
return ok(`Message edit queued for #${seq}`);
},
};
export const addReaction: McpToolDefinition = {
tool: {
name: 'add_reaction',
description: 'Add an emoji reaction to a message.',
inputSchema: {
type: 'object' as const,
properties: {
messageId: { type: 'integer', description: 'Message ID (the numeric id shown in messages)' },
emoji: { type: 'string', description: 'Emoji name (e.g., thumbs_up, heart, check)' },
},
required: ['messageId', 'emoji'],
},
},
async handler(args) {
const seq = Number(args.messageId);
const emoji = args.emoji as string;
if (!seq || !emoji) return err('messageId and emoji are required');
const platformId = getMessageIdBySeq(seq);
if (!platformId) return err(`Message #${seq} not found`);
const id = generateId();
const r = routing();
writeMessageOut({
id,
kind: 'chat',
platform_id: r.platform_id,
channel_type: r.channel_type,
thread_id: r.thread_id,
content: JSON.stringify({ operation: 'reaction', messageId: platformId, emoji }),
});
log(`add_reaction: #${seq}${emoji} on ${platformId}`);
return ok(`Reaction queued for #${seq}`);
},
};
export const coreTools: McpToolDefinition[] = [sendMessage, sendFile, editMessage, addReaction];