-
-
Notifications
You must be signed in to change notification settings - Fork 74
Building a React Dashboard for MCP Memory Service
This guide walks through creating a complete React-based dashboard for the MCP Memory Service, from architecture decisions to implementation details.
- Overview
- Architecture Decisions
- Project Setup
- Core Components
- MCP Integration
- UI Implementation
- Error Handling
- Configuration Management
- Best Practices
The MCP Memory Dashboard provides a web-based UI for interacting with the MCP Memory Service, featuring memory storage, semantic search, tag management, and real-time statistics.
Originally, the dashboard was designed to work within Claude Desktop's window interface. However, to make it more versatile, we pivoted to a standalone architecture:
Original Design: Final Design:
Claude Desktop Standalone React App
↓ ↓
Window Interface → MCP Client
↓ ↓
MCP Memory Service MCP Memory Service
- React 18.2.0 - Modern React with hooks
- TypeScript - Type safety
- Tailwind CSS - Utility-first styling
- shadcn/ui - High-quality UI components
- Lucide React - Icon library
- Vite - Fast build tool
npm create vite@latest mcp-memory-dashboard -- --template react-ts
cd mcp-memory-dashboard
npm install
# UI Components
npm install @radix-ui/react-tabs @radix-ui/react-dialog
npm install lucide-react class-variance-authority clsx
# Styling
npm install tailwindcss postcss autoprefixer
npx tailwindcss init -p
# Development
npm install -D @types/react @types/react-dom
mcp-memory-dashboard/
├── src/
│ ├── components/
│ │ ├── ui/ # shadcn/ui components
│ │ └── dashboard/ # Dashboard-specific components
│ ├── services/
│ │ └── memory.ts # MCP integration service
│ ├── hooks/ # Custom React hooks
│ ├── types/ # TypeScript types
│ └── MemoryDashboard.tsx # Main component
├── electron/ # Electron wrapper (if needed)
└── public/
import React, { useState, useEffect } from 'react';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Brain, Search, Tag, Database } from 'lucide-react';
interface DashboardStats {
totalMemories: number;
uniqueTags: number;
databaseHealth: number;
avgQueryTime: number;
}
const MemoryDashboard: React.FC = () => {
const [stats, setStats] = useState<DashboardStats>({
totalMemories: 0,
uniqueTags: 0,
databaseHealth: 100,
avgQueryTime: 0
});
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
// Load stats on mount
useEffect(() => {
loadStats();
}, []);
const loadStats = async () => {
try {
setLoading(true);
const response = await window.mcpTools.dashboard_get_stats();
setStats({
totalMemories: response.total_memories,
uniqueTags: response.unique_tags,
databaseHealth: response.health || 100,
avgQueryTime: response.avg_query_time || 0
});
} catch (err) {
setError('Failed to load statistics');
} finally {
setLoading(false);
}
};
return (
<div className=\"min-h-screen bg-gray-50 p-8\">
<div className=\"max-w-7xl mx-auto\">
<h1 className=\"text-4xl font-bold mb-8\">Memory Dashboard</h1>
{/* Statistics Cards */}
<div className=\"grid grid-cols-1 md:grid-cols-4 gap-4 mb-8\">
<StatsCard
icon={<Brain />}
title=\"Total Memories\"
value={stats.totalMemories}
loading={loading}
/>
<StatsCard
icon={<Tag />}
title=\"Unique Tags\"
value={stats.uniqueTags}
loading={loading}
/>
<StatsCard
icon={<Database />}
title=\"Health\"
value={`${stats.databaseHealth}%`}
loading={loading}
/>
<StatsCard
icon={<Search />}
title=\"Avg Query Time\"
value={`${stats.avgQueryTime}ms`}
loading={loading}
/>
</div>
{/* Main Tabs */}
<Tabs defaultValue=\"store\" className=\"w-full\">
<TabsList className=\"grid w-full grid-cols-3\">
<TabsTrigger value=\"store\">Store Memory</TabsTrigger>
<TabsTrigger value=\"search\">Search Memories</TabsTrigger>
<TabsTrigger value=\"tags\">Manage Tags</TabsTrigger>
</TabsList>
<TabsContent value=\"store\">
<StoreMemoryTab onMemoryStored={loadStats} />
</TabsContent>
<TabsContent value=\"search\">
<SearchMemoriesTab />
</TabsContent>
<TabsContent value=\"tags\">
<ManageTagsTab />
</TabsContent>
</Tabs>
</div>
</div>
);
};
const StoreMemoryTab: React.FC<{ onMemoryStored: () => void }> = ({ onMemoryStored }) => {
const [content, setContent] = useState('');
const [tags, setTags] = useState('');
const [storing, setStoring] = useState(false);
const handleStore = async () => {
if (!content.trim()) {
alert('Please enter some content');
return;
}
try {
setStoring(true);
const tagArray = tags
.split(',')
.map(t => t.trim())
.filter(t => t.length > 0);
await window.mcpTools.store_memory({
content,
metadata: { tags: tagArray }
});
// Clear form
setContent('');
setTags('');
// Refresh stats
onMemoryStored();
// Show success
alert('Memory stored successfully!');
} catch (err) {
alert('Failed to store memory: ' + err.message);
} finally {
setStoring(false);
}
};
return (
<Card>
<CardHeader>
<CardTitle>Store New Memory</CardTitle>
</CardHeader>
<CardContent>
<div className=\"space-y-4\">
<div>
<label className=\"block text-sm font-medium mb-2\">
Content
</label>
<textarea
className=\"w-full p-3 border rounded-lg h-32\"
value={content}
onChange={(e) => setContent(e.target.value)}
placeholder=\"Enter your memory content...\"
disabled={storing}
/>
</div>
<div>
<label className=\"block text-sm font-medium mb-2\">
Tags (comma-separated)
</label>
<input
type=\"text\"
className=\"w-full p-3 border rounded-lg\"
value={tags}
onChange={(e) => setTags(e.target.value)}
placeholder=\"tag1, tag2, tag3\"
disabled={storing}
/>
</div>
<Button
onClick={handleStore}
disabled={storing || !content.trim()}
className=\"w-full\"
>
{storing ? 'Storing...' : 'Store Memory'}
</Button>
</div>
</CardContent>
</Card>
);
};
const SearchMemoriesTab: React.FC = () => {
const [query, setQuery] = useState('');
const [results, setResults] = useState<MemoryResult[]>([]);
const [searching, setSearching] = useState(false);
const handleSearch = async () => {
if (!query.trim()) return;
try {
setSearching(true);
const response = await window.mcpTools.dashboard_retrieve_memory({
query,
n_results: 10
});
setResults(response.memories || []);
} catch (err) {
alert('Search failed: ' + err.message);
} finally {
setSearching(false);
}
};
return (
<Card>
<CardHeader>
<CardTitle>Search Memories</CardTitle>
</CardHeader>
<CardContent>
<div className=\"space-y-4\">
{/* Search Input */}
<div className=\"flex gap-2\">
<input
type=\"text\"
className=\"flex-1 p-3 border rounded-lg\"
value={query}
onChange={(e) => setQuery(e.target.value)}
onKeyPress={(e) => e.key === 'Enter' && handleSearch()}
placeholder=\"Search your memories...\"
disabled={searching}
/>
<Button onClick={handleSearch} disabled={searching}>
{searching ? 'Searching...' : 'Search'}
</Button>
</div>
{/* Results */}
<div className=\"space-y-2\">
{results.map((result, index) => (
<MemoryCard
key={index}
memory={result}
onDelete={() => handleDelete(result.id)}
/>
))}
{results.length === 0 && !searching && (
<p className=\"text-gray-500 text-center py-8\">
No results to display
</p>
)}
</div>
</div>
</CardContent>
</Card>
);
};
// services/memory.ts
export class MemoryService {
private mcpClient: any;
constructor() {
this.initializeMCP();
}
private async initializeMCP() {
// Initialize MCP client
if (window.mcpTools) {
// Running in Claude Desktop or Electron
this.mcpClient = window.mcpTools;
} else {
// Standalone mode - implement MCP protocol
throw new Error('MCP tools not available');
}
}
async storeMemory(content: string, tags: string[]): Promise<void> {
return await this.mcpClient.store_memory({
content,
metadata: { tags }
});
}
async searchMemories(query: string, limit: number = 10): Promise<MemoryResult[]> {
const response = await this.mcpClient.dashboard_retrieve_memory({
query,
n_results: limit
});
return response.memories || [];
}
async searchByTags(tags: string[]): Promise<MemoryResult[]> {
const response = await this.mcpClient.dashboard_search_by_tag({ tags });
return response.memories || [];
}
async getStats(): Promise<DashboardStats> {
return await this.mcpClient.dashboard_get_stats();
}
async deleteMemory(memoryId: string): Promise<void> {
return await this.mcpClient.delete_memory({ content_hash: memoryId });
}
}
// types/mcp.d.ts
interface MCPTools {
store_memory: (params: StoreMemoryParams) => Promise<void>;
dashboard_retrieve_memory: (params: RetrieveParams) => Promise<MemoryResponse>;
dashboard_search_by_tag: (params: TagSearchParams) => Promise<MemoryResponse>;
dashboard_get_stats: () => Promise<StatsResponse>;
delete_memory: (params: DeleteParams) => Promise<void>;
}
interface Window {
mcpTools?: MCPTools;
}
class ErrorBoundary extends React.Component<
{ children: ReactNode },
{ hasError: boolean; error: Error | null }
> {
constructor(props) {
super(props);
this.state = { hasError: false, error: null };
}
static getDerivedStateFromError(error: Error) {
return { hasError: true, error };
}
render() {
if (this.state.hasError) {
return (
<div className=\"min-h-screen flex items-center justify-center\">
<Card className=\"w-96\">
<CardHeader>
<CardTitle>Something went wrong</CardTitle>
</CardHeader>
<CardContent>
<p className=\"text-red-600\">
{this.state.error?.message || 'Unknown error'}
</p>
<Button
onClick={() => window.location.reload()}
className=\"mt-4\"
>
Reload Page
</Button>
</CardContent>
</Card>
</div>
);
}
return this.props.children;
}
}
const useAsyncOperation = () => {
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const execute = async (operation: () => Promise<void>) => {
try {
setLoading(true);
setError(null);
await operation();
} catch (err) {
setError(err instanceof Error ? err.message : 'Unknown error');
throw err;
} finally {
setLoading(false);
}
};
return { execute, loading, error };
};
interface Config {
mcpServers: {
memory: {
command: string;
args: string[];
env?: Record<string, string>;
};
};
}
const validateConfig = (config: any): config is Config => {
return (
config?.mcpServers?.memory?.command &&
Array.isArray(config.mcpServers.memory.args)
);
};
const ConfigurationHelper: React.FC = () => {
const [showConfig, setShowConfig] = useState(false);
return (
<Dialog open={showConfig} onOpenChange={setShowConfig}>
<DialogTrigger asChild>
<Button variant=\"outline\" size=\"icon\">
<Settings className=\"h-4 w-4\" />
</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>Configuration Setup</DialogTitle>
</DialogHeader>
<div className=\"space-y-4\">
<p>Add this to your claude_desktop_config.json:</p>
<pre className=\"bg-gray-100 p-4 rounded overflow-x-auto\">
{JSON.stringify({
mcpServers: {
memory: {
command: \"uv\",
args: [\"--directory\", \"/path/to/mcp-memory-service\", \"run\", \"memory\"],
env: {
MCP_MEMORY_CHROMA_PATH: \"/path/to/chroma_db\"
}
}
}
}, null, 2)}
</pre>
</div>
</DialogContent>
</Dialog>
);
};
Always show loading indicators for async operations:
const LoadingSpinner: React.FC = () => (
<div className=\"flex items-center justify-center p-4\">
<div className=\"animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600\" />
</div>
);
Prevent excessive API calls:
import { useMemo } from 'react';
import debounce from 'lodash/debounce';
const useDebounce = (callback: Function, delay: number) => {
return useMemo(
() => debounce(callback, delay),
[callback, delay]
);
};
Update UI immediately for better UX:
const handleDelete = async (memoryId: string) => {
// Optimistic update
setResults(prev => prev.filter(r => r.id !== memoryId));
try {
await memoryService.deleteMemory(memoryId);
// Refresh stats after delete
await loadStats();
} catch (err) {
// Revert on error
setResults(prev => [...prev, deletedMemory]);
alert('Failed to delete memory');
}
};
Use Tailwind's responsive utilities:
<div className=\"grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4\">
{/* Stats cards responsive grid */}
</div>
Building a React dashboard for MCP Memory Service involves:
- Clear architecture - Separation of UI and service logic
- Type safety - TypeScript throughout
- Modern UI - Tailwind CSS with shadcn/ui components
- Error handling - Graceful failure recovery
- Performance - Loading states, debouncing, and optimization
The result is a professional, responsive dashboard that provides an excellent user experience for memory management operations.