2025-08-26 12:32:21 -07:00
|
|
|
/**
|
|
|
|
|
* BrainyMCPBroadcast
|
|
|
|
|
*
|
|
|
|
|
* Enhanced MCP service with real-time WebSocket broadcasting capabilities
|
|
|
|
|
* for multi-agent coordination (Jarvis ↔ Picasso communication)
|
|
|
|
|
*
|
|
|
|
|
* Features:
|
|
|
|
|
* - WebSocket server for real-time push notifications
|
|
|
|
|
* - Subscription management for multiple Claude instances
|
|
|
|
|
* - Message broadcasting to all connected agents
|
|
|
|
|
* - Works both locally and with cloud deployment
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
import { WebSocketServer, WebSocket } from 'ws'
|
feat: add node: protocol to all Node.js built-in imports for bundler compatibility
- Updated all fs, path, crypto, os, url, util, events, http, https, net, child_process, stream, and zlib imports
- Changed both static imports and dynamic imports to use node: protocol
- This makes Brainy more bundler-friendly by explicitly marking Node.js built-ins
- Prevents bundlers from attempting to polyfill or bundle these modules
- Reduces bundle size for web applications using Brainy
- Improves tree-shaking and dead code elimination
Benefits for external bundlers:
- Clear distinction between Node.js built-ins and external dependencies
- No ambiguity about what needs polyfilling
- Smaller bundles for browser builds
- Better compatibility with modern bundlers (Webpack 5, Vite, Rollup, esbuild)
🤖 Generated with Claude Code
Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-17 14:20:21 -07:00
|
|
|
import { createServer, IncomingMessage } from 'node:http'
|
2025-08-26 12:32:21 -07:00
|
|
|
import { BrainyMCPService } from './brainyMCPService.js'
|
2025-09-30 16:04:00 -07:00
|
|
|
import { BrainyInterface } from '../types/brainyInterface.js'
|
2025-08-26 12:32:21 -07:00
|
|
|
import { MCPServiceOptions } from '../types/mcpTypes.js'
|
|
|
|
|
import { v4 as uuidv4 } from '../universal/uuid.js'
|
|
|
|
|
|
|
|
|
|
interface BroadcastMessage {
|
|
|
|
|
id: string
|
|
|
|
|
from: string
|
|
|
|
|
to?: string | string[]
|
|
|
|
|
type: 'message' | 'notification' | 'sync' | 'heartbeat' | 'identify'
|
|
|
|
|
event?: string
|
|
|
|
|
data: any
|
|
|
|
|
timestamp: number
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
interface ConnectedAgent {
|
|
|
|
|
id: string
|
|
|
|
|
name: string
|
|
|
|
|
role: string
|
|
|
|
|
socket: WebSocket
|
|
|
|
|
lastSeen: number
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export class BrainyMCPBroadcast extends BrainyMCPService {
|
|
|
|
|
private wsServer?: WebSocketServer
|
|
|
|
|
private httpServer?: any
|
|
|
|
|
private agents: Map<string, ConnectedAgent> = new Map()
|
|
|
|
|
private messageHistory: BroadcastMessage[] = []
|
|
|
|
|
private maxHistorySize = 100
|
|
|
|
|
|
|
|
|
|
constructor(
|
2025-09-11 16:23:32 -07:00
|
|
|
brainyData: BrainyInterface,
|
2025-08-26 12:32:21 -07:00
|
|
|
options: MCPServiceOptions & {
|
|
|
|
|
broadcastPort?: number
|
|
|
|
|
cloudUrl?: string
|
|
|
|
|
} = {}
|
|
|
|
|
) {
|
|
|
|
|
super(brainyData, options)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Start the WebSocket broadcast server
|
|
|
|
|
* @param port Port to listen on (default: 8765)
|
|
|
|
|
* @param isCloud Whether this is a cloud deployment
|
|
|
|
|
*/
|
|
|
|
|
async startBroadcastServer(port = 8765, isCloud = false): Promise<void> {
|
|
|
|
|
return new Promise((resolve, reject) => {
|
|
|
|
|
try {
|
|
|
|
|
// Create HTTP server
|
|
|
|
|
this.httpServer = createServer((req, res) => {
|
|
|
|
|
// Health check endpoint
|
|
|
|
|
if (req.url === '/health') {
|
|
|
|
|
res.writeHead(200, { 'Content-Type': 'application/json' })
|
|
|
|
|
res.end(JSON.stringify({
|
|
|
|
|
status: 'healthy',
|
|
|
|
|
agents: Array.from(this.agents.values()).map(a => ({
|
|
|
|
|
id: a.id,
|
|
|
|
|
name: a.name,
|
|
|
|
|
role: a.role,
|
|
|
|
|
connected: true
|
|
|
|
|
})),
|
|
|
|
|
uptime: process.uptime()
|
|
|
|
|
}))
|
|
|
|
|
} else {
|
|
|
|
|
res.writeHead(404)
|
|
|
|
|
res.end('Not found')
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
// Create WebSocket server
|
|
|
|
|
this.wsServer = new WebSocketServer({
|
|
|
|
|
server: this.httpServer,
|
|
|
|
|
perMessageDeflate: false // Better performance
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
this.wsServer.on('connection', (socket, request) => {
|
|
|
|
|
this.handleNewConnection(socket, request)
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
// Start listening
|
|
|
|
|
this.httpServer.listen(port, () => {
|
|
|
|
|
console.log(`🧠 Brain Jar Broadcast Server running on ${isCloud ? 'cloud' : 'local'} port ${port}`)
|
|
|
|
|
console.log(`📡 WebSocket: ws://localhost:${port}`)
|
|
|
|
|
console.log(`🔍 Health: http://localhost:${port}/health`)
|
|
|
|
|
resolve()
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
// Heartbeat to keep connections alive
|
|
|
|
|
setInterval(() => {
|
|
|
|
|
this.agents.forEach((agent) => {
|
|
|
|
|
if (Date.now() - agent.lastSeen > 30000) {
|
|
|
|
|
// Remove inactive agents
|
|
|
|
|
this.removeAgent(agent.id)
|
|
|
|
|
} else {
|
|
|
|
|
// Send heartbeat
|
|
|
|
|
this.sendToAgent(agent.id, {
|
|
|
|
|
id: uuidv4(),
|
|
|
|
|
from: 'server',
|
|
|
|
|
type: 'heartbeat',
|
|
|
|
|
data: { timestamp: Date.now() },
|
|
|
|
|
timestamp: Date.now()
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
}, 15000)
|
|
|
|
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
reject(error)
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Handle new WebSocket connection
|
|
|
|
|
*/
|
|
|
|
|
private handleNewConnection(socket: WebSocket, request: IncomingMessage) {
|
|
|
|
|
const agentId = uuidv4()
|
|
|
|
|
|
|
|
|
|
// Send welcome message
|
|
|
|
|
socket.send(JSON.stringify({
|
|
|
|
|
id: uuidv4(),
|
|
|
|
|
from: 'server',
|
|
|
|
|
type: 'notification',
|
|
|
|
|
event: 'welcome',
|
|
|
|
|
data: {
|
|
|
|
|
agentId,
|
|
|
|
|
message: 'Connected to Brain Jar Broadcast Server',
|
|
|
|
|
agents: Array.from(this.agents.values()).map(a => ({
|
|
|
|
|
id: a.id,
|
|
|
|
|
name: a.name,
|
|
|
|
|
role: a.role
|
|
|
|
|
}))
|
|
|
|
|
},
|
|
|
|
|
timestamp: Date.now()
|
|
|
|
|
}))
|
|
|
|
|
|
|
|
|
|
// Handle messages from this agent
|
|
|
|
|
socket.on('message', (data) => {
|
|
|
|
|
try {
|
|
|
|
|
const message = JSON.parse(data.toString())
|
|
|
|
|
this.handleAgentMessage(agentId, message)
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('Invalid message from agent:', error)
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
// Handle disconnection
|
|
|
|
|
socket.on('close', () => {
|
|
|
|
|
this.removeAgent(agentId)
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
// Handle errors
|
|
|
|
|
socket.on('error', (error) => {
|
|
|
|
|
console.error(`Agent ${agentId} error:`, error)
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
// Store temporary connection until identified
|
|
|
|
|
this.agents.set(agentId, {
|
|
|
|
|
id: agentId,
|
|
|
|
|
name: 'Unknown',
|
|
|
|
|
role: 'Unknown',
|
|
|
|
|
socket,
|
|
|
|
|
lastSeen: Date.now()
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Handle message from an agent
|
|
|
|
|
*/
|
|
|
|
|
private handleAgentMessage(agentId: string, message: any) {
|
|
|
|
|
const agent = this.agents.get(agentId)
|
|
|
|
|
if (!agent) return
|
|
|
|
|
|
|
|
|
|
// Update last seen
|
|
|
|
|
agent.lastSeen = Date.now()
|
|
|
|
|
|
|
|
|
|
// Handle identification
|
|
|
|
|
if (message.type === 'identify') {
|
|
|
|
|
agent.name = message.name || agent.name
|
|
|
|
|
agent.role = message.role || agent.role
|
|
|
|
|
|
|
|
|
|
// Notify all agents about new member
|
|
|
|
|
this.broadcast({
|
|
|
|
|
id: uuidv4(),
|
|
|
|
|
from: 'server',
|
|
|
|
|
type: 'notification',
|
|
|
|
|
event: 'agent_joined',
|
|
|
|
|
data: {
|
|
|
|
|
agent: {
|
|
|
|
|
id: agent.id,
|
|
|
|
|
name: agent.name,
|
|
|
|
|
role: agent.role
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
timestamp: Date.now()
|
|
|
|
|
}, agentId) // Exclude the joining agent
|
|
|
|
|
|
|
|
|
|
// Send recent history to new agent
|
|
|
|
|
if (this.messageHistory.length > 0) {
|
|
|
|
|
this.sendToAgent(agentId, {
|
|
|
|
|
id: uuidv4(),
|
|
|
|
|
from: 'server',
|
|
|
|
|
type: 'sync',
|
|
|
|
|
data: {
|
|
|
|
|
history: this.messageHistory.slice(-20) // Last 20 messages
|
|
|
|
|
},
|
|
|
|
|
timestamp: Date.now()
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Create broadcast message
|
|
|
|
|
const broadcastMsg: BroadcastMessage = {
|
|
|
|
|
id: message.id || uuidv4(),
|
|
|
|
|
from: agent.name,
|
|
|
|
|
to: message.to,
|
|
|
|
|
type: message.type || 'message',
|
|
|
|
|
event: message.event,
|
|
|
|
|
data: message.data,
|
|
|
|
|
timestamp: Date.now()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Store in history
|
|
|
|
|
this.addToHistory(broadcastMsg)
|
|
|
|
|
|
|
|
|
|
// Broadcast based on recipient
|
|
|
|
|
if (message.to) {
|
|
|
|
|
// Send to specific agent(s)
|
|
|
|
|
const recipients = Array.isArray(message.to) ? message.to : [message.to]
|
|
|
|
|
recipients.forEach((recipientName: string) => {
|
|
|
|
|
const recipient = Array.from(this.agents.values()).find(
|
|
|
|
|
a => a.name === recipientName
|
|
|
|
|
)
|
|
|
|
|
if (recipient) {
|
|
|
|
|
this.sendToAgent(recipient.id, broadcastMsg)
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
} else {
|
|
|
|
|
// Broadcast to all agents except sender
|
|
|
|
|
this.broadcast(broadcastMsg, agentId)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Broadcast message to all connected agents
|
|
|
|
|
*/
|
|
|
|
|
broadcast(message: BroadcastMessage, excludeId?: string) {
|
|
|
|
|
const messageStr = JSON.stringify(message)
|
|
|
|
|
|
|
|
|
|
this.agents.forEach((agent) => {
|
|
|
|
|
if (agent.id !== excludeId && agent.socket.readyState === WebSocket.OPEN) {
|
|
|
|
|
agent.socket.send(messageStr)
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Send message to specific agent
|
|
|
|
|
*/
|
|
|
|
|
private sendToAgent(agentId: string, message: BroadcastMessage) {
|
|
|
|
|
const agent = this.agents.get(agentId)
|
|
|
|
|
if (agent && agent.socket.readyState === WebSocket.OPEN) {
|
|
|
|
|
agent.socket.send(JSON.stringify(message))
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Remove agent from connected list
|
|
|
|
|
*/
|
|
|
|
|
private removeAgent(agentId: string) {
|
|
|
|
|
const agent = this.agents.get(agentId)
|
|
|
|
|
if (agent) {
|
|
|
|
|
// Notify others about disconnection
|
|
|
|
|
this.broadcast({
|
|
|
|
|
id: uuidv4(),
|
|
|
|
|
from: 'server',
|
|
|
|
|
type: 'notification',
|
|
|
|
|
event: 'agent_left',
|
|
|
|
|
data: {
|
|
|
|
|
agent: {
|
|
|
|
|
id: agent.id,
|
|
|
|
|
name: agent.name,
|
|
|
|
|
role: agent.role
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
timestamp: Date.now()
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
this.agents.delete(agentId)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Add message to history
|
|
|
|
|
*/
|
|
|
|
|
private addToHistory(message: BroadcastMessage) {
|
|
|
|
|
this.messageHistory.push(message)
|
|
|
|
|
|
|
|
|
|
// Trim history if too large
|
|
|
|
|
if (this.messageHistory.length > this.maxHistorySize) {
|
|
|
|
|
this.messageHistory = this.messageHistory.slice(-this.maxHistorySize)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Stop the broadcast server
|
|
|
|
|
*/
|
|
|
|
|
async stopBroadcastServer(): Promise<void> {
|
|
|
|
|
// Close all agent connections
|
|
|
|
|
this.agents.forEach(agent => {
|
|
|
|
|
agent.socket.close(1000, 'Server shutting down')
|
|
|
|
|
})
|
|
|
|
|
this.agents.clear()
|
|
|
|
|
|
|
|
|
|
// Close WebSocket server
|
|
|
|
|
if (this.wsServer) {
|
|
|
|
|
this.wsServer.close()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Close HTTP server
|
|
|
|
|
if (this.httpServer) {
|
|
|
|
|
this.httpServer.close()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Get connected agents
|
|
|
|
|
*/
|
|
|
|
|
getConnectedAgents(): Array<{ id: string; name: string; role: string }> {
|
|
|
|
|
return Array.from(this.agents.values()).map(a => ({
|
|
|
|
|
id: a.id,
|
|
|
|
|
name: a.name,
|
|
|
|
|
role: a.role
|
|
|
|
|
}))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Get message history
|
|
|
|
|
*/
|
|
|
|
|
getMessageHistory(): BroadcastMessage[] {
|
|
|
|
|
return [...this.messageHistory]
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Export for both environments
|
|
|
|
|
export default BrainyMCPBroadcast
|