#!/usr/bin/env node
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import {
CallToolRequestSchema,
ListToolsRequestSchema,
Tool,
} from '@modelcontextprotocol/sdk/types.js';
import { config } from './config/env.js';
import { ProjectBrain } from './core/ProjectBrain.js';
import {
CheckpointInputSchema,
RollbackInputSchema,
} from './types/schema.js';
import { logger } from './utils/logger.js';
/**
* MCP Server for Claude Infinite Context
*/
class InfiniteContextServer {
private server: Server;
private brain: ProjectBrain;
private initialized = false;
constructor() {
this.server = new Server(
{
name: 'infinite-context',
version: '1.0.0',
},
{
capabilities: {
tools: {},
},
}
);
this.brain = new ProjectBrain(
config.REDIS_URL,
config.GEMINI_API_KEY,
config.ANTHROPIC_API_KEY || '',
config.PROJECT_ROOT
);
this.setupHandlers();
this.setupErrorHandling();
}
/**
* Sets up MCP request handlers
*/
private setupHandlers(): void {
// List available tools
this.server.setRequestHandler(ListToolsRequestSchema, async () => {
const tools: Tool[] = [
{
name: 'checkpoint',
description:
'Save current context to Redis before running /clear. Merges new context with existing project state using LLM-based summarization.',
inputSchema: {
type: 'object',
properties: {
context: {
type: 'string',
description: 'The current work context to checkpoint (summary of recent work, decisions, files)',
},
token_count: {
type: 'number',
description: 'Current token budget usage',
},
},
required: ['context', 'token_count'],
},
},
{
name: 'resume',
description:
'Load the last checkpoint at session start. Returns formatted context to inject into the conversation.',
inputSchema: {
type: 'object',
properties: {},
},
},
{
name: 'rollback',
description:
'Revert to a previous checkpoint version. Useful if a merge produced incorrect results.',
inputSchema: {
type: 'object',
properties: {
steps: {
type: 'number',
description: 'Number of versions to roll back (default: 1)',
default: 1,
},
},
},
},
{
name: 'status',
description:
'Show current state metadata including version, active files, tasks, token usage, and checkpoint history.',
inputSchema: {
type: 'object',
properties: {},
},
},
];
return { tools };
});
// Handle tool calls
this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
try {
// Lazy initialization on first tool call
if (!this.initialized) {
await this.brain.initialize();
this.initialized = true;
}
const { name, arguments: args } = request.params;
switch (name) {
case 'checkpoint': {
const input = CheckpointInputSchema.parse(args);
const result = await this.brain.checkpoint(
input.context,
input.token_count
);
return {
content: [{ type: 'text', text: result }],
};
}
case 'resume': {
const result = await this.brain.resume();
return {
content: [{ type: 'text', text: result }],
};
}
case 'rollback': {
const input = RollbackInputSchema.parse(args || {});
const result = await this.brain.rollback(input.steps);
return {
content: [{ type: 'text', text: result }],
};
}
case 'status': {
const result = await this.brain.status();
return {
content: [{ type: 'text', text: result }],
};
}
default:
throw new Error(`Unknown tool: ${name}`);
}
} catch (error) {
logger.error('Tool execution failed', { error, tool: request.params.name });
return {
content: [
{
type: 'text',
text: this.formatUserError(error, request.params.name),
},
],
isError: true,
};
}
});
}
private formatUserError(error: unknown, tool: string): string {
const baseMessage = error instanceof Error ? error.message : String(error);
const guidance = this.getErrorGuidance(baseMessage, tool);
return guidance ? `${baseMessage}\n\nSuggestion: ${guidance}` : baseMessage;
}
private getErrorGuidance(message: string, tool: string): string | null {
if (message.includes('Redis')) {
return 'Ensure Redis Stack is running with: redis-stack-server';
}
if (message.includes('API key')) {
return 'Check your .env file and ensure API keys are set correctly';
}
if (message.includes('Session ID')) {
return 'Try deleting .claude_session_id and restarting';
}
if (tool === 'rollback' && message.includes('available')) {
return 'No checkpoint history found. Create a checkpoint first using the checkpoint tool';
}
return null;
}
/**
* Sets up error handling and graceful shutdown
*/
private setupErrorHandling(): void {
const shutdown = async () => {
logger.info('Shutting down server...');
if (this.initialized) {
await this.brain.shutdown();
}
process.exit(0);
};
process.on('SIGINT', shutdown);
process.on('SIGTERM', shutdown);
process.on('uncaughtException', (error) => {
logger.error('Uncaught exception', { error });
shutdown();
});
process.on('unhandledRejection', (reason) => {
logger.error('Unhandled rejection', { reason });
shutdown();
});
}
/**
* Starts the MCP server
*/
async start(): Promise<void> {
const transport = new StdioServerTransport();
await this.server.connect(transport);
logger.info('Infinite Context MCP server started', {
projectRoot: config.PROJECT_ROOT,
redisUrl: config.REDIS_URL,
});
}
}
// Start the server
const server = new InfiniteContextServer();
server.start().catch((error) => {
console.error('Failed to start server:', error);
process.exit(1);
});