claudepoint
Version:
The safest way to experiment with Claude Code. Create instant checkpoints, experiment fearlessly, restore instantly.
866 lines (779 loc) ⢠26.4 kB
JavaScript
/**
* ClaudePoint MCP Server
* The safest way to experiment with Claude Code
*
* GitHub: https://github.com/Andycufari/ClaudePoint
*/
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import {
CallToolRequestSchema,
ListToolsRequestSchema
} from '@modelcontextprotocol/sdk/types.js';
import CheckpointManager from './lib/checkpoint-manager.js';
import { initializeSlashCommands } from './lib/slash-commands.js';
import { createRequire } from 'module';
const require = createRequire(import.meta.url);
const packageJson = require('../package.json');
class ClaudePointMCPServer {
constructor() {
try {
this.server = new Server(
{
name: 'claudepoint',
version: packageJson.version,
},
{
capabilities: {
tools: {},
},
}
);
// Get the working directory from MCP environment or current directory
// Claude Code sets the cwd to the project directory when launching MCP servers
const workingDir = process.cwd();
console.error(`[claudepoint] Using working directory: ${workingDir}`);
console.error(`[claudepoint] Process started from: ${process.cwd()}`);
console.error(`[claudepoint] Environment CWD: ${process.env.PWD || 'not set'}`);
this.manager = new CheckpointManager(workingDir);
this.setupToolHandlers();
} catch (error) {
console.error('Failed to initialize ClaudePoint MCP server:', error);
console.error('Error details:', error.stack);
throw error;
}
}
setupToolHandlers() {
// List available tools
this.server.setRequestHandler(ListToolsRequestSchema, async () => {
return {
tools: [
{
name: 'create_claudepoint',
description: 'š¾ Deploy a new claudepoint // Lock in your digital DNA and experiment fearlessly',
inputSchema: {
type: 'object',
properties: {
name: {
type: 'string',
description: 'Optional custom name for the checkpoint'
},
description: {
type: 'string',
description: 'Description of what this checkpoint represents'
}
}
}
},
{
name: 'list_claudepoints',
description: 'šļø Browse your claudepoint vault // View your collection of digital artifacts',
inputSchema: {
type: 'object',
properties: {}
}
},
{
name: 'restore_claudepoint',
description: 'š Time travel to a specific claudepoint // Precision restoration with emergency backup',
inputSchema: {
type: 'object',
properties: {
claudepoint: {
type: 'string',
description: 'Name or partial name of the claudepoint to restore'
},
dry_run: {
type: 'boolean',
description: 'Preview changes without actually restoring',
default: false
}
},
required: ['claudepoint']
}
},
{
name: 'setup_claudepoint',
description: 'š Initialize ClaudePoint // Creates .claudepoint vault and activates stealth mode',
inputSchema: {
type: 'object',
properties: {}
}
},
{
name: 'get_changelog',
description: 'š” Access development history // View your coding adventure timeline',
inputSchema: {
type: 'object',
properties: {}
}
},
{
name: 'set_changelog',
description: 'Add a custom entry to the development history (for Claude Code to log what changes were made)',
inputSchema: {
type: 'object',
properties: {
description: {
type: 'string',
description: 'Brief description of what changes were made'
},
details: {
type: 'string',
description: 'Optional detailed explanation of the changes'
},
action_type: {
type: 'string',
description: 'Type of action (e.g., REFACTOR, ADD_FEATURE, BUG_FIX, OPTIMIZATION)',
default: 'CODE_CHANGE'
}
},
required: ['description']
}
},
{
name: 'init_slash_commands',
description: 'š Deploy slash command arsenal // Install claudepoint commands in Claude Code',
inputSchema: {
type: 'object',
properties: {}
}
},
{
name: 'undo_claudepoint',
description: 'š Instant time hack // Quick restore to your last claudepoint',
inputSchema: {
type: 'object',
properties: {}
}
},
{
name: 'get_changes',
description: 'š Scan for changes // See what\'s different since your last claudepoint',
inputSchema: {
type: 'object',
properties: {}
}
},
{
name: 'configure_claudepoint',
description: 'āļø Enter configuration mode // View and tune your claudepoint settings',
inputSchema: {
type: 'object',
properties: {}
}
}
]
};
});
// Handle tool calls
this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { name, arguments: args } = request.params;
try {
switch (name) {
case 'create_claudepoint':
return await this.handleCreateClaudepoint(args);
case 'list_claudepoints':
return await this.handleListClaudepoints(args);
case 'restore_claudepoint':
return await this.handleRestoreClaudepoint(args);
case 'undo_claudepoint':
return await this.handleUndoClaudepoint(args);
case 'get_changes':
return await this.handleGetChanges(args);
case 'configure_claudepoint':
return await this.handleConfigureClaudepoint(args);
case 'setup_claudepoint':
return await this.handleSetup(args);
case 'get_changelog':
return await this.handleGetChangelog(args);
case 'set_changelog':
return await this.handleSetChangelog(args);
case 'init_slash_commands':
return await this.handleInitSlashCommands(args);
default:
throw new Error(`Unknown tool: ${name}`);
}
} catch (error) {
return {
content: [
{
type: 'text',
text: `Error: ${error.message}`
}
],
isError: true
};
}
});
}
async handleCreateClaudepoint(args) {
const { name, description } = args || {};
console.error(`[claudepoint] Creating claudepoint: name=${name}, desc=${description}`);
console.error(`[claudepoint] Working in: ${process.cwd()}`);
try {
// Add timeout protection
const timeoutPromise = new Promise((_, reject) => {
setTimeout(() => reject(new Error('Operation timed out after 10 seconds')), 10000);
});
const operationPromise = (async () => {
await this.manager.ensureDirectories();
console.error(`[claudepoint] Getting project files...`);
const files = await this.manager.getProjectFiles();
console.error(`[claudepoint] Found ${files.length} files`);
return files;
})();
const files = await Promise.race([operationPromise, timeoutPromise]);
if (files.length === 0) {
return {
content: [
{
type: 'text',
text: 'šØ No files found to deploy! Make sure you\'re in a project directory and run setup first.'
}
]
};
}
const result = await this.manager.create(name, description);
if (result.success) {
const successMsg = this.manager.getRandomMessage(this.manager.successMessages);
return {
content: [
{
type: 'text',
text: `${successMsg}\n š¾ Name: ${result.name}\n š Files: ${result.fileCount}\n š Size: ${result.size}\n š Description: ${result.description || 'Manual claudepoint'}`
}
]
};
} else if (result.noChanges) {
return {
content: [
{
type: 'text',
text: 'š¤ Codebase is stable // No changes detected since last claudepoint'
}
]
};
} else {
return {
content: [
{
type: 'text',
text: `šØ Deploy failed: ${result.error}`
}
]
};
}
} catch (error) {
return {
content: [
{
type: 'text',
text: `šØ Error deploying claudepoint: ${error.message}`
}
]
};
}
}
async handleSetChangelog(args) {
const { description, details, action_type = 'CODE_CHANGE' } = args || {};
if (!description) {
return {
content: [
{
type: 'text',
text: 'ā Error: Description is required for changelog entry'
}
]
};
}
try {
await this.manager.logToChangelog(action_type, description, details);
return {
content: [
{
type: 'text',
text: `ā
Changelog entry added: ${description}`
}
]
};
} catch (error) {
return {
content: [
{
type: 'text',
text: `ā Error adding changelog entry: ${error.message}`
}
]
};
}
}
async handleGetChangelog(args) {
try {
const changelog = await this.manager.getChangelog();
if (changelog.length === 0) {
return {
content: [
{
type: 'text',
text: 'š No development history found. Start creating checkpoints to build your project timeline!'
}
]
};
}
let output = `š Development History (${changelog.length} entries):\n\n`;
changelog.slice(0, 10).forEach((entry, index) => {
output += `${index + 1}. **${entry.action}** - ${entry.timestamp}\n`;
output += ` ${entry.description}\n`;
if (entry.details) {
output += ` _${entry.details}_\n`;
}
output += '\n';
});
if (changelog.length > 10) {
output += `... and ${changelog.length - 10} more entries. Use CLI 'claudepoint changelog' for full history.`;
}
return {
content: [
{
type: 'text',
text: output
}
]
};
} catch (error) {
return {
content: [
{
type: 'text',
text: `ā Error getting changelog: ${error.message}`
}
]
};
}
}
async handleListClaudepoints(args) {
try {
const claudepoints = await this.manager.getCheckpoints();
if (claudepoints.length === 0) {
return {
content: [
{
type: 'text',
text: 'š¤ No claudepoints found in the vault. Deploy your first with create_claudepoint!'
}
]
};
}
const listMsg = this.manager.getRandomMessage(this.manager.listMessages);
let output = `${listMsg}\nš¦ Total claudepoints: ${claudepoints.length}\n\n`;
claudepoints.forEach((cp, index) => {
const date = new Date(cp.timestamp).toLocaleString();
output += `${index + 1}. š¾ ${cp.name}\n`;
output += ` š ${cp.description}\n`;
output += ` š
${date} | ${cp.fileCount} files | ${this.manager.formatSize(cp.totalSize)}\n\n`;
});
return {
content: [
{
type: 'text',
text: output
}
]
};
} catch (error) {
return {
content: [
{
type: 'text',
text: `šØ Error browsing vault: ${error.message}`
}
]
};
}
}
async handleRestoreClaudepoint(args) {
const { claudepoint, dry_run = false } = args || {};
try {
const checkpoints = await this.manager.getCheckpoints();
const targetCheckpoint = checkpoints.find(cp =>
cp.name === claudepoint || cp.name.includes(claudepoint)
);
if (!targetCheckpoint) {
const available = checkpoints.slice(0, 5).map(cp => ` - ${cp.name}`).join('\n');
return {
content: [
{
type: 'text',
text: `šØ Claudepoint not found: ${claudepoint}\n\nAvailable claudepoints:\n${available}`
}
]
};
}
if (dry_run) {
const currentFiles = await this.manager.getProjectFiles();
const filesToDelete = currentFiles.filter(f => !targetCheckpoint.files.includes(f));
let output = `š DRY RUN - Would restore: ${targetCheckpoint.name}\n`;
output += ` š Description: ${targetCheckpoint.description}\n`;
output += ` š
Date: ${new Date(targetCheckpoint.timestamp).toLocaleString()}\n`;
output += ` š Files: ${targetCheckpoint.fileCount}\n`;
if (filesToDelete.length > 0) {
output += ` šļø Would delete ${filesToDelete.length} files that didn't exist in checkpoint\n`;
}
output += '\nUse restore_claudepoint without dry_run to proceed.';
return {
content: [
{
type: 'text',
text: output
}
]
};
}
// Perform actual restore
const result = await this.manager.restore(claudepoint, false);
if (result.success) {
return {
content: [
{
type: 'text',
text: `${this.manager.getRandomMessage(this.manager.undoMessages)}\n š Emergency backup: ${result.emergencyBackup}\n š Restored: ${targetCheckpoint.name}\n š Files restored: ${targetCheckpoint.fileCount}`
}
]
};
} else {
return {
content: [
{
type: 'text',
text: `šØ Time travel failed: ${result.error}`
}
]
};
}
} catch (error) {
return {
content: [
{
type: 'text',
text: `šØ Time travel error: ${error.message}`
}
]
};
}
}
async handleInitSlashCommands(args) {
try {
const result = await initializeSlashCommands();
if (result.success) {
let output = 'š ClaudePoint slash commands initialized!\n\n';
output += 'ā
Created .claude/commands directory\n';
output += 'ā
Added complete claudepoint command arsenal\n';
output += 'ā
Created 7 essential slash commands\n';
output += '\nš” Main slash commands:\n';
output += ' ⢠/claudepoint - Deploy a new claudepoint\n';
output += ' ⢠/undo - Instant time hack to last claudepoint\n';
output += ' ⢠/claudepoint-restore - Time travel with interactive selection\n';
output += ' ⢠/claudepoint-list - Browse your claudepoint vault\n';
output += ' ⢠/changes - Scan for modifications\n';
output += ' ⢠/claudepoint-changelog - View history\n';
output += ' ⢠/ultrathink - Activate deep reasoning mode\n';
output += '\nšÆ Type / in Claude Code to see and use these commands!';
return {
content: [
{
type: 'text',
text: output
}
]
};
} else {
return {
content: [
{
type: 'text',
text: `ā Failed to initialize slash commands: ${result.error}`
}
]
};
}
} catch (error) {
return {
content: [
{
type: 'text',
text: `ā Error initializing slash commands: ${error.message}`
}
]
};
}
}
async handleSetup(args) {
try {
const result = await this.manager.setup();
if (result.success) {
let output = 'š¾ ClaudePoint is ONLINE!\n\n';
output += '⨠Created .claudepoint vault\n';
output += 'š Updated .gitignore (stealth mode activated)\n';
output += 'āļø Configuration loaded\n';
if (result.initialCheckpoint) {
output += `⨠Deployed initial claudepoint: ${result.initialCheckpoint}\n`;
}
output += '\nš Quick command arsenal:\n';
output += ' ⢠create_claudepoint - Deploy a new claudepoint\n';
output += ' ⢠list_claudepoints - Browse your vault\n';
output += ' ⢠restore_claudepoint - Time travel to previous state\n';
output += ' ⢠undo_claudepoint - Quick time hack to last claudepoint\n';
output += ' ⢠get_changes - Scan for code changes\n';
output += ' ⢠get_changelog - View your coding adventure timeline\n';
output += '\nš Tip: Deploy claudepoints before hacking the impossible!';
return {
content: [
{
type: 'text',
text: output
}
]
};
} else {
return {
content: [
{
type: 'text',
text: `šØ Initialization failed: ${result.error}`
}
]
};
}
} catch (error) {
return {
content: [
{
type: 'text',
text: `šØ Error during setup: ${error.message}`
}
]
};
}
}
// š NEW: Quick undo handler
async handleUndoClaudepoint(args) {
try {
const result = await this.manager.undoLastClaudepoint();
if (result.success) {
const undoMsg = this.manager.getRandomMessage(this.manager.undoMessages);
return {
content: [
{
type: 'text',
text: `${undoMsg}\n š”ļø Emergency backup: ${result.emergencyBackup}\n š Restored: ${result.restored}\n š
Back to the future!`
}
]
};
} else if (result.noClaudepoints) {
return {
content: [
{
type: 'text',
text: 'š¤ No claudepoints found to undo. Deploy your first safety net!'
}
]
};
} else {
return {
content: [
{
type: 'text',
text: `šØ Time hack failed: ${result.error}`
}
]
};
}
} catch (error) {
return {
content: [
{
type: 'text',
text: `šØ Error during time hack: ${error.message}`
}
]
};
}
}
// š NEW: Changes handler
async handleGetChanges(args) {
try {
const changes = await this.manager.getChangedFilesSinceLastClaudepoint();
if (changes.error) {
return {
content: [
{
type: 'text',
text: `šØ Scan error: ${changes.error}`
}
]
};
}
if (!changes.hasLastClaudepoint) {
return {
content: [
{
type: 'text',
text: `š No previous claudepoint found // Everything is new!\nš Total files in project: ${changes.totalChanges}\n\nDeploy your first claudepoint to start tracking changes.`
}
]
};
}
if (changes.totalChanges === 0) {
return {
content: [
{
type: 'text',
text: `⨠Codebase is stable // No changes detected\nš Last claudepoint: ${changes.lastClaudepointName}\nš
Created: ${changes.lastClaudepointDate}\n\nšÆ Perfect time to experiment - you're fully protected!`
}
]
};
}
let output = `šÆ Changes detected: ${changes.totalChanges} modifications found\n`;
output += `š Since claudepoint: ${changes.lastClaudepointName}\n`;
output += `š
Created: ${changes.lastClaudepointDate}\n\n`;
if (changes.added.length > 0) {
output += `ā Added files (${changes.added.length}):\n`;
changes.added.slice(0, 5).forEach(file => {
output += ` + ${file}\n`;
});
if (changes.added.length > 5) {
output += ` ... and ${changes.added.length - 5} more\n`;
}
output += '\n';
}
if (changes.modified.length > 0) {
output += `š Modified files (${changes.modified.length}):\n`;
changes.modified.slice(0, 5).forEach(file => {
output += ` ~ ${file}\n`;
});
if (changes.modified.length > 5) {
output += ` ... and ${changes.modified.length - 5} more\n`;
}
output += '\n';
}
if (changes.deleted.length > 0) {
output += `šļø Deleted files (${changes.deleted.length}):\n`;
changes.deleted.slice(0, 5).forEach(file => {
output += ` - ${file}\n`;
});
if (changes.deleted.length > 5) {
output += ` ... and ${changes.deleted.length - 5} more\n`;
}
output += '\n';
}
output += 'š” Ready to lock in these changes? Use create_claudepoint!';
return {
content: [
{
type: 'text',
text: output
}
]
};
} catch (error) {
return {
content: [
{
type: 'text',
text: `šØ Scan error: ${error.message}`
}
]
};
}
}
// āļø NEW: Configuration handler
async handleConfigureClaudepoint(args) {
try {
const status = await this.manager.getConfigurationStatus();
const configMsg = this.manager.getRandomMessage(this.manager.configMessages);
let output = `${configMsg}\n\n`;
output += `šļø Current Configuration:\n`;
output += ` Max Claudepoints: ${status.maxClaudepoints}\n`;
output += ` Current Claudepoints: ${status.currentClaudepoints}\n`;
output += ` Max Age: ${status.maxAge} days ${status.maxAge === 0 ? '(unlimited)' : ''}\n`;
output += ` Ignore Patterns: ${status.ignorePatterns} rules\n`;
output += ` Auto Naming: ${status.autoName ? 'Enabled' : 'Disabled'}\n`;
output += ` Config File: ${status.configPath}\n\n`;
const config = await this.manager.loadConfig();
if (config.additionalIgnores && config.additionalIgnores.length > 0) {
output += `š· Additional Ignore Patterns:\n`;
config.additionalIgnores.forEach(pattern => {
output += ` ⢠${pattern}\n`;
});
output += '\n';
}
if (config.forceInclude && config.forceInclude.length > 0) {
output += `ā Force Include Patterns:\n`;
config.forceInclude.forEach(pattern => {
output += ` ⢠${pattern}\n`;
});
output += '\n';
}
output += 'šØ Tip: Edit the config file directly or use the CLI setup for interactive configuration.';
return {
content: [
{
type: 'text',
text: output
}
]
};
} catch (error) {
return {
content: [
{
type: 'text',
text: `šØ Configuration error: ${error.message}`
}
]
};
}
}
async start() {
try {
const transport = new StdioServerTransport();
await this.server.connect(transport);
console.error('ClaudePoint MCP server running on stdio');
console.error('Available tools: setup_claudepoint, create_claudepoint, list_claudepoints, restore_claudepoint, undo_claudepoint, get_changes, configure_claudepoint, get_changelog, set_changelog, init_slash_commands');
// Keep the process alive
process.on('SIGINT', () => {
console.error('MCP server shutting down...');
process.exit(0);
});
process.on('SIGTERM', () => {
console.error('MCP server shutting down...');
process.exit(0);
});
} catch (error) {
console.error('Failed to start MCP server:', error);
console.error('Error details:', error.stack);
process.exit(1);
}
}
}
// Start the server
const server = new ClaudePointMCPServer();
server.start().catch(error => {
console.error('Failed to start server:', error);
console.error('Error stack:', error.stack);
process.exit(1);
});
// Handle unhandled promise rejections
process.on('unhandledRejection', (reason, promise) => {
console.error('Unhandled Rejection at:', promise, 'reason:', reason);
// Don't exit the process for unhandled rejections in MCP server
});
// Handle uncaught exceptions
process.on('uncaughtException', (error) => {
console.error('Uncaught Exception:', error);
console.error('Stack:', error.stack);
process.exit(1);
});
export default ClaudePointMCPServer;