Skip to content

Building a React Dashboard for MCP Memory Service

Henry edited this page Jul 20, 2025 · 1 revision

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.

Table of Contents

Overview

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.

Architecture Decisions

Initial Approach vs Final Design

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

Technology Stack

  • 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

Project Setup

1. Initialize Project

npm create vite@latest mcp-memory-dashboard -- --template react-ts
cd mcp-memory-dashboard
npm install

2. Install Dependencies

# 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

3. Project Structure

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/

Core Components

1. Main Dashboard Component

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>
  );
};

2. Store Memory Component

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>
  );
};

3. Search Component with Results

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>
  );
};

MCP Integration

1. Memory Service Class

// 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 });
  }
}

2. Window Interface Types

// 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;
}

Error Handling

1. Global Error Boundary

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;
  }
}

2. Async Error Handling

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 };
};

Configuration Management

1. Configuration Validation

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)
  );
};

2. Configuration UI Helper

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>
  );
};

Best Practices

1. Loading States

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>
);

2. Debouncing Search

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]
  );
};

3. Optimistic Updates

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');
  }
};

4. Responsive Design

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>

Conclusion

Building a React dashboard for MCP Memory Service involves:

  1. Clear architecture - Separation of UI and service logic
  2. Type safety - TypeScript throughout
  3. Modern UI - Tailwind CSS with shadcn/ui components
  4. Error handling - Graceful failure recovery
  5. Performance - Loading states, debouncing, and optimization

The result is a professional, responsive dashboard that provides an excellent user experience for memory management operations.

Clone this wiki locally