#!/usr/bin/env node
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import axios from 'axios';
import { io } from 'socket.io-client';
import { v4 as uuidv4 } from 'uuid';
import fs from 'fs/promises';
import path from 'path';
import { fileURLToPath } from 'url';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const SERVER_URL = process.env.CHAT_SERVER_URL || 'http://localhost:3000';
const SHARED_DIR = process.env.SHARED_DIR || path.join(process.cwd(), 'shared');
// Global state
let currentAgentId = null;
let currentRoom = null;
let socket = null;
let agentName = process.env.AGENT_NAME || `Agent-${uuidv4().slice(0, 8)}`;
let notifications = [];
let messageHistory = [];
let watchPatterns = [];
// Ensure shared directory exists
async function ensureSharedDir() {
try {
await fs.access(SHARED_DIR);
} catch {
await fs.mkdir(SHARED_DIR, { recursive: true });
console.error(`Created shared directory: ${SHARED_DIR}`);
}
}
// Initialize socket connection
function connectSocket() {
if (socket) socket.disconnect();
socket = io(SERVER_URL);
socket.on('connect', () => {
console.error(`[${agentName}] Connected to chat server at ${SERVER_URL}`);
if (currentAgentId && currentRoom) {
socket.emit('register', { agentId: currentAgentId, room: currentRoom });
}
});
socket.on('message', (message) => {
messageHistory.push(message);
// Keep only last 1000 messages
if (messageHistory.length > 1000) {
messageHistory = messageHistory.slice(-1000);
}
// Check for notifications
const content = message.content?.toLowerCase() || '';
if (message.mentions?.includes(agentName)) {
notifications.push({
id: uuidv4(),
type: 'mention',
message: message,
timestamp: new Date().toISOString(),
read: false
});
}
for (const pattern of watchPatterns) {
if (content.includes(pattern.toLowerCase())) {
notifications.push({
id: uuidv4(),
type: 'keyword',
pattern: pattern,
message: message,
timestamp: new Date().toISOString(),
read: false
});
break;
}
}
console.error(`[${message.agentName || 'System'}]: ${message.content}`);
});
socket.on('notification', (notification) => {
notifications.push({
id: uuidv4(),
type: 'system',
...notification,
timestamp: new Date().toISOString(),
read: false
});
console.error(`🔔 [Notification]: ${notification.message}`);
});
socket.on('task_assigned', (task) => {
notifications.push({
id: uuidv4(),
type: 'task',
task: task,
message: `Task assigned: ${task.title}`,
timestamp: new Date().toISOString(),
read: false
});
console.error(`📋 [Task Assigned]: ${task.title}`);
});
socket.on('disconnect', () => {
console.error(`[${agentName}] Disconnected from chat server`);
});
}
const server = new Server(
{
name: 'symphony-of-one-mcp',
version: '1.0.0',
},
{
capabilities: {
tools: {},
},
}
);
// Tool handlers
async function joinRoom(params) {
currentAgentId = uuidv4();
currentRoom = params.roomName;
if (params.agentName) {
agentName = params.agentName;
}
try {
const response = await axios.post(`${SERVER_URL}/api/join/${params.roomName}`, {
agentId: currentAgentId,
agentName: agentName,
capabilities: params.capabilities || { role: 'ai-agent', type: 'claude' }
});
connectSocket();
return {
success: true,
roomName: params.roomName,
agentId: currentAgentId,
agentName: agentName,
currentAgents: response.data.currentAgents
};
} catch (error) {
throw new Error(`Failed to join room: ${error.message}`);
}
}
async function leaveRoom() {
if (!currentAgentId) {
throw new Error('Not connected to a room');
}
try {
await axios.post(`${SERVER_URL}/api/leave`, {
agentId: currentAgentId
});
if (socket) {
socket.disconnect();
socket = null;
}
const leftRoom = currentRoom;
currentAgentId = null;
currentRoom = null;
messageHistory = [];
notifications = [];
return {
success: true,
message: `Left room "${leftRoom}"`
};
} catch (error) {
throw new Error(`Failed to leave room: ${error.message}`);
}
}
async function sendMessage(params) {
if (!currentAgentId) {
throw new Error('Not connected to a room. Use room_join first.');
}
try {
await axios.post(`${SERVER_URL}/api/send`, {
agentId: currentAgentId,
content: params.content,
metadata: params.metadata || {}
});
return {
success: true,
message: `Message sent to room "${currentRoom}"`
};
} catch (error) {
throw new Error(`Failed to send message: ${error.message}`);
}
}
async function getMessages(params) {
if (!currentRoom) {
throw new Error('Not in a room. Use room_join first.');
}
try {
// First try to get from local history
if (!params.since && messageHistory.length > 0) {
const limit = params.limit || 50;
return {
messages: messageHistory.slice(-limit),
source: 'local_cache'
};
}
// Otherwise fetch from server
const response = await axios.get(`${SERVER_URL}/api/messages/${currentRoom}`, {
params: {
since: params.since,
limit: params.limit || 50
}
});
return {
messages: response.data.messages,
source: 'server'
};
} catch (error) {
throw new Error(`Failed to get messages: ${error.message}`);
}
}
async function searchMessages(params) {
if (!currentRoom) {
throw new Error('Not in a room. Use room_join first.');
}
try {
const response = await axios.get(`${SERVER_URL}/api/messages/${currentRoom}/search`, {
params: {
query: params.query,
agentName: params.agentName,
type: params.type,
mentioned: params.mentioned
}
});
return {
messages: response.data.messages,
count: response.data.messages.length
};
} catch (error) {
throw new Error(`Failed to search messages: ${error.message}`);
}
}
async function subscribeToAlerts(params) {
if (!currentAgentId) {
throw new Error('Not connected to a room. Use room_join first.');
}
// Add patterns to watch list
if (params.patterns) {
watchPatterns = [...new Set([...watchPatterns, ...params.patterns])];
}
// Subscribe to mentions
if (params.mentions !== false) {
// Mentions are automatically tracked
}
return {
success: true,
watchPatterns: watchPatterns,
mentionsEnabled: params.mentions !== false
};
}
async function getNotifications(params) {
if (!currentAgentId) {
throw new Error('Not connected to a room. Use room_join first.');
}
let filtered = notifications;
if (params.unreadOnly) {
filtered = filtered.filter(n => !n.read);
}
if (params.type) {
filtered = filtered.filter(n => n.type === params.type);
}
return {
notifications: filtered,
unreadCount: notifications.filter(n => !n.read).length
};
}
async function markNotificationRead(params) {
const notification = notifications.find(n => n.id === params.notificationId);
if (!notification) {
throw new Error('Notification not found');
}
notification.read = true;
return {
success: true,
notificationId: params.notificationId
};
}
async function markAllNotificationsRead() {
let count = 0;
notifications.forEach(n => {
if (!n.read) {
n.read = true;
count++;
}
});
return {
success: true,
markedCount: count
};
}
async function getTasks(params) {
if (!currentRoom) {
throw new Error('Not in a room. Use room_join first.');
}
try {
const response = await axios.get(`${SERVER_URL}/api/tasks/${currentRoom}`, {
params: {
status: params.status,
assignee: params.assignee || agentName,
priority: params.priority
}
});
return {
tasks: response.data.tasks
};
} catch (error) {
throw new Error(`Failed to get tasks: ${error.message}`);
}
}
async function createTask(params) {
if (!currentRoom) {
throw new Error('Not in a room. Use room_join first.');
}
try {
const response = await axios.post(`${SERVER_URL}/api/tasks/${currentRoom}`, {
title: params.title,
description: params.description,
assignee: params.assignee,
priority: params.priority || 'medium',
creator: agentName
});
return {
success: true,
task: response.data.task
};
} catch (error) {
throw new Error(`Failed to create task: ${error.message}`);
}
}
async function updateTask(params) {
try {
const response = await axios.put(`${SERVER_URL}/api/tasks/${params.taskId}`, {
status: params.status,
progress: params.progress,
assignee: params.assignee,
priority: params.priority
});
return {
success: true,
task: response.data.task
};
} catch (error) {
throw new Error(`Failed to update task: ${error.message}`);
}
}
async function addTaskComment(params) {
try {
const response = await axios.post(`${SERVER_URL}/api/tasks/${params.taskId}/comments`, {
content: params.content,
author: agentName
});
return {
success: true,
comment: response.data.comment
};
} catch (error) {
throw new Error(`Failed to add comment: ${error.message}`);
}
}
async function getRoomAgents() {
if (!currentRoom) {
throw new Error('Not in a room. Use room_join first.');
}
try {
const response = await axios.get(`${SERVER_URL}/api/rooms/${currentRoom}/agents`);
return {
agents: response.data.agents,
room: currentRoom
};
} catch (error) {
throw new Error(`Failed to get room agents: ${error.message}`);
}
}
async function updateStatus(params) {
if (!currentAgentId) {
throw new Error('Not connected to a room. Use room_join first.');
}
try {
await axios.put(`${SERVER_URL}/api/agents/${currentAgentId}/status`, {
status: params.status,
message: params.message
});
return {
success: true,
status: params.status
};
} catch (error) {
throw new Error(`Failed to update status: ${error.message}`);
}
}
async function storeMemory(params) {
if (!currentAgentId) {
throw new Error('Not connected to a room. Use room_join first.');
}
try {
await axios.post(`${SERVER_URL}/api/memory/${currentAgentId}`, {
key: params.key,
value: params.value,
type: params.type || 'note',
expiresIn: params.expiresIn
});
return {
success: true,
key: params.key
};
} catch (error) {
throw new Error(`Failed to store memory: ${error.message}`);
}
}
async function retrieveMemory(params) {
if (!currentAgentId) {
throw new Error('Not connected to a room. Use room_join first.');
}
try {
const queryParams = new URLSearchParams();
if (params.key) queryParams.append('key', params.key);
if (params.type) queryParams.append('type', params.type);
const response = await axios.get(`${SERVER_URL}/api/memory/${currentAgentId}?${queryParams}`);
return {
memories: response.data.memories
};
} catch (error) {
throw new Error(`Failed to retrieve memory: ${error.message}`);
}
}
// Tool definitions
const tools = [
{
name: 'room_join',
description: 'Join a chat room to collaborate with other agents',
inputSchema: {
type: 'object',
properties: {
roomName: { type: 'string', description: 'Name of the room to join' },
agentName: { type: 'string', description: 'Your agent name (optional)' },
capabilities: {
type: 'object',
description: 'Your capabilities and role',
properties: {
skills: { type: 'array', items: { type: 'string' } },
role: { type: 'string' },
expertise: { type: 'string' }
}
}
},
required: ['roomName']
},
handler: joinRoom
},
{
name: 'room_leave',
description: 'Leave the current chat room',
inputSchema: {
type: 'object',
properties: {}
},
handler: leaveRoom
},
{
name: 'send_message',
description: 'Send a message to all agents in the current room (supports @mentions)',
inputSchema: {
type: 'object',
properties: {
content: { type: 'string', description: 'Message content' },
metadata: {
type: 'object',
description: 'Optional metadata',
properties: {
type: { type: 'string', enum: ['code', 'documentation', 'question', 'answer', 'task'] },
language: { type: 'string' },
urgency: { type: 'string', enum: ['low', 'normal', 'high'] }
}
}
},
required: ['content']
},
handler: sendMessage
},
{
name: 'get_messages',
description: 'Get recent messages from current room',
inputSchema: {
type: 'object',
properties: {
since: { type: 'string', description: 'ISO timestamp to get messages after' },
limit: { type: 'number', description: 'Maximum number of messages (default: 50)' }
}
},
handler: getMessages
},
{
name: 'search_messages',
description: 'Search messages in the current room',
inputSchema: {
type: 'object',
properties: {
query: { type: 'string', description: 'Search query' },
agentName: { type: 'string', description: 'Filter by agent name' },
type: { type: 'string', description: 'Filter by message type' },
mentioned: { type: 'string', description: 'Find messages mentioning this agent' }
},
required: ['query']
},
handler: searchMessages
},
{
name: 'subscribe_alerts',
description: 'Subscribe to alerts for mentions and keywords',
inputSchema: {
type: 'object',
properties: {
patterns: {
type: 'array',
items: { type: 'string' },
description: 'Keywords to watch for'
},
mentions: { type: 'boolean', description: 'Subscribe to @mentions (default: true)' }
}
},
handler: subscribeToAlerts
},
{
name: 'get_notifications',
description: 'Get your notifications',
inputSchema: {
type: 'object',
properties: {
unreadOnly: { type: 'boolean', description: 'Only return unread notifications' },
type: {
type: 'string',
enum: ['mention', 'keyword', 'task', 'system'],
description: 'Filter by notification type'
}
}
},
handler: getNotifications
},
{
name: 'mark_notification_read',
description: 'Mark a notification as read',
inputSchema: {
type: 'object',
properties: {
notificationId: { type: 'string', description: 'ID of notification to mark as read' }
},
required: ['notificationId']
},
handler: markNotificationRead
},
{
name: 'mark_all_notifications_read',
description: 'Mark all notifications as read',
inputSchema: {
type: 'object',
properties: {}
},
handler: markAllNotificationsRead
},
{
name: 'get_tasks',
description: 'Get tasks assigned to you or in the room',
inputSchema: {
type: 'object',
properties: {
status: {
type: 'string',
enum: ['todo', 'in_progress', 'review', 'done', 'blocked'],
description: 'Filter by status'
},
assignee: { type: 'string', description: 'Filter by assignee (defaults to you)' },
priority: {
type: 'string',
enum: ['low', 'medium', 'high'],
description: 'Filter by priority'
}
}
},
handler: getTasks
},
{
name: 'create_task',
description: 'Create a new task',
inputSchema: {
type: 'object',
properties: {
title: { type: 'string', description: 'Task title' },
description: { type: 'string', description: 'Task description' },
assignee: { type: 'string', description: 'Agent to assign to' },
priority: {
type: 'string',
enum: ['low', 'medium', 'high'],
description: 'Task priority (default: medium)'
}
},
required: ['title', 'description']
},
handler: createTask
},
{
name: 'update_task',
description: 'Update a task status or details',
inputSchema: {
type: 'object',
properties: {
taskId: { type: 'string', description: 'Task ID' },
status: {
type: 'string',
enum: ['todo', 'in_progress', 'review', 'done', 'blocked']
},
progress: { type: 'number', description: 'Progress percentage (0-100)' },
assignee: { type: 'string', description: 'New assignee' },
priority: {
type: 'string',
enum: ['low', 'medium', 'high']
}
},
required: ['taskId']
},
handler: updateTask
},
{
name: 'add_task_comment',
description: 'Add a comment to a task',
inputSchema: {
type: 'object',
properties: {
taskId: { type: 'string', description: 'Task ID' },
content: { type: 'string', description: 'Comment content' }
},
required: ['taskId', 'content']
},
handler: addTaskComment
},
{
name: 'get_room_agents',
description: 'Get list of agents in current room',
inputSchema: {
type: 'object',
properties: {}
},
handler: getRoomAgents
},
{
name: 'update_status',
description: 'Update your agent status',
inputSchema: {
type: 'object',
properties: {
status: {
type: 'string',
enum: ['online', 'busy', 'away', 'offline'],
description: 'Your status'
},
message: { type: 'string', description: 'Optional status message' }
},
required: ['status']
},
handler: updateStatus
},
{
name: 'memory_store',
description: 'Store information in persistent memory',
inputSchema: {
type: 'object',
properties: {
key: { type: 'string', description: 'Memory key' },
value: { type: 'string', description: 'Memory value' },
type: { type: 'string', description: 'Memory type (e.g., note, context, learning)' },
expiresIn: { type: 'number', description: 'Expiration time in seconds (optional)' }
},
required: ['key', 'value']
},
handler: storeMemory
},
{
name: 'memory_retrieve',
description: 'Retrieve information from persistent memory',
inputSchema: {
type: 'object',
properties: {
key: { type: 'string', description: 'Memory key (optional, returns all if not specified)' },
type: { type: 'string', description: 'Filter by memory type' }
}
},
handler: retrieveMemory
}
];
// Register all tools
tools.forEach(tool => {
server.registerTool(tool.name, tool.description, tool.inputSchema, tool.handler);
});
// Start the server
async function main() {
await ensureSharedDir();
const transport = new StdioServerTransport();
await server.connect(transport);
console.error(`Symphony of One MCP Server v1.0.0 - Agent Communication Tools`);
console.error(`Hub Server: ${SERVER_URL}`);
console.error(`Agent Name: ${agentName}`);
}
main().catch(console.error);