8.0 KiB

App Build Agent Prompt

Build ALL React MCP Apps for the {{NAME}} MCP server at {{DIR}}.

Foundation + Tools Already Exist

DO NOT modify any existing files. Only ADD app files under src/ui/react-app/.

Apps to Build

{{APP_LIST}}

Quality Requirements — 2026 Standards

Each App Directory Structure

src/ui/react-app/{app-name}/
├── App.tsx          # Main component
├── index.html       # Entry point
├── main.tsx         # React mount with ErrorBoundary
├── styles.css       # Dark theme styles
└── vite.config.ts   # Build config

main.tsx — With Error Boundary + Suspense

import React, { Suspense } from 'react';
import ReactDOM from 'react-dom/client';
import './styles.css';

// Lazy load the main app component
const App = React.lazy(() => import('./App'));

// Error Boundary
class ErrorBoundary extends React.Component<
  { children: React.ReactNode },
  { hasError: boolean; error?: Error }
> {
  constructor(props: { children: React.ReactNode }) {
    super(props);
    this.state = { hasError: false };
  }
  static getDerivedStateFromError(error: Error) {
    return { hasError: true, error };
  }
  render() {
    if (this.state.hasError) {
      return (
        <div style={{ padding: 40, color: '#f87171', background: '#0f172a', minHeight: '100vh' }}>
          <h2>Something went wrong</h2>
          <pre style={{ color: '#94a3b8' }}>{this.state.error?.message}</pre>
          <button onClick={() => this.setState({ hasError: false })}
            style={{ marginTop: 16, padding: '8px 16px', background: '#3b82f6', color: '#fff', border: 'none', borderRadius: 6, cursor: 'pointer' }}>
            Retry
          </button>
        </div>
      );
    }
    return this.props.children;
  }
}

// Loading skeleton
function LoadingSkeleton() {
  return (
    <div style={{ padding: 40, background: '#0f172a', minHeight: '100vh' }}>
      <div className="skeleton" style={{ height: 32, width: '40%', marginBottom: 24 }} />
      <div className="skeleton" style={{ height: 200, width: '100%', marginBottom: 16 }} />
      <div className="skeleton" style={{ height: 200, width: '100%' }} />
    </div>
  );
}

ReactDOM.createRoot(document.getElementById('root')!).render(
  <React.StrictMode>
    <ErrorBoundary>
      <Suspense fallback={<LoadingSkeleton />}>
        <App />
      </Suspense>
    </ErrorBoundary>
  </React.StrictMode>
);

App.tsx — Modern React Patterns

import React, { useState, useMemo, useCallback, useTransition } from 'react';

// Debounce hook for search
function useDebounce<T>(value: T, delay: number): T {
  const [debounced, setDebounced] = React.useState(value);
  React.useEffect(() => {
    const t = setTimeout(() => setDebounced(value), delay);
    return () => clearTimeout(t);
  }, [value, delay]);
  return debounced;
}

// Toast notification system
function useToast() {
  const [toasts, setToasts] = useState<Array<{ id: number; message: string; type: 'success' | 'error' }>>([]);
  const show = useCallback((message: string, type: 'success' | 'error' = 'success') => {
    const id = Date.now();
    setToasts(prev => [...prev, { id, message, type }]);
    setTimeout(() => setToasts(prev => prev.filter(t => t.id !== id)), 3000);
  }, []);
  return { toasts, show };
}

