feat: Add MCP search server toggle with dedicated API architecture (#85)
* feat: add MCP search server toggle functionality - Introduced `CLAUDE_MEM_MCP_ENABLED` setting to manage the MCP search server state. - Updated `WorkerService` to handle MCP enabling/disabling based on the new setting. - Enhanced `Sidebar` component to include a checkbox for toggling MCP search server. - Modified `useSettings` hook to incorporate the new MCP setting. - Updated default settings to include `CLAUDE_MEM_MCP_ENABLED` with a default value of true. - Adjusted TypeScript types to include the new MCP setting. * feat: Implement MCP toggle functionality in WorkerService and Sidebar - Added API endpoints for MCP status retrieval and toggling in WorkerService. - Updated Sidebar component to manage MCP toggle state and display status messages. - Removed MCP_ENABLED from settings state management and default settings. - Adjusted settings interface and related hooks to reflect the removal of MCP_ENABLED.
This commit is contained in:
@@ -14,7 +14,7 @@ import express, { Request, Response } from 'express';
|
||||
import cors from 'cors';
|
||||
import http from 'http';
|
||||
import path from 'path';
|
||||
import { readFileSync, writeFileSync, statSync, existsSync } from 'fs';
|
||||
import { readFileSync, writeFileSync, statSync, existsSync, renameSync } from 'fs';
|
||||
import { homedir } from 'os';
|
||||
import { getPackageRoot } from '../shared/paths.js';
|
||||
import { getWorkerPort } from '../shared/worker-utils.js';
|
||||
@@ -101,6 +101,10 @@ export class WorkerService {
|
||||
this.app.get('/api/settings', this.handleGetSettings.bind(this));
|
||||
this.app.post('/api/settings', this.handleUpdateSettings.bind(this));
|
||||
|
||||
// MCP toggle
|
||||
this.app.get('/api/mcp/status', this.handleGetMcpStatus.bind(this));
|
||||
this.app.post('/api/mcp/toggle', this.handleToggleMcp.bind(this));
|
||||
|
||||
// Search API endpoints (for skill-based search)
|
||||
this.app.get('/api/search/observations', this.handleSearchObservations.bind(this));
|
||||
this.app.get('/api/search/sessions', this.handleSearchSessions.bind(this));
|
||||
@@ -655,6 +659,83 @@ export class WorkerService {
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// MCP Toggle Handlers
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* GET /api/mcp/status - Check if MCP search server is enabled
|
||||
*/
|
||||
private handleGetMcpStatus(req: Request, res: Response): void {
|
||||
try {
|
||||
const enabled = this.isMcpEnabled();
|
||||
res.json({ enabled });
|
||||
} catch (error) {
|
||||
logger.failure('WORKER', 'Get MCP status failed', {}, error as Error);
|
||||
res.status(500).json({ error: (error as Error).message });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /api/mcp/toggle - Toggle MCP search server on/off
|
||||
* Body: { enabled: boolean }
|
||||
*/
|
||||
private handleToggleMcp(req: Request, res: Response): void {
|
||||
try {
|
||||
const { enabled } = req.body;
|
||||
|
||||
if (typeof enabled !== 'boolean') {
|
||||
res.status(400).json({ error: 'enabled must be a boolean' });
|
||||
return;
|
||||
}
|
||||
|
||||
this.toggleMcp(enabled);
|
||||
res.json({ success: true, enabled: this.isMcpEnabled() });
|
||||
} catch (error) {
|
||||
logger.failure('WORKER', 'Toggle MCP failed', {}, error as Error);
|
||||
res.status(500).json({ success: false, error: (error as Error).message });
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// MCP Toggle Helpers
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Check if MCP search server is enabled
|
||||
*/
|
||||
private isMcpEnabled(): boolean {
|
||||
const packageRoot = getPackageRoot();
|
||||
const mcpPath = path.join(packageRoot, 'plugin', '.mcp.json');
|
||||
return existsSync(mcpPath);
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle MCP search server (rename .mcp.json <-> .mcp.json.disabled)
|
||||
*/
|
||||
private toggleMcp(enabled: boolean): void {
|
||||
try {
|
||||
const packageRoot = getPackageRoot();
|
||||
const mcpPath = path.join(packageRoot, 'plugin', '.mcp.json');
|
||||
const mcpDisabledPath = path.join(packageRoot, 'plugin', '.mcp.json.disabled');
|
||||
|
||||
if (enabled && existsSync(mcpDisabledPath)) {
|
||||
// Enable: rename .mcp.json.disabled -> .mcp.json
|
||||
renameSync(mcpDisabledPath, mcpPath);
|
||||
logger.info('WORKER', 'MCP search server enabled');
|
||||
} else if (!enabled && existsSync(mcpPath)) {
|
||||
// Disable: rename .mcp.json -> .mcp.json.disabled
|
||||
renameSync(mcpPath, mcpDisabledPath);
|
||||
logger.info('WORKER', 'MCP search server disabled');
|
||||
} else {
|
||||
logger.debug('WORKER', 'MCP toggle no-op (already in desired state)', { enabled });
|
||||
}
|
||||
} catch (error) {
|
||||
logger.failure('WORKER', 'Failed to toggle MCP', { enabled }, error as Error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Search API Handlers (for skill-based search)
|
||||
// ============================================================================
|
||||
|
||||
@@ -15,17 +15,31 @@ interface SidebarProps {
|
||||
}
|
||||
|
||||
export function Sidebar({ isOpen, settings, stats, isSaving, saveStatus, isConnected, onSave, onClose }: SidebarProps) {
|
||||
// Settings form state
|
||||
const [model, setModel] = useState(settings.CLAUDE_MEM_MODEL || DEFAULT_SETTINGS.CLAUDE_MEM_MODEL);
|
||||
const [contextObs, setContextObs] = useState(settings.CLAUDE_MEM_CONTEXT_OBSERVATIONS || DEFAULT_SETTINGS.CLAUDE_MEM_CONTEXT_OBSERVATIONS);
|
||||
const [workerPort, setWorkerPort] = useState(settings.CLAUDE_MEM_WORKER_PORT || DEFAULT_SETTINGS.CLAUDE_MEM_WORKER_PORT);
|
||||
|
||||
// Update local state when settings change
|
||||
// MCP toggle state (separate from settings)
|
||||
const [mcpEnabled, setMcpEnabled] = useState(true);
|
||||
const [mcpToggling, setMcpToggling] = useState(false);
|
||||
const [mcpStatus, setMcpStatus] = useState('');
|
||||
|
||||
// Update settings form state when settings change
|
||||
useEffect(() => {
|
||||
setModel(settings.CLAUDE_MEM_MODEL || DEFAULT_SETTINGS.CLAUDE_MEM_MODEL);
|
||||
setContextObs(settings.CLAUDE_MEM_CONTEXT_OBSERVATIONS || DEFAULT_SETTINGS.CLAUDE_MEM_CONTEXT_OBSERVATIONS);
|
||||
setWorkerPort(settings.CLAUDE_MEM_WORKER_PORT || DEFAULT_SETTINGS.CLAUDE_MEM_WORKER_PORT);
|
||||
}, [settings]);
|
||||
|
||||
// Fetch MCP status on mount
|
||||
useEffect(() => {
|
||||
fetch('/api/mcp/status')
|
||||
.then(res => res.json())
|
||||
.then(data => setMcpEnabled(data.enabled))
|
||||
.catch(error => console.error('Failed to load MCP status:', error));
|
||||
}, []);
|
||||
|
||||
const handleSave = () => {
|
||||
onSave({
|
||||
CLAUDE_MEM_MODEL: model,
|
||||
@@ -34,6 +48,35 @@ export function Sidebar({ isOpen, settings, stats, isSaving, saveStatus, isConne
|
||||
});
|
||||
};
|
||||
|
||||
const handleMcpToggle = async (enabled: boolean) => {
|
||||
setMcpToggling(true);
|
||||
setMcpStatus('Toggling...');
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/mcp/toggle', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ enabled })
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
setMcpEnabled(result.enabled);
|
||||
setMcpStatus('✓ Updated (restart Claude Code to apply)');
|
||||
setTimeout(() => setMcpStatus(''), 3000);
|
||||
} else {
|
||||
setMcpStatus(`✗ Error: ${result.error}`);
|
||||
setTimeout(() => setMcpStatus(''), 3000);
|
||||
}
|
||||
} catch (error) {
|
||||
setMcpStatus(`✗ Error: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
||||
setTimeout(() => setMcpStatus(''), 3000);
|
||||
} finally {
|
||||
setMcpToggling(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={`sidebar ${isOpen ? 'open' : ''}`}>
|
||||
<div className="sidebar-header">
|
||||
@@ -119,6 +162,29 @@ export function Sidebar({ isOpen, settings, stats, isSaving, saveStatus, isConne
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="settings-section">
|
||||
<h3>MCP Search Server</h3>
|
||||
<div className="form-group">
|
||||
<label htmlFor="mcpEnabled" style={{ display: 'flex', alignItems: 'center', gap: '8px', cursor: 'pointer' }}>
|
||||
<input
|
||||
type="checkbox"
|
||||
id="mcpEnabled"
|
||||
checked={mcpEnabled}
|
||||
onChange={e => handleMcpToggle(e.target.checked)}
|
||||
disabled={mcpToggling}
|
||||
style={{ cursor: mcpToggling ? 'not-allowed' : 'pointer' }}
|
||||
/>
|
||||
Enable MCP Search Server
|
||||
</label>
|
||||
<div className="setting-description">
|
||||
claude-mem suggests using skill-based search (saves ~2,500 tokens at session start), but some users prefer MCP. Disable to only use skill-based search. Requires Claude Code restart to apply changes.
|
||||
</div>
|
||||
{mcpStatus && (
|
||||
<div className="save-status">{mcpStatus}</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="settings-section">
|
||||
<h3>Worker Stats</h3>
|
||||
<div className="stats-grid">
|
||||
|
||||
Reference in New Issue
Block a user