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:
+6
-1
@@ -1,3 +1,8 @@
|
|||||||
{
|
{
|
||||||
"mcpServers": {}
|
"mcpServers": {
|
||||||
|
"claude-mem-search": {
|
||||||
|
"type": "stdio",
|
||||||
|
"command": "${CLAUDE_PLUGIN_ROOT}/scripts/search-server.mjs"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -14,7 +14,7 @@ import express, { Request, Response } from 'express';
|
|||||||
import cors from 'cors';
|
import cors from 'cors';
|
||||||
import http from 'http';
|
import http from 'http';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import { readFileSync, writeFileSync, statSync, existsSync } from 'fs';
|
import { readFileSync, writeFileSync, statSync, existsSync, renameSync } from 'fs';
|
||||||
import { homedir } from 'os';
|
import { homedir } from 'os';
|
||||||
import { getPackageRoot } from '../shared/paths.js';
|
import { getPackageRoot } from '../shared/paths.js';
|
||||||
import { getWorkerPort } from '../shared/worker-utils.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.get('/api/settings', this.handleGetSettings.bind(this));
|
||||||
this.app.post('/api/settings', this.handleUpdateSettings.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)
|
// Search API endpoints (for skill-based search)
|
||||||
this.app.get('/api/search/observations', this.handleSearchObservations.bind(this));
|
this.app.get('/api/search/observations', this.handleSearchObservations.bind(this));
|
||||||
this.app.get('/api/search/sessions', this.handleSearchSessions.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)
|
// 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) {
|
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 [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 [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);
|
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(() => {
|
useEffect(() => {
|
||||||
setModel(settings.CLAUDE_MEM_MODEL || DEFAULT_SETTINGS.CLAUDE_MEM_MODEL);
|
setModel(settings.CLAUDE_MEM_MODEL || DEFAULT_SETTINGS.CLAUDE_MEM_MODEL);
|
||||||
setContextObs(settings.CLAUDE_MEM_CONTEXT_OBSERVATIONS || DEFAULT_SETTINGS.CLAUDE_MEM_CONTEXT_OBSERVATIONS);
|
setContextObs(settings.CLAUDE_MEM_CONTEXT_OBSERVATIONS || DEFAULT_SETTINGS.CLAUDE_MEM_CONTEXT_OBSERVATIONS);
|
||||||
setWorkerPort(settings.CLAUDE_MEM_WORKER_PORT || DEFAULT_SETTINGS.CLAUDE_MEM_WORKER_PORT);
|
setWorkerPort(settings.CLAUDE_MEM_WORKER_PORT || DEFAULT_SETTINGS.CLAUDE_MEM_WORKER_PORT);
|
||||||
}, [settings]);
|
}, [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 = () => {
|
const handleSave = () => {
|
||||||
onSave({
|
onSave({
|
||||||
CLAUDE_MEM_MODEL: model,
|
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 (
|
return (
|
||||||
<div className={`sidebar ${isOpen ? 'open' : ''}`}>
|
<div className={`sidebar ${isOpen ? 'open' : ''}`}>
|
||||||
<div className="sidebar-header">
|
<div className="sidebar-header">
|
||||||
@@ -119,6 +162,29 @@ export function Sidebar({ isOpen, settings, stats, isSaving, saveStatus, isConne
|
|||||||
)}
|
)}
|
||||||
</div>
|
</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">
|
<div className="settings-section">
|
||||||
<h3>Worker Stats</h3>
|
<h3>Worker Stats</h3>
|
||||||
<div className="stats-grid">
|
<div className="stats-grid">
|
||||||
|
|||||||
Reference in New Issue
Block a user