claude-self-reflect 3.3.0 → 4.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,289 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Claude Code Statusline Integration Setup
4
+ * Automatically configures CC statusline to show CSR metrics
5
+ */
6
+
7
+ import fs from 'fs';
8
+ import path from 'path';
9
+ import { fileURLToPath } from 'url';
10
+ import { execSync } from 'child_process';
11
+ import os from 'os';
12
+
13
+ const __filename = fileURLToPath(import.meta.url);
14
+ const __dirname = path.dirname(__filename);
15
+
16
+ class StatuslineSetup {
17
+ constructor() {
18
+ this.homeDir = os.homedir();
19
+ this.claudeDir = path.join(this.homeDir, '.claude');
20
+ this.csrScript = path.join(path.dirname(__dirname), 'scripts', 'csr-status');
21
+ this.globalBin = '/usr/local/bin/csr-status';
22
+ this.statuslineWrapper = path.join(this.claudeDir, 'statusline-wrapper.sh');
23
+ this.statuslineBackup = path.join(this.claudeDir, 'statusline-wrapper.sh.backup');
24
+ }
25
+
26
+ log(message, type = 'info') {
27
+ const colors = {
28
+ info: '\x1b[36m',
29
+ success: '\x1b[32m',
30
+ warning: '\x1b[33m',
31
+ error: '\x1b[31m'
32
+ };
33
+ console.log(`${colors[type]}${message}\x1b[0m`);
34
+ }
35
+
36
+ checkPrerequisites() {
37
+ // Check if Claude Code directory exists
38
+ if (!fs.existsSync(this.claudeDir)) {
39
+ this.log('Claude Code directory not found. Please ensure Claude Code is installed.', 'warning');
40
+ return false;
41
+ }
42
+
43
+ // Check if csr-status script exists
44
+ if (!fs.existsSync(this.csrScript)) {
45
+ this.log('CSR status script not found. Please ensure the package is installed correctly.', 'error');
46
+ return false;
47
+ }
48
+
49
+ return true;
50
+ }
51
+
52
+ installGlobalCommand() {
53
+ try {
54
+ // Check if we need sudo
55
+ const needsSudo = !this.canWriteTo('/usr/local/bin');
56
+
57
+ if (fs.existsSync(this.globalBin)) {
58
+ // Check if it's already pointing to our script
59
+ try {
60
+ const target = fs.readlinkSync(this.globalBin);
61
+ if (target === this.csrScript) {
62
+ this.log('Global csr-status command already installed', 'success');
63
+ return true;
64
+ }
65
+ } catch (e) {
66
+ // Not a symlink or can't read, will replace
67
+ }
68
+ }
69
+
70
+ // Create symlink
71
+ const cmd = needsSudo
72
+ ? `sudo ln -sf "${this.csrScript}" "${this.globalBin}"`
73
+ : `ln -sf "${this.csrScript}" "${this.globalBin}"`;
74
+
75
+ if (needsSudo) {
76
+ this.log('Installing global csr-status command (may require password)...', 'info');
77
+ }
78
+
79
+ execSync(cmd, { stdio: 'inherit' });
80
+
81
+ // Make executable
82
+ const chmodCmd = needsSudo
83
+ ? `sudo chmod +x "${this.globalBin}"`
84
+ : `chmod +x "${this.globalBin}"`;
85
+ execSync(chmodCmd);
86
+
87
+ this.log('Global csr-status command installed successfully', 'success');
88
+ return true;
89
+ } catch (error) {
90
+ this.log(`Failed to install global command: ${error.message}`, 'warning');
91
+ this.log('You can manually install by running:', 'info');
92
+ this.log(` sudo ln -sf "${this.csrScript}" "${this.globalBin}"`, 'info');
93
+ return false;
94
+ }
95
+ }
96
+
97
+ canWriteTo(dir) {
98
+ try {
99
+ fs.accessSync(dir, fs.constants.W_OK);
100
+ return true;
101
+ } catch {
102
+ return false;
103
+ }
104
+ }
105
+
106
+ patchStatuslineWrapper() {
107
+ if (!fs.existsSync(this.statuslineWrapper)) {
108
+ this.log('Claude Code statusline wrapper not found. Statusline integration skipped.', 'warning');
109
+ return false;
110
+ }
111
+
112
+ try {
113
+ // Read current wrapper
114
+ let content = fs.readFileSync(this.statuslineWrapper, 'utf8');
115
+
116
+ // Check if already patched with new approach
117
+ if (content.includes('CSR compact status instead of MCP bar')) {
118
+ this.log('Statusline wrapper already patched', 'success');
119
+ return true;
120
+ }
121
+
122
+ // Create backup
123
+ if (!fs.existsSync(this.statuslineBackup)) {
124
+ fs.copyFileSync(this.statuslineWrapper, this.statuslineBackup);
125
+ this.log(`Backup created: ${this.statuslineBackup}`, 'info');
126
+ }
127
+
128
+ // Find and replace the MCP bar generation section
129
+ const barPattern = /# Create mini progress bar[\s\S]*?MCP_COLOR="\\033\[1;90m" # Gray/;
130
+
131
+ if (content.match(barPattern)) {
132
+ // Replace the entire bar generation section
133
+ const replacement = `# Use CSR compact status instead of MCP bar
134
+ # This shows both import percentage and code quality in format: [100% <time>][🟢:A+]
135
+ CSR_COMPACT=$(csr-status --compact 2>/dev/null || echo "")
136
+
137
+ if [[ -n "$CSR_COMPACT" ]]; then
138
+ MCP_STATUS="$CSR_COMPACT"
139
+
140
+ # Color based on content
141
+ if [[ "$CSR_COMPACT" == *"100%"* ]]; then
142
+ MCP_COLOR="\\033[1;32m" # Green for complete
143
+ elif [[ "$CSR_COMPACT" == *"[🟢:"* ]]; then
144
+ MCP_COLOR="\\033[1;32m" # Green for good quality
145
+ elif [[ "$CSR_COMPACT" == *"[🟡:"* ]]; then
146
+ MCP_COLOR="\\033[1;33m" # Yellow for medium quality
147
+ elif [[ "$CSR_COMPACT" == *"[🔴:"* ]]; then
148
+ MCP_COLOR="\\033[1;31m" # Red for poor quality
149
+ else
150
+ MCP_COLOR="\\033[1;36m" # Cyan default
151
+ fi
152
+ else
153
+ MCP_STATUS=""
154
+ MCP_COLOR="\\033[1;90m" # Gray`;
155
+
156
+ content = content.replace(barPattern, replacement);
157
+
158
+ // Write back
159
+ fs.writeFileSync(this.statuslineWrapper, content);
160
+ this.log('Statusline wrapper patched successfully (replaced MCP bar)', 'success');
161
+ return true;
162
+ } else {
163
+ // Fallback: Look for the PERCENTAGE check
164
+ const altPattern = /if \[\[ "\$PERCENTAGE" != "null"[\s\S]*?MCP_COLOR="\\033\[1;90m" # Gray/;
165
+
166
+ if (content.match(altPattern)) {
167
+ const replacement = `# Use CSR compact status instead of MCP bar
168
+ # This shows both import percentage and code quality in format: [100% <time>][🟢:A+]
169
+ CSR_COMPACT=$(csr-status --compact 2>/dev/null || echo "")
170
+
171
+ if [[ -n "$CSR_COMPACT" ]]; then
172
+ MCP_STATUS="$CSR_COMPACT"
173
+
174
+ # Color based on content
175
+ if [[ "$CSR_COMPACT" == *"100%"* ]]; then
176
+ MCP_COLOR="\\033[1;32m" # Green for complete
177
+ elif [[ "$CSR_COMPACT" == *"[🟢:"* ]]; then
178
+ MCP_COLOR="\\033[1;32m" # Green for good quality
179
+ elif [[ "$CSR_COMPACT" == *"[🟡:"* ]]; then
180
+ MCP_COLOR="\\033[1;33m" # Yellow for medium quality
181
+ elif [[ "$CSR_COMPACT" == *"[🔴:"* ]]; then
182
+ MCP_COLOR="\\033[1;31m" # Red for poor quality
183
+ else
184
+ MCP_COLOR="\\033[1;36m" # Cyan default
185
+ fi
186
+ else
187
+ MCP_STATUS=""
188
+ MCP_COLOR="\\033[1;90m" # Gray`;
189
+
190
+ content = content.replace(altPattern, replacement);
191
+
192
+ // Write back
193
+ fs.writeFileSync(this.statuslineWrapper, content);
194
+ this.log('Statusline wrapper patched successfully (replaced PERCENTAGE section)', 'success');
195
+ return true;
196
+ } else {
197
+ this.log('Could not find MCP bar section to replace', 'warning');
198
+ return false;
199
+ }
200
+ }
201
+ } catch (error) {
202
+ this.log(`Failed to patch statusline wrapper: ${error.message}`, 'error');
203
+ return false;
204
+ }
205
+ }
206
+
207
+ validateIntegration() {
208
+ try {
209
+ // Test csr-status command
210
+ const output = execSync('csr-status --compact', { encoding: 'utf8' });
211
+ if (output) {
212
+ this.log(`CSR status output: ${output.trim()}`, 'success');
213
+ return true;
214
+ }
215
+ } catch (error) {
216
+ this.log('CSR status command not working properly', 'warning');
217
+ return false;
218
+ }
219
+ return false;
220
+ }
221
+
222
+ async run() {
223
+ this.log('🚀 Setting up Claude Code Statusline Integration...', 'info');
224
+
225
+ if (!this.checkPrerequisites()) {
226
+ this.log('Prerequisites check failed', 'error');
227
+ return false;
228
+ }
229
+
230
+ const steps = [
231
+ { name: 'Install global command', fn: () => this.installGlobalCommand() },
232
+ { name: 'Patch statusline wrapper', fn: () => this.patchStatuslineWrapper() },
233
+ { name: 'Validate integration', fn: () => this.validateIntegration() }
234
+ ];
235
+
236
+ let success = true;
237
+ for (const step of steps) {
238
+ this.log(`\n📋 ${step.name}...`, 'info');
239
+ if (!step.fn()) {
240
+ success = false;
241
+ this.log(`❌ ${step.name} failed`, 'error');
242
+ }
243
+ }
244
+
245
+ if (success) {
246
+ this.log('\n✅ Statusline integration completed successfully!', 'success');
247
+ this.log('The CSR status will now appear in your Claude Code statusline.', 'info');
248
+ this.log('Format: [import%][🟢:grade] for compact quality metrics', 'info');
249
+ } else {
250
+ this.log('\n⚠️ Statusline integration completed with warnings', 'warning');
251
+ this.log('Some features may need manual configuration.', 'warning');
252
+ }
253
+
254
+ return success;
255
+ }
256
+
257
+ // Restore original statusline if needed
258
+ restore() {
259
+ if (fs.existsSync(this.statuslineBackup)) {
260
+ try {
261
+ fs.copyFileSync(this.statuslineBackup, this.statuslineWrapper);
262
+ this.log('Statusline wrapper restored from backup', 'success');
263
+ return true;
264
+ } catch (error) {
265
+ this.log(`Failed to restore: ${error.message}`, 'error');
266
+ return false;
267
+ }
268
+ } else {
269
+ this.log('No backup found to restore', 'warning');
270
+ return false;
271
+ }
272
+ }
273
+ }
274
+
275
+ // Run if called directly
276
+ if (process.argv[1] === fileURLToPath(import.meta.url)) {
277
+ const setup = new StatuslineSetup();
278
+
279
+ if (process.argv[2] === '--restore') {
280
+ setup.restore();
281
+ } else {
282
+ setup.run().catch(error => {
283
+ console.error('Setup failed:', error);
284
+ process.exit(1);
285
+ });
286
+ }
287
+ }
288
+
289
+ export default StatuslineSetup;
@@ -11,8 +11,24 @@ SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
11
11
  # Navigate to the mcp-server directory
12
12
  cd "$SCRIPT_DIR"
13
13
 
14
- # CRITICAL: Load .env file from project root if it exists
15
- # This ensures the MCP server uses the same settings as other scripts
14
+ # CRITICAL: Environment variables priority:
15
+ # 1. Command-line args from Claude Code (already in environment)
16
+ # 2. .env file (only for missing values)
17
+ # 3. Defaults (as fallback)
18
+
19
+ # Store any command-line provided values BEFORE loading .env
20
+ CMDLINE_VOYAGE_KEY="${VOYAGE_KEY:-}"
21
+ CMDLINE_PREFER_LOCAL="${PREFER_LOCAL_EMBEDDINGS:-}"
22
+ CMDLINE_QDRANT_URL="${QDRANT_URL:-}"
23
+
24
+ # CRITICAL: If local mode is explicitly requested, skip VOYAGE_KEY from .env
25
+ if [ "$CMDLINE_PREFER_LOCAL" = "true" ]; then
26
+ echo "[DEBUG] Local mode explicitly requested - will skip VOYAGE_KEY from .env" >&2
27
+ # Save current VOYAGE_KEY state
28
+ SAVED_VOYAGE_KEY="${VOYAGE_KEY:-}"
29
+ fi
30
+
31
+ # Load .env file for any missing values
16
32
  if [ -f "../.env" ]; then
17
33
  echo "[DEBUG] Loading .env file from project root" >&2
18
34
  set -a # Export all variables
@@ -22,8 +38,31 @@ else
22
38
  echo "[DEBUG] No .env file found, using defaults" >&2
23
39
  fi
24
40
 
25
- # Set smart defaults if not already set
26
- # These match what the CLI setup wizard uses
41
+ # CRITICAL: Handle local mode by clearing VOYAGE_KEY if local was explicitly requested
42
+ if [ "$CMDLINE_PREFER_LOCAL" = "true" ]; then
43
+ unset VOYAGE_KEY
44
+ echo "[DEBUG] Local mode: VOYAGE_KEY cleared to force local embeddings" >&2
45
+ elif [ "${CMDLINE_VOYAGE_KEY+x}" ]; then
46
+ # Restore command-line VOYAGE_KEY if it was explicitly set
47
+ export VOYAGE_KEY="$CMDLINE_VOYAGE_KEY"
48
+ if [ -z "$VOYAGE_KEY" ]; then
49
+ echo "[DEBUG] VOYAGE_KEY explicitly set to empty (forcing local mode)" >&2
50
+ else
51
+ echo "[DEBUG] Using command-line VOYAGE_KEY" >&2
52
+ fi
53
+ fi
54
+
55
+ if [ ! -z "$CMDLINE_PREFER_LOCAL" ]; then
56
+ export PREFER_LOCAL_EMBEDDINGS="$CMDLINE_PREFER_LOCAL"
57
+ echo "[DEBUG] Using command-line PREFER_LOCAL_EMBEDDINGS: $PREFER_LOCAL_EMBEDDINGS" >&2
58
+ fi
59
+
60
+ if [ ! -z "$CMDLINE_QDRANT_URL" ]; then
61
+ export QDRANT_URL="$CMDLINE_QDRANT_URL"
62
+ echo "[DEBUG] Using command-line QDRANT_URL: $QDRANT_URL" >&2
63
+ fi
64
+
65
+ # Set smart defaults ONLY if still not set
27
66
  if [ -z "$QDRANT_URL" ]; then
28
67
  export QDRANT_URL="http://localhost:6333"
29
68
  echo "[DEBUG] Using default QDRANT_URL: $QDRANT_URL" >&2
@@ -52,9 +91,8 @@ fi
52
91
  # CRITICAL FIX: Pass through environment variables from Claude Code
53
92
  # These environment variables are set by `claude mcp add -e KEY=value`
54
93
  # Export them so the Python process can access them
55
- if [ ! -z "$VOYAGE_KEY" ]; then
56
- export VOYAGE_KEY="$VOYAGE_KEY"
57
- fi
94
+ # BUT: Don't export VOYAGE_KEY if we're in local mode
95
+ # Note: VOYAGE_KEY might have been unset earlier for local mode, so skip this entirely
58
96
 
59
97
  if [ ! -z "$VOYAGE_KEY_2" ]; then
60
98
  export VOYAGE_KEY_2="$VOYAGE_KEY_2"
@@ -0,0 +1,271 @@
1
+ """Runtime code reloading tool for MCP server development."""
2
+
3
+ import os
4
+ import sys
5
+ import importlib
6
+ import logging
7
+ from pathlib import Path
8
+ from typing import Dict, List, Optional, Literal
9
+ from fastmcp import Context
10
+ from pydantic import Field
11
+ import hashlib
12
+ import json
13
+
14
+ logger = logging.getLogger(__name__)
15
+
16
+
17
+ class CodeReloader:
18
+ """Handles runtime code reloading for the MCP server."""
19
+
20
+ def __init__(self):
21
+ """Initialize the code reloader."""
22
+ self.module_hashes: Dict[str, str] = {}
23
+ self.reload_history: List[Dict] = []
24
+ self.cache_dir = Path.home() / '.claude-self-reflect' / 'reload_cache'
25
+ self.cache_dir.mkdir(parents=True, exist_ok=True)
26
+ # Test comment: Hot reload test at 2025-09-15
27
+ logger.info("CodeReloader initialized with hot reload support")
28
+
29
+ def _get_file_hash(self, filepath: Path) -> str:
30
+ """Get SHA256 hash of a file."""
31
+ with open(filepath, 'rb') as f:
32
+ return hashlib.sha256(f.read()).hexdigest()
33
+
34
+ def _get_changed_modules(self) -> List[str]:
35
+ """Detect which modules have changed since last check."""
36
+ changed = []
37
+ src_dir = Path(__file__).parent
38
+
39
+ for py_file in src_dir.glob("*.py"):
40
+ if py_file.name == "__pycache__":
41
+ continue
42
+
43
+ module_name = f"src.{py_file.stem}"
44
+ current_hash = self._get_file_hash(py_file)
45
+
46
+ if module_name in self.module_hashes:
47
+ if self.module_hashes[module_name] != current_hash:
48
+ changed.append(module_name)
49
+
50
+ self.module_hashes[module_name] = current_hash
51
+
52
+ return changed
53
+
54
+ async def reload_modules(
55
+ self,
56
+ ctx: Context,
57
+ modules: Optional[List[str]] = None,
58
+ auto_detect: bool = True
59
+ ) -> str:
60
+ """Reload Python modules at runtime without restarting the MCP server."""
61
+
62
+ await ctx.debug("Starting code reload process...")
63
+
64
+ try:
65
+ # Track what we're reloading
66
+ reload_targets = []
67
+
68
+ if auto_detect:
69
+ # Detect changed modules
70
+ changed = self._get_changed_modules()
71
+ if changed:
72
+ reload_targets.extend(changed)
73
+ await ctx.debug(f"Auto-detected changes in: {changed}")
74
+
75
+ if modules:
76
+ # Add explicitly requested modules
77
+ reload_targets.extend(modules)
78
+
79
+ if not reload_targets:
80
+ return "📊 No modules to reload. All code is up to date!"
81
+
82
+ # Perform the reload
83
+ reloaded = []
84
+ failed = []
85
+
86
+ for module_name in reload_targets:
87
+ try:
88
+ # SECURITY FIX: Validate module is in whitelist
89
+ from .security_patches import ModuleWhitelist
90
+ if not ModuleWhitelist.is_allowed_module(module_name):
91
+ logger.warning(f"Module not in whitelist, skipping: {module_name}")
92
+ failed.append((module_name, "Module not in whitelist"))
93
+ continue
94
+
95
+ if module_name in sys.modules:
96
+ # Store old module reference for rollback
97
+ old_module = sys.modules[module_name]
98
+
99
+ # Reload the module
100
+ logger.info(f"Reloading module: {module_name}")
101
+ reloaded_module = importlib.reload(sys.modules[module_name])
102
+
103
+ # Update any global references if needed
104
+ self._update_global_references(module_name, reloaded_module)
105
+
106
+ reloaded.append(module_name)
107
+ await ctx.debug(f"✅ Reloaded: {module_name}")
108
+ else:
109
+ # Module not loaded yet, import it
110
+ importlib.import_module(module_name)
111
+ reloaded.append(module_name)
112
+ await ctx.debug(f"✅ Imported: {module_name}")
113
+
114
+ except Exception as e:
115
+ logger.error(f"Failed to reload {module_name}: {e}", exc_info=True)
116
+ failed.append((module_name, str(e)))
117
+ await ctx.debug(f"❌ Failed: {module_name} - {e}")
118
+
119
+ # Record reload history
120
+ self.reload_history.append({
121
+ "timestamp": os.environ.get('MCP_REQUEST_ID', 'unknown'),
122
+ "reloaded": reloaded,
123
+ "failed": failed
124
+ })
125
+
126
+ # Build response
127
+ response = "🔄 **Code Reload Results**\n\n"
128
+
129
+ if reloaded:
130
+ response += f"**Successfully Reloaded ({len(reloaded)}):**\n"
131
+ for module in reloaded:
132
+ response += f"- ✅ {module}\n"
133
+ response += "\n"
134
+
135
+ if failed:
136
+ response += f"**Failed to Reload ({len(failed)}):**\n"
137
+ for module, error in failed:
138
+ response += f"- ❌ {module}: {error}\n"
139
+ response += "\n"
140
+
141
+ response += "**Important Notes:**\n"
142
+ response += "- Class instances created before reload keep old code\n"
143
+ response += "- New requests will use the reloaded code\n"
144
+ response += "- Some changes may require full restart (e.g., new tools)\n"
145
+
146
+ return response
147
+
148
+ except Exception as e:
149
+ logger.error(f"Code reload failed: {e}", exc_info=True)
150
+ return f"❌ Code reload failed: {str(e)}"
151
+
152
+ def _update_global_references(self, module_name: str, new_module):
153
+ """Update global references after module reload."""
154
+ # This is where we'd update any global singleton references
155
+ # For example, if we reload embedding_manager, we might need to
156
+ # update the global embedding manager instance
157
+
158
+ if module_name == "src.embedding_manager":
159
+ # Update the global embedding manager if it exists
160
+ if hasattr(new_module, 'get_embedding_manager'):
161
+ # The singleton pattern should handle this automatically
162
+ pass
163
+
164
+ elif module_name == "src.search_tools":
165
+ # Search tools might need to refresh their references
166
+ pass
167
+
168
+ # Add more specific updates as needed
169
+
170
+ async def get_reload_status(self, ctx: Context) -> str:
171
+ """Get the current reload status and history."""
172
+
173
+ try:
174
+ # Check for changed files
175
+ changed = self._get_changed_modules()
176
+
177
+ response = "📊 **Code Reload Status**\n\n"
178
+
179
+ response += "**Module Status:**\n"
180
+ if changed:
181
+ response += f"⚠️ {len(changed)} modules have pending changes:\n"
182
+ for module in changed:
183
+ response += f" - {module}\n"
184
+ else:
185
+ response += "✅ All modules are up to date\n"
186
+
187
+ response += f"\n**Tracked Modules:** {len(self.module_hashes)}\n"
188
+
189
+ if self.reload_history:
190
+ response += f"\n**Recent Reloads:**\n"
191
+ for entry in self.reload_history[-5:]: # Last 5 reloads
192
+ response += f"- {entry['timestamp']}: "
193
+ response += f"{len(entry['reloaded'])} success, "
194
+ response += f"{len(entry['failed'])} failed\n"
195
+
196
+ return response
197
+
198
+ except Exception as e:
199
+ logger.error(f"Failed to get reload status: {e}", exc_info=True)
200
+ return f"❌ Failed to get reload status: {str(e)}"
201
+
202
+ async def clear_python_cache(self, ctx: Context) -> str:
203
+ """Clear Python's module cache and bytecode."""
204
+
205
+ try:
206
+ await ctx.debug("Clearing Python cache...")
207
+
208
+ # Clear __pycache__ directories
209
+ src_dir = Path(__file__).parent
210
+ pycache_dirs = list(src_dir.rglob("__pycache__"))
211
+
212
+ for pycache in pycache_dirs:
213
+ if pycache.is_dir():
214
+ import shutil
215
+ shutil.rmtree(pycache)
216
+ await ctx.debug(f"Removed: {pycache}")
217
+
218
+ # Clear import cache
219
+ importlib.invalidate_caches()
220
+
221
+ return f"✅ Cleared {len(pycache_dirs)} __pycache__ directories and invalidated import caches"
222
+
223
+ except Exception as e:
224
+ logger.error(f"Failed to clear cache: {e}", exc_info=True)
225
+ return f"❌ Failed to clear cache: {str(e)}"
226
+
227
+
228
+ def register_code_reload_tool(mcp, get_embedding_manager):
229
+ """Register the code reloading tool with the MCP server."""
230
+
231
+ reloader = CodeReloader()
232
+
233
+ @mcp.tool()
234
+ async def reload_code(
235
+ ctx: Context,
236
+ modules: Optional[List[str]] = Field(
237
+ default=None,
238
+ description="Specific modules to reload (e.g., ['src.search_tools', 'src.embedding_manager'])"
239
+ ),
240
+ auto_detect: bool = Field(
241
+ default=True,
242
+ description="Automatically detect and reload changed modules"
243
+ )
244
+ ) -> str:
245
+ """Reload Python code at runtime without restarting the MCP server.
246
+
247
+ This allows hot-reloading of code changes during development, similar to
248
+ the mode switching capability. Changes take effect for new requests.
249
+
250
+ Note: Some changes (new tools, startup configuration) still require restart.
251
+ """
252
+ return await reloader.reload_modules(ctx, modules, auto_detect)
253
+
254
+ @mcp.tool()
255
+ async def reload_status(ctx: Context) -> str:
256
+ """Check which modules have pending changes and reload history.
257
+
258
+ Shows which files have been modified since last reload and
259
+ the history of recent reload operations.
260
+ """
261
+ return await reloader.get_reload_status(ctx)
262
+
263
+ @mcp.tool()
264
+ async def clear_module_cache(ctx: Context) -> str:
265
+ """Clear Python's module cache and __pycache__ directories.
266
+
267
+ Useful when reload isn't working due to cached bytecode.
268
+ """
269
+ return await reloader.clear_python_cache(ctx)
270
+
271
+ logger.info("Code reload tools registered successfully")