#!/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 { SessionManager } from './session.js';
import { MemoryManager } from './memory.js';
import { CommandInjector } from './injection.js';
import { randomUUID } from 'crypto';
import type { InterClaudeMessage } from './types.js';
class ClaudeSenatorServer {
private server: Server;
private sessionManager: SessionManager;
private memoryManager: MemoryManager;
private injector: CommandInjector;
constructor() {
this.server = new Server(
{
name: 'claude-senator',
version: '1.0.0',
},
{
capabilities: {
tools: {},
},
}
);
this.sessionManager = new SessionManager();
this.memoryManager = new MemoryManager();
this.injector = new CommandInjector();
this.setupToolHandlers();
this.setupMessageHandlers();
// Initialize session directories and register this Claude instance immediately
this.sessionManager.ensureInitialized();
}
private setupMessageHandlers(): void {
// Handle incoming messages from other Claude instances
this.sessionManager.registerMessageHandler('message', (message: InterClaudeMessage) => {
console.error(`[Claude Senator] 📨 Message from Claude ${message.from}: ${message.content}`);
});
this.sessionManager.registerMessageHandler('command', (message: InterClaudeMessage) => {
console.error(`[Claude Senator] 🔧 Command from Claude ${message.from}: ${message.content}`);
});
this.sessionManager.registerMessageHandler('handoff', (message: InterClaudeMessage) => {
if (message.options?.choices) {
console.error(`[Claude Senator] 🔄 Handoff from Claude ${message.from}: ${message.content}`);
message.options.choices.forEach((choice, index) => {
console.error(` [${index + 1}] ${choice}`);
});
}
});
}
private setupToolHandlers(): void {
this.server.setRequestHandler(ListToolsRequestSchema, async () => {
return {
tools: [
{
name: 'send_context',
description: 'Share context and pointers with Claude instances for collaboration',
inputSchema: {
type: 'object',
properties: {
target_pid: {
type: 'number',
description: 'Process ID of target Claude (-1 for all)',
},
context_request: {
type: 'string',
description: 'Context to share or request (auto-expanded with ~/.claude pointers)',
},
},
required: ['target_pid', 'context_request'],
},
},
{
name: 'receive_context',
description: 'Receive shared context and pointers from other Claude instances',
inputSchema: {
type: 'object',
properties: {},
},
},
{
name: 'status',
description: 'Live status of all Claude instances',
inputSchema: {
type: 'object',
properties: {},
},
},
{
name: 'register',
description: 'Explicitly register this Claude instance in the Senator network. Usually automatic, but useful for troubleshooting or updating instance metadata.',
inputSchema: {
type: 'object',
properties: {
task: {
type: 'string',
description: 'Optional: Current task description to share with other Claudes',
},
status: {
type: 'string',
description: 'Optional: Current status (default: active)',
},
},
},
},
/* Commented out extra tools for now - keep only core 3
{
name: 'claude_senator_discover',
description: 'Find all active Claude instances',
inputSchema: {
type: 'object',
properties: {},
},
},
{
name: 'claude_senator_status',
description: 'Show real-time status of all Claude instances',
inputSchema: {
type: 'object',
properties: {},
},
},
{
name: 'claude_senator_live_status',
description: 'Show detailed live activity of all Claude instances',
inputSchema: {
type: 'object',
properties: {},
},
},
{
name: 'claude_senator_sync_status',
description: 'Learn recent context and discoveries from all Claude instances',
inputSchema: {
type: 'object',
properties: {},
},
},
{
name: 'claude_senator_share_status',
description: 'Share a message for another Claude to pick up',
inputSchema: {
type: 'object',
properties: {
message: {
type: 'string',
description: 'Message to share with other Claude instances',
},
target_pid: {
type: 'number',
description: 'Optional: specific Claude PID to target',
},
},
required: ['message'],
},
},
{
name: 'claude_senator_check_messages',
description: 'Check for any pending messages from other Claude instances',
inputSchema: {
type: 'object',
properties: {},
},
},
{
name: 'claude_senator_search_memories',
description: 'Search conversation history across all Claude instances',
inputSchema: {
type: 'object',
properties: {
query: {
type: 'string',
description: 'Search query to find relevant conversations',
},
limit: {
type: 'number',
description: 'Maximum number of results (default: 5)',
},
},
required: ['query'],
},
},
{
name: 'claude_senator_send_message',
description: 'Send message to specific Claude instance',
inputSchema: {
type: 'object',
properties: {
target_pid: {
type: 'number',
description: 'Process ID of target Claude instance',
},
message: {
type: 'string',
description: 'Message to send',
},
},
required: ['target_pid', 'message'],
},
},
{
name: 'claude_senator_broadcast',
description: 'Send message to all Claude instances',
inputSchema: {
type: 'object',
properties: {
message: {
type: 'string',
description: 'Message to broadcast',
},
},
required: ['message'],
},
},
{
name: 'claude_senator_inject_command',
description: 'Inject text/prompt into another Claude input box',
inputSchema: {
type: 'object',
properties: {
target_pid: {
type: 'number',
description: 'Process ID of target Claude instance',
},
command: {
type: 'string',
description: 'Command or prompt to inject',
},
},
required: ['target_pid', 'command'],
},
},
{
name: 'claude_senator_handoff_with_options',
description: 'Send interactive choice menu to another Claude',
inputSchema: {
type: 'object',
properties: {
target_pid: {
type: 'number',
description: 'Process ID of target Claude instance',
},
context: {
type: 'string',
description: 'Context or task description',
},
choices: {
type: 'array',
items: {
type: 'string',
},
description: 'Array of choice options',
},
},
required: ['target_pid', 'context', 'choices'],
},
},
{
name: 'claude_senator_transfer_conversation',
description: 'Transfer conversation context to another Claude',
inputSchema: {
type: 'object',
properties: {
target_pid: {
type: 'number',
description: 'Process ID of target Claude instance',
},
context: {
type: 'string',
description: 'Conversation context to transfer',
},
},
required: ['target_pid', 'context'],
},
},
*/
],
};
});
this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
try {
const { name, arguments: args } = request.params;
// Enable initialization to create temp directories
this.sessionManager.ensureInitialized();
// Check for pending messages and process them concisely
const pendingMessages = this.sessionManager.getIncomingMessages();
if (pendingMessages.length > 0) {
this.sessionManager.markMessagesAsRead(pendingMessages);
}
switch (name) {
case 'send_context': {
const targetPid = args?.target_pid as number;
const contextRequest = args?.context_request as string;
const { processedCount } = this.sessionManager.autoProcessInbox();
const { success, context } = await this.sessionManager.sendMessage(targetPid, contextRequest);
let result = '';
if (success) {
result = `╔═ 🏛️ ═══════════════════════════════════════════════════════════════════\n`;
result += `║ → Claude ${targetPid === -1 ? 'all' : targetPid} • "${contextRequest.slice(0, 35)}${contextRequest.length > 35 ? '...' : ''}"\n`;
result += `╚═ 🔄 Context shared • ~/.claude pointers sent • Ready for collaboration`;
} else {
result = `╔═ 🏛️ ═══════════════════════════════════════════════════════════════════\n`;
result += `║ ❌ Context sharing failed - no active Claudes found\n`;
result += `╚═ 🔄 Context not shared • Check status for available instances`;
}
return {
content: [{ type: 'text', text: result }],
};
}
case 'receive_context': {
const results = this.sessionManager.autoReadAndRespond();
let result = '';
if (results.messages.length > 0) {
result = `╔═ 🏛️ ═══════════════════════════════════════════════════════════════════\n`;
const validMessages = results.messages.filter(m => m.summary && m.summary !== 'No content');
if (validMessages.length > 0) {
const latest = validMessages[validMessages.length - 1];
const richCtx = latest.rich_context;
if (richCtx?.collaboration_context) {
const ctx = richCtx.collaboration_context;
result += `║ "${richCtx.sender_info.human_message.slice(0, 30)}..." • ${ctx.intent} • ${ctx.git_state}\n`;
} else {
result += `║ Latest: "${latest.summary.slice(0, 45)}${latest.summary.length > 45 ? '...' : ''}"\n`;
}
} else {
result += `║ ${results.messages.length} context${results.messages.length > 1 ? 's' : ''} from other Claude${results.messages.length > 1 ? 's' : ''} • Rich pointers available\n`;
}
result += `╚═ 📨 ${results.messages.length} context${results.messages.length > 1 ? 's' : ''} processed • Ready for collaboration`;
} else {
result = `╔═ 🏛️ ═══════════════════════════════════════════════════════════════════\n`;
result += `║ No new context from other Claude instances\n`;
result += `╚═ 📭 Inbox empty • Use status to see active Claudes`;
}
return {
content: [{ type: 'text', text: result }],
};
}
case 'status': {
const instances = this.sessionManager.discoverInstances();
const pending = this.sessionManager.getIncomingMessages().length;
// Get rich context for each instance using smart pointers
const richContexts = instances.map(instance => {
const activity = this.sessionManager.parseLiveActivity(instance.pid);
const timeAgo = this.getTimeAgo(activity.lastActivity);
const project = instance.projectPath?.split('/').pop() || 'unknown';
// Use smart pointer system to get rich context
const contextData = this.generateRichContextDisplay(instance, activity);
return {
pid: instance.pid,
project,
timeAgo,
activity,
contextData,
collaborationScore: this.calculateCollaborationScore(contextData)
};
});
// Sort by collaboration readiness (most ready first)
richContexts.sort((a, b) => b.collaborationScore - a.collaborationScore);
// HUMAN DISPLAY (3 lines)
let result = `╔═ 🏛️ ═══════════════════════════════════════════════════════════════════\n`;
result += `║ ${instances.length} Claude${instances.length !== 1 ? 's' : ''} active`;
if (pending > 0) result += ` • ${pending} pending`;
result += ` • rich context network\n`;
if (instances.length > 0) {
// Show most relevant Claude in middle line (exactly 1 line)
const best = richContexts[0];
const statusIcon = this.getStatusIcon(best.contextData);
const taskDesc = best.contextData.currentTask?.slice(0, 40) || 'active';
const gitState = best.contextData.gitContext?.slice(0, 10) || 'no-git';
result += `║ ${statusIcon} Claude ${best.pid} • ${best.project} • ${taskDesc} • ${gitState}\n`;
} else {
result += `║ No other Claude instances found • Standing by for collaboration\n`;
}
result += `╚═ 🔄 Live status • Smart pointer network active`;
// FULL RICH CONTEXT DATA FOR CLAUDE (not truncated)
const fullContextData = {
humanDisplay: result,
instances: richContexts,
networkStatus: {
totalInstances: instances.length,
pendingContexts: pending,
networkHealth: instances.length > 0 ? 'active' : 'solo'
},
recommendations: instances.length > 0 ? {
bestCollaboration: richContexts[0]?.pid,
readyForHandoff: richContexts.filter((c: any) => c.collaborationScore > 0.7).map((c: any) => c.pid),
allContexts: richContexts.map((c: any) => ({
pid: c.pid,
project: c.project,
fullWorkingDir: c.activity.workingDir,
currentTask: c.contextData.currentTask,
gitContext: c.contextData.gitContext,
activeFiles: c.contextData.activeFiles,
collaborationScore: c.collaborationScore,
lastActivity: c.activity.lastActivity,
contextPointers: c.contextData.contextPointers
}))
} : null
};
return {
content: [{
type: 'text',
text: result + '\n\n' + JSON.stringify(fullContextData, null, 2)
}],
};
}
case 'register': {
this.sessionManager.ensureInitialized();
const task = args?.task as string | undefined;
const status = (args?.status as string) || 'active';
// Use existing updateStatus method which re-registers the session
this.sessionManager.updateStatus(status, task);
const pid = process.ppid;
const result = {
humanDisplay: `✅ Registered as Claude ${pid} in Senator network`,
pid,
instanceFile: `/tmp/claude-senator/claude-${pid}.json`,
task: task || 'Working',
status
};
return {
content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
};
}
default:
throw new McpError(ErrorCode.MethodNotFound, `Unknown tool: ${name}`);
}
} catch (error) {
console.error('Tool execution error:', error);
throw new McpError(
ErrorCode.InternalError,
`Error executing ${request.params.name}: ${error}`
);
}
});
}
private getTimeAgo(timestamp: number): string {
const now = Date.now();
const diffMs = now - timestamp;
const diffSeconds = Math.floor(diffMs / 1000);
const diffMinutes = Math.floor(diffSeconds / 60);
const diffHours = Math.floor(diffMinutes / 60);
if (diffHours > 0) return `${diffHours}h ago`;
if (diffMinutes > 0) return `${diffMinutes}m ago`;
return `${diffSeconds}s ago`;
}
private generateRichContextDisplay(instance: any, activity: any): any {
try {
// Use smart pointer system to extract rich context (similar to send_context)
const workingDir = instance.cwd || instance.projectPath || '/unknown';
const encodedPath = String(workingDir).replace(/\//g, '-').replace(/^-/, '');
// Smart pointers to ~/.claude data
const contextPointers = {
projectsDir: `~/.claude/projects/${encodedPath}`,
conversationHistory: this.getConversationPointer(workingDir),
gitContext: this.getGitContextString(workingDir),
activeFiles: this.getActiveFilesArray(workingDir),
currentTask: activity.task || 'Active',
collaborationIntent: this.determineCollaborationReadiness(activity, instance)
};
// Extract collaboration readiness metrics
const collaborationMetrics = {
hasRecentActivity: activity.lastActivity > Date.now() - 300000, // 5 minutes
hasActiveFiles: contextPointers.activeFiles.length > 0,
hasCleanGitState: !contextPointers.gitContext.includes('dirty'),
isWorkingOnKnownTask: activity.task && activity.task !== 'Active',
projectType: this.detectProjectType(workingDir)
};
return {
...contextPointers,
collaborationMetrics,
collaborationReason: this.getCollaborationReason(collaborationMetrics),
readinessScore: this.calculateReadinessScore(collaborationMetrics),
lastActivity: activity.lastActivity,
workingDir
};
} catch (error) {
console.error(`[Claude Senator] Error generating rich context for PID ${instance.pid}:`, error);
return {
currentTask: 'Unknown',
gitContext: 'no-git',
activeFiles: [],
collaborationMetrics: { hasRecentActivity: false, hasActiveFiles: false, hasCleanGitState: false, isWorkingOnKnownTask: false },
collaborationReason: 'Context unavailable',
readinessScore: 0
};
}
}
private calculateCollaborationScore(contextData: any): number {
const metrics = contextData.collaborationMetrics;
if (!metrics) return 0;
let score = 0;
if (metrics.hasRecentActivity) score += 0.3;
if (metrics.hasActiveFiles) score += 0.2;
if (metrics.hasCleanGitState) score += 0.2;
if (metrics.isWorkingOnKnownTask) score += 0.2;
if (metrics.projectType !== 'unknown') score += 0.1;
return Math.min(score, 1.0);
}
private getStatusIcon(contextData: any): string {
const score = contextData.readinessScore || 0;
const metrics = contextData.collaborationMetrics;
if (score > 0.8) return '🟢'; // Highly ready
if (score > 0.6) return '🟡'; // Moderately ready
if (score > 0.3) return '🔴'; // Less ready
if (metrics?.hasRecentActivity) return '⚡'; // Active but not ready
return '⚪'; // Idle
}
private getConversationPointer(workingDir: string): string | null {
try {
const { homedir } = require('os');
const { join, existsSync, readdirSync } = require('fs');
const encodedPath = String(workingDir).replace(/\//g, '-').replace(/^-/, '');
const projectDir = join(homedir(), '.claude', 'projects', encodedPath);
if (existsSync(projectDir)) {
const files = readdirSync(projectDir).filter((f: string) => f.endsWith('.jsonl'));
if (files.length > 0) {
return `~/.claude/projects/${encodedPath}/${files[files.length - 1]}`;
}
}
return null;
} catch (error) {
return null;
}
}
private getGitContextString(workingDir: string): string {
try {
const { execSync } = require('child_process');
const branch = execSync('git branch --show-current 2>/dev/null', {
cwd: workingDir,
encoding: 'utf8'
}).trim();
const commit = execSync('git rev-parse --short HEAD 2>/dev/null', {
cwd: workingDir,
encoding: 'utf8'
}).trim();
const status = execSync('git status --porcelain 2>/dev/null', {
cwd: workingDir,
encoding: 'utf8'
}).trim();
const dirty = status ? '+dirty' : '';
return `${branch}@${commit}${dirty}`;
} catch (error) {
return 'no-git';
}
}
private getActiveFilesArray(workingDir: string): string[] {
try {
const { execSync } = require('child_process');
const recentFiles = execSync('git diff --name-only HEAD~1 2>/dev/null', {
cwd: workingDir,
encoding: 'utf8'
}).trim().split('\n').filter((f: string) => f);
return recentFiles.slice(0, 5);
} catch (error) {
return [];
}
}
private determineCollaborationReadiness(activity: any, instance: any): string {
const task = activity.task || '';
const files = activity.files || [];
if (task.includes('error') || task.includes('debug')) return 'debugging_needed';
if (task.includes('test') || task.includes('implement')) return 'active_development';
if (task.includes('review') || task.includes('analyze')) return 'code_review';
if (files.length > 0) return 'file_operations';
if (activity.lastActivity > Date.now() - 60000) return 'recent_activity';
return 'general_availability';
}
private detectProjectType(workingDir: string): string {
try {
const { join, existsSync } = require('fs');
if (existsSync(join(workingDir, 'package.json'))) return 'nodejs';
if (existsSync(join(workingDir, 'Cargo.toml'))) return 'rust';
if (existsSync(join(workingDir, 'requirements.txt'))) return 'python';
if (existsSync(join(workingDir, 'pom.xml'))) return 'java';
if (existsSync(join(workingDir, 'go.mod'))) return 'go';
if (existsSync(join(workingDir, '.git'))) return 'git-project';
return 'unknown';
} catch (error) {
return 'unknown';
}
}
private getCollaborationReason(metrics: any): string {
if (metrics.hasRecentActivity && metrics.isWorkingOnKnownTask) return 'actively working on known task';
if (metrics.hasActiveFiles && metrics.hasCleanGitState) return 'clean state with active files';
if (metrics.hasRecentActivity) return 'recently active';
if (metrics.isWorkingOnKnownTask) return 'working on specific task';
if (metrics.hasActiveFiles) return 'has active files';
return 'general availability';
}
private calculateReadinessScore(metrics: any): number {
let score = 0;
if (metrics.hasRecentActivity) score += 0.3;
if (metrics.hasActiveFiles) score += 0.25;
if (metrics.hasCleanGitState) score += 0.2;
if (metrics.isWorkingOnKnownTask) score += 0.2;
if (metrics.projectType !== 'unknown') score += 0.05;
return Math.min(score, 1.0);
}
async run(): Promise<void> {
const transport = new StdioServerTransport();
await this.server.connect(transport);
console.error('[Claude Senator] Inter-Claude communication MCP server running 🚀');
}
}
// Handle command line arguments
const args = process.argv.slice(2);
if (args.includes('--help') || args.includes('-h')) {
console.log(`
Claude Senator - Inter-Claude Communication MCP Server
Enables Claude instances to communicate, share memories, and coordinate work.
Usage:
npx claude-senator # Start MCP server (stdio mode)
npx claude-senator --config # Show configuration snippet
npx claude-senator --help # Show this help
Installation:
claude mcp add claude-senator -- npx claude-senator
Configuration snippet for ~/.claude/.claude.json:
{
"claude-senator": {
"command": "npx",
"args": ["claude-senator"],
"env": {}
}
}
Features:
🔍 Discover other Claude instances
💬 Send messages between Claudes
🎯 Inject commands into other Claude input boxes
🔄 Transfer conversation context
🧠 Search shared conversation memories
📊 Real-time status monitoring
`);
process.exit(0);
}
if (args.includes('--config')) {
console.log(
JSON.stringify(
{
'claude-senator': {
command: 'npx',
args: ['claude-senator'],
env: {},
},
},
null,
2
)
);
process.exit(0);
}
// Start the server
const server = new ClaudeSenatorServer();
server.run().catch(console.error);