export default function App() {
  const [search, setSearch] = useState('');
  const [isPending, startTransition] = useTransition();
  const debouncedSearch = useDebounce(search, 300);
  const { toasts, show: showToast } = useToast();

  // Mock data — replace with MCP tool calls
  const [data] = useState(MOCK_DATA);

  // Client-side filtering with useMemo
  const filtered = useMemo(() =>
    data.filter(item =>
      item.name.toLowerCase().includes(debouncedSearch.toLowerCase())
    ), [data, debouncedSearch]);

  return (
    <div className="app">
      {/* Header with search */}
      <header className="header">
        <h1>{{App Title}}</h1>
        <div className="search-wrapper">
          <input
            type="text"
            placeholder="Search..."
            value={search}
            onChange={(e) => startTransition(() => setSearch(e.target.value))}
            className="search-input"
          />
          {isPending && <span className="spinner" />}
        </div>
      </header>

      {/* Stats cards */}
      <div className="stats-grid">
        {/* Stat cards with numbers */}
      </div>

      {/* Data grid or dashboard content */}
      <div className="content">
        {filtered.length === 0 ? (
          <div className="empty-state">
            <p>No results found</p>
            <span className="empty-hint">Try adjusting your search</span>
          </div>
        ) : (
          <div className="data-grid">
            {filtered.map(item => (
              <div key={item.id} className="card">
                {/* Card content */}
              </div>
            ))}
          </div>
        )}
      </div>

      {/* Toast notifications */}
      <div className="toast-container">
        {toasts.map(t => (
          <div key={t.id} className={`toast toast-${t.type}`}>{t.message}</div>
        ))}
      </div>
    </div>
  );
}

styles.css — Dark Theme with CSS Variables + Skeleton Animation

:root {
  --bg-primary: #0f172a;
  --bg-secondary: #1e293b;
  --bg-tertiary: #334155;
  --text-primary: #f1f5f9;
  --text-secondary: #94a3b8;
  --text-muted: #64748b;
  --accent: #3b82f6;
  --accent-hover: #2563eb;
  --success: #22c55e;
  --warning: #f59e0b;
  --error: #ef4444;
  --border: #334155;
  --radius: 8px;
  --shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.3);
}

* { margin: 0; padding: 0; box-sizing: border-box; }

body {
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
  background: var(--bg-primary);
  color: var(--text-primary);
  min-height: 100vh;
}

/* Skeleton loading animation */
.skeleton {
  background: linear-gradient(90deg, var(--bg-secondary) 25%, var(--bg-tertiary) 50%, var(--bg-secondary) 75%);
  background-size: 200% 100%;
  animation: shimmer 1.5s infinite;
  border-radius: var(--radius);
}
@keyframes shimmer { 0% { background-position: 200% 0; } 100% { background-position: -200% 0; } }

/* Search input */
.search-input {
  background: var(--bg-secondary);
  border: 1px solid var(--border);
  color: var(--text-primary);
  padding: 10px 16px;
  border-radius: var(--radius);
  width: 280px;
  transition: border-color 0.2s;
}
.search-input:focus { outline: none; border-color: var(--accent); }
.search-input::placeholder { color: var(--text-muted); }

/* Stats grid */
.stats-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 16px; margin-bottom: 24px; }

/* Cards */
.card {
  background: var(--bg-secondary);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 20px;
  transition: transform 0.15s, box-shadow 0.15s;
}
.card:hover { transform: translateY(-2px); box-shadow: var(--shadow); }

/* Empty state */
.empty-state { text-align: center; padding: 60px 20px; color: var(--text-secondary); }
.empty-hint { font-size: 14px; color: var(--text-muted); }

/* Toast notifications */
.toast-container { position: fixed; bottom: 20px; right: 20px; display: flex; flex-direction: column; gap: 8px; z-index: 1000; }
.toast {
  padding: 12px 20px;
  border-radius: var(--radius);
  color: white;
  font-weight: 500;
  animation: slideIn 0.3s ease;
  box-shadow: var(--shadow);
}
.toast-success { background: var(--success); }
.toast-error { background: var(--error); }
@keyframes slideIn { from { transform: translateX(100%); opacity: 0; } to { transform: translateX(0); opacity: 1; } }

/* Responsive */
@media (max-width: 768px) {
  .stats-grid { grid-template-columns: repeat(2, 1fr); }
  .search-input { width: 100%; }
}
@media (max-width: 480px) {
  .stats-grid { grid-template-columns: 1fr; }
}

Rules

  • Each app MUST have: ErrorBoundary, Suspense, loading skeleton, empty state, toast system
  • Debounced search on all grids/lists
  • CSS custom properties (not hardcoded colors)
  • Mobile responsive
  • DO NOT modify existing files
  • Commit when done: git add src/ui/ && git commit -m "{{name}}: Add {{count}} React MCP apps"