Skip to content

Commit 650bcfc

Browse files
authored
Merge pull request #21 from chrisribe/copilot/enhance-mcp-configuration-access
Add MCP configuration file paths to memory-stats command. Now detects common llm clients and displays simple-memory config paths
2 parents 0d5f87f + 4301e74 commit 650bcfc

File tree

5 files changed

+189
-55
lines changed

5 files changed

+189
-55
lines changed

scripts/setup-vscode.js

Lines changed: 23 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -19,14 +19,14 @@ const MCP_CONFIG_WITH_COMMENTS = `{
1919
"servers": {
2020
"simple-memory-mcp": {
2121
"command": "simple-memory"
22-
// 💡 Customize with environment variables:
22+
// 💡 Uncomment and customize environment variables as needed:
2323
// "env": {
24-
// "MEMORY_DB": "/path/to/your/memory.db",
25-
// "MEMORY_BACKUP_PATH": "/path/to/backups",
26-
// "MEMORY_BACKUP_INTERVAL": "1440",
27-
// "DEBUG": "false"
24+
// "MEMORY_DB": "./memory.db", // Custom database location
25+
// "MEMORY_BACKUP_PATH": "./backups", // Enable automatic backups
26+
// "MEMORY_BACKUP_INTERVAL": "1440", // Backup interval in minutes
27+
// "MEMORY_BACKUP_RETENTION": "30", // Keep backups for N days
28+
// "DEBUG": "false" // Enable debug logging
2829
// }
29-
// See README for more options: https://github.com/chrisribe/simple-memory-mcp#configuration
3030
}
3131
}
3232
}`;
@@ -39,8 +39,8 @@ function getVSCodeConfigPaths() {
3939
if (platform === 'win32') {
4040
const appData = process.env.APPDATA || join(home, 'AppData', 'Roaming');
4141
paths.push(
42-
{ name: 'VS Code', path: join(appData, 'Code', 'User') },
43-
{ name: 'VS Code Insiders', path: join(appData, 'Code - Insiders', 'User') }
42+
{ name: 'VS Code', path: join(appData, 'Code', 'User').replace(/\\/g, '/') },
43+
{ name: 'VS Code Insiders', path: join(appData, 'Code - Insiders', 'User').replace(/\\/g, '/') }
4444
);
4545
} else if (platform === 'darwin') {
4646
const appSupport = join(home, 'Library', 'Application Support');
@@ -99,8 +99,7 @@ function configureVSCode(name, vscodeUserPath) {
9999
// Check if already configured
100100
if (mcpConfig[serversProp]['simple-memory-mcp']) {
101101
console.log(`✅ Already configured in ${name}`);
102-
console.log(` 💡 To customize: ${mcpJsonPath}`);
103-
return { success: true, reason: 'already-configured', path: mcpJsonPath };
102+
return { success: true, reason: 'already-configured', path: mcpJsonPath.replace(/\\/g, '/') };
104103
}
105104

106105
// Add simple-memory-mcp config
@@ -110,8 +109,7 @@ function configureVSCode(name, vscodeUserPath) {
110109
mkdirSync(vscodeUserPath, { recursive: true });
111110
writeFileSync(mcpJsonPath, JSON.stringify(mcpConfig, null, 2), 'utf8');
112111
console.log(`✅ Added to ${name} mcp.json`);
113-
console.log(` Location: ${mcpJsonPath}`);
114-
return { success: true, reason: 'configured', path: mcpJsonPath };
112+
return { success: true, reason: 'configured', path: mcpJsonPath.replace(/\\/g, '/') };
115113
} catch (error) {
116114
console.error(`❌ Failed to update ${name} mcp.json:`, error.message);
117115
return { success: false, reason: 'write-error' };
@@ -142,19 +140,25 @@ function main() {
142140
if (foundCount === 0) {
143141
console.log('\nℹ️ No VS Code installations detected');
144142
console.log(' Add this to your VS Code User/mcp.json manually:');
145-
console.log(MCP_CONFIG_WITH_COMMENTS);
146143
} else if (configuredCount > 0) {
147144
console.log('\n🎉 Configuration complete!');
148145
console.log(' Restart VS Code and simple-memory-mcp will be available');
149-
console.log('\n💡 Customization Tips:');
150-
console.log(' • Set custom database location with MEMORY_DB environment variable');
151-
console.log(' • Enable automatic backups with MEMORY_BACKUP_PATH');
152-
console.log(' • Run multiple instances for work/personal contexts');
153-
console.log(' • See README for all configuration options');
154-
console.log('\n📖 Configuration docs: https://github.com/chrisribe/simple-memory-mcp#configuration');
155146
} else {
156147
console.log('\n✅ All installations already configured');
157148
}
149+
150+
// Show example config and instructions (for all cases)
151+
if (foundCount > 0) {
152+
console.log('\n💡 Example configuration with all options:');
153+
console.log(MCP_CONFIG_WITH_COMMENTS);
154+
console.log('\n💡 To find and edit your config file:');
155+
console.log(' Run: node dist/index.js memory-stats');
156+
if (configuredCount > 0) {
157+
console.log('\n📖 Configuration docs: https://github.com/chrisribe/simple-memory-mcp#configuration');
158+
}
159+
} else {
160+
console.log(MCP_CONFIG_WITH_COMMENTS);
161+
}
158162
}
159163

160164
main();

src/services/backup-service.ts

Lines changed: 47 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -27,53 +27,63 @@ export class BackupService {
2727
mkdirSync(this.config.backupPath, { recursive: true });
2828
debugLog(`Backup directory ready: ${this.config.backupPath}`);
2929

30-
// Initialize lastBackupTime from most recent backup file (for CLI persistence)
31-
this.initializeLastBackupTime();
30+
// Load lastBackupTime from metadata file (for CLI persistence)
31+
this.lastBackupTime = this.getLastBackupTime();
3232
} catch (error: any) {
3333
debugLog(`Warning: Could not create backup directory: ${error.message}`);
3434
}
3535
}
3636

3737
/**
38-
* Initialize lastBackupTime from the most recent backup file
39-
* This allows throttling to work across CLI invocations
38+
* Get path to backup metadata file
4039
*/
41-
private initializeLastBackupTime(): void {
40+
private getMetadataPath(): string {
41+
return join(this.config.backupPath, '.backup-metadata.json');
42+
}
43+
44+
/**
45+
* Save backup metadata
46+
* @returns true if saved successfully, false on error
47+
*/
48+
private saveMetadata(): boolean {
49+
try {
50+
const metadata = {
51+
lastBackupTime: this.lastBackupTime,
52+
lastBackupDate: new Date(this.lastBackupTime).toISOString()
53+
};
54+
const { writeFileSync } = require('fs');
55+
writeFileSync(this.getMetadataPath(), JSON.stringify(metadata, null, 2));
56+
return true;
57+
} catch (error: any) {
58+
debugLog(`Warning: Could not save backup metadata: ${error.message}`);
59+
return false;
60+
}
61+
}
62+
63+
/**
64+
* Get last backup time from metadata file
65+
* @returns timestamp in milliseconds, or 0 if no metadata exists
66+
*/
67+
private getLastBackupTime(): number {
4268
try {
4369
if (!existsSync(this.config.backupPath)) {
4470
debugLog(`Backup path does not exist yet: ${this.config.backupPath}`);
45-
return;
71+
return 0;
4672
}
4773

48-
const files = readdirSync(this.config.backupPath)
49-
.filter(f => f.endsWith('.db') && f.includes('_auto'))
50-
.map(f => {
51-
const filePath = join(this.config.backupPath, f);
52-
const stats = statSync(filePath);
53-
return {
54-
name: f,
55-
path: filePath,
56-
time: stats.mtime.getTime(),
57-
mtimeDate: stats.mtime
58-
};
59-
})
60-
.sort((a, b) => b.time - a.time); // Newest first
61-
62-
if (files.length > 0) {
63-
const mostRecent = files[0];
64-
this.lastBackupTime = mostRecent.time;
65-
const now = Date.now();
66-
const ageMinutes = Math.floor((now - this.lastBackupTime) / 60000);
67-
debugLog(`📁 Found ${files.length} existing auto backup(s)`);
68-
debugLog(`📅 Most recent: ${mostRecent.name}`);
69-
debugLog(`🕐 Last backup time: ${new Date(this.lastBackupTime).toISOString()}`);
70-
debugLog(`🕐 Current time: ${new Date(now).toISOString()}`);
71-
debugLog(`⏱️ Age: ${ageMinutes} minutes (${Math.floor((now - this.lastBackupTime) / 1000)} seconds)`);
74+
const metadataPath = this.getMetadataPath();
75+
if (existsSync(metadataPath)) {
76+
const { readFileSync } = require('fs');
77+
const metadata = JSON.parse(readFileSync(metadataPath, 'utf-8'));
78+
debugLog(`📋 Loaded backup metadata: last backup at ${metadata.lastBackupDate}`);
79+
return metadata.lastBackupTime;
7280
} else {
73-
debugLog(`No existing auto backups found in ${this.config.backupPath}`);
81+
debugLog(`📋 No backup metadata found (will create on first backup)`);
82+
return 0;
7483
}
7584
} catch (error: any) {
76-
debugLog(`Warning: Could not initialize last backup time: ${error.message}`);
85+
debugLog(`Warning: Could not read last backup time: ${error.message}`);
86+
return 0;
7787
}
7888
}
7989

@@ -117,6 +127,7 @@ export class BackupService {
117127
}
118128

119129
this.lastBackupTime = Date.now();
130+
this.saveMetadata(); // Persist backup time
120131
debugLog(`✅ Backup created: ${backupPath}`);
121132

122133
// Clean up old backups
@@ -199,6 +210,7 @@ export class BackupService {
199210

200211
/**
201212
* Clean up old backups, keeping only the most recent N
213+
* Uses filename timestamps for reliable ordering
202214
*/
203215
private cleanupOldBackups(): void {
204216
if (!this.config.maxBackups || this.config.maxBackups <= 0) {
@@ -210,10 +222,10 @@ export class BackupService {
210222
.filter(f => f.endsWith('.db'))
211223
.map(f => ({
212224
name: f,
213-
path: join(this.config.backupPath, f),
214-
time: statSync(join(this.config.backupPath, f)).mtime.getTime()
225+
path: join(this.config.backupPath, f)
215226
}))
216-
.sort((a, b) => b.time - a.time); // Newest first
227+
// Sort by filename (which contains timestamp) - newest first
228+
.sort((a, b) => b.name.localeCompare(a.name));
217229

218230
// Delete backups beyond the max count
219231
const toDelete = files.slice(this.config.maxBackups);

src/services/memory-service.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { debugLog, debugLogHash } from '../utils/debug.js';
99
import { runMigrations } from './migrations.js';
1010
import { DatabaseOptimizer } from './database-optimizer.js';
1111
import { BackupService, BackupConfig } from './backup-service.js';
12+
import { getMCPConfigPaths, type MCPConfigPath } from '../utils/mcp-config.js';
1213
import type { ExportFilters, ImportOptions, ImportResult, ExportFormat, ExportedMemory } from '../types/tools.js';
1314

1415
// Get package version for export metadata
@@ -53,6 +54,7 @@ export interface MemoryStats {
5354
backupCount?: number;
5455
lastBackupAge?: number; // minutes since last backup
5556
nextBackupIn?: number; // minutes until next backup (-1 if will backup on next write)
57+
mcpConfigPaths?: MCPConfigPath[]; // MCP configuration file paths with matching server entries
5658
}
5759

5860
/**
@@ -671,7 +673,7 @@ export class MemoryService {
671673
dbSize: (this.db.pragma('page_size', { simple: true }) as number) *
672674
(this.db.pragma('page_count', { simple: true }) as number),
673675
dbPath: this.dbPath,
674-
resolvedPath: this.resolvedDbPath, // Use cached value
676+
resolvedPath: this.resolvedDbPath.replace(/\\/g, '/'), // Normalize to forward slashes
675677
schemaVersion
676678
};
677679

@@ -695,6 +697,9 @@ export class MemoryService {
695697
}
696698
}
697699

700+
// Add MCP configuration file paths (only existing ones)
701+
stats.mcpConfigPaths = getMCPConfigPaths().filter(p => p.exists);
702+
698703
debugLog('MemoryService: Stats:', stats);
699704
return stats;
700705
}

src/tests/memory-server-tests.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -196,7 +196,30 @@ async function testMemoryStats(): Promise<void> {
196196
throw new Error(`Expected at least 5 memories, got ${output.totalMemories}`);
197197
}
198198

199+
// Verify MCP config paths are included
200+
if (!output.mcpConfigPaths || !Array.isArray(output.mcpConfigPaths)) {
201+
throw new Error('Memory stats did not include mcpConfigPaths');
202+
}
203+
204+
// Verify each path has the expected structure
205+
for (const configPath of output.mcpConfigPaths) {
206+
if (!configPath.name || !configPath.path || typeof configPath.exists !== 'boolean') {
207+
throw new Error('MCP config path missing required fields');
208+
}
209+
// matchingServers is optional, but if present should be an array
210+
if (configPath.matchingServers && !Array.isArray(configPath.matchingServers)) {
211+
throw new Error('MCP config path matchingServers should be an array');
212+
}
213+
}
214+
199215
console.log(`✓ Database contains ${output.totalMemories} memories, ${output.totalRelationships} relationships`);
216+
console.log(`✓ Found ${output.mcpConfigPaths.length} MCP config paths`);
217+
218+
// Show helpful info about matching configs
219+
const matching = output.mcpConfigPaths.filter((c: any) => c.matchingServers?.length > 0);
220+
if (matching.length > 0) {
221+
console.log(`✓ Found ${matching.length} config(s) with matching server entries`);
222+
}
200223
}
201224

202225
/**

src/utils/mcp-config.ts

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
import { existsSync, readFileSync } from 'fs';
2+
import { join } from 'path';
3+
import { homedir } from 'os';
4+
5+
export interface MCPConfigPath {
6+
name: string;
7+
path: string;
8+
exists: boolean;
9+
matchingServers?: string[]; // Server names that match current MEMORY_DB
10+
}
11+
12+
/**
13+
* Normalize path to use forward slashes for better cross-platform copy/paste
14+
*/
15+
function normalizePath(p: string): string {
16+
return p.replace(/\\/g, '/');
17+
}
18+
19+
/**
20+
* Get potential MCP configuration file paths across multiple clients
21+
* Returns an array of config paths with their existence status and matching server entries
22+
*/
23+
export function getMCPConfigPaths(): MCPConfigPath[] {
24+
const platform = process.platform;
25+
const home = homedir();
26+
const paths: MCPConfigPath[] = [];
27+
28+
if (platform === 'win32') {
29+
const appData = process.env.APPDATA || join(home, 'AppData', 'Roaming');
30+
paths.push(
31+
{ name: 'VS Code', path: normalizePath(join(appData, 'Code', 'User', 'mcp.json')), exists: false },
32+
{ name: 'VS Code Insiders', path: normalizePath(join(appData, 'Code - Insiders', 'User', 'mcp.json')), exists: false },
33+
{ name: 'Cursor', path: normalizePath(join(appData, 'Cursor', 'User', 'mcp.json')), exists: false },
34+
{ name: 'Claude Desktop', path: normalizePath(join(appData, 'Claude', 'claude_desktop_config.json')), exists: false }
35+
);
36+
} else if (platform === 'darwin') {
37+
const appSupport = join(home, 'Library', 'Application Support');
38+
paths.push(
39+
{ name: 'VS Code', path: normalizePath(join(appSupport, 'Code', 'User', 'mcp.json')), exists: false },
40+
{ name: 'VS Code Insiders', path: normalizePath(join(appSupport, 'Code - Insiders', 'User', 'mcp.json')), exists: false },
41+
{ name: 'Cursor', path: normalizePath(join(appSupport, 'Cursor', 'User', 'mcp.json')), exists: false },
42+
{ name: 'Claude Desktop', path: normalizePath(join(appSupport, 'Claude', 'claude_desktop_config.json')), exists: false }
43+
);
44+
} else {
45+
const config = join(home, '.config');
46+
paths.push(
47+
{ name: 'VS Code', path: normalizePath(join(config, 'Code', 'User', 'mcp.json')), exists: false },
48+
{ name: 'VS Code Insiders', path: normalizePath(join(config, 'Code - Insiders', 'User', 'mcp.json')), exists: false },
49+
{ name: 'Cursor', path: normalizePath(join(config, 'Cursor', 'User', 'mcp.json')), exists: false },
50+
{ name: 'Claude Desktop', path: normalizePath(join(config, 'Claude', 'claude_desktop_config.json')), exists: false }
51+
);
52+
}
53+
54+
const currentDb = process.env.MEMORY_DB || 'memory.db';
55+
56+
// Check which paths exist and find matching server entries
57+
for (const pathInfo of paths) {
58+
pathInfo.exists = existsSync(pathInfo.path);
59+
60+
if (pathInfo.exists) {
61+
try {
62+
const content = readFileSync(pathInfo.path, 'utf-8');
63+
const config = JSON.parse(content);
64+
const servers = pathInfo.name === 'Claude Desktop' ? config.mcpServers : config.servers;
65+
66+
if (servers) {
67+
const matching: string[] = [];
68+
for (const [name, serverConfig] of Object.entries(servers)) {
69+
if (typeof serverConfig === 'object' && serverConfig !== null) {
70+
const cmd = (serverConfig as any).command || '';
71+
if (cmd.includes('simple-memory')) {
72+
const configDb = (serverConfig as any).env?.MEMORY_DB || 'memory.db';
73+
if (configDb === currentDb) {
74+
matching.push(name);
75+
}
76+
}
77+
}
78+
}
79+
if (matching.length > 0) {
80+
pathInfo.matchingServers = matching;
81+
}
82+
}
83+
} catch {
84+
// Ignore parse errors
85+
}
86+
}
87+
}
88+
89+
return paths;
90+
}

0 commit comments

Comments
 (0)