diff --git a/client/package.json b/client/package.json
index b30115c0b..96e35a98b 100644
--- a/client/package.json
+++ b/client/package.json
@@ -9,23 +9,37 @@
"preview": "vite preview"
},
"dependencies": {
+ "@dnd-kit/core": "^6.3.1",
+ "@dnd-kit/modifiers": "^9.0.0",
+ "@dnd-kit/sortable": "^10.0.0",
+ "@dnd-kit/utilities": "^3.2.2",
+ "@hookform/resolvers": "^3.10.0",
"@tanstack/react-query": "^5.83.0",
"@tanstack/react-table": "^8.21.3",
+ "@uiw/react-json-view": "^2.0.0-alpha.33",
"autoprefixer": "^10.4.21",
"class-variance-authority": "^0.7.1",
+ "classnames": "^2.5.1",
"clsx": "^2.1.1",
"cmdk": "^1.1.1",
"date-fns": "^3.6.0",
+ "embla-carousel-react": "^8.6.0",
+ "fast-deep-equal": "^3.1.3",
"framer-motion": "^12.23.6",
"input-otp": "^1.4.2",
"lucide-react": "^0.525.0",
"next-themes": "^0.4.6",
+ "radix-ui": "^1.4.2",
"react": "19.1.0",
+ "react-day-picker": "^9.8.0",
"react-dom": "19.1.0",
"react-hook-form": "^7.60.0",
"react-markdown": "^10.1.0",
"react-resizable-panels": "^3.0.3",
+ "react18-json-view": "^0.2.9",
+ "recharts": "^2.15.4",
"remark-gfm": "^4.0.1",
+ "simple-icons": "^15.6.0",
"sonner": "^2.0.6",
"tailwind-merge": "^3.3.1",
"vaul": "^1.1.2",
diff --git a/client/public/catalyst.png b/client/public/catalyst.png
new file mode 100644
index 000000000..b32348ae3
Binary files /dev/null and b/client/public/catalyst.png differ
diff --git a/client/public/claude_logo.png b/client/public/claude_logo.png
new file mode 100644
index 000000000..931464775
Binary files /dev/null and b/client/public/claude_logo.png differ
diff --git a/client/public/demo_1.png b/client/public/demo_1.png
new file mode 100644
index 000000000..7499b8b00
Binary files /dev/null and b/client/public/demo_1.png differ
diff --git a/client/public/demo_2.png b/client/public/demo_2.png
new file mode 100644
index 000000000..1d13989f4
Binary files /dev/null and b/client/public/demo_2.png differ
diff --git a/client/public/demo_3.png b/client/public/demo_3.png
new file mode 100644
index 000000000..d2de09409
Binary files /dev/null and b/client/public/demo_3.png differ
diff --git a/client/public/file.svg b/client/public/file.svg
new file mode 100644
index 000000000..004145cdd
--- /dev/null
+++ b/client/public/file.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/client/public/globe.svg b/client/public/globe.svg
new file mode 100644
index 000000000..567f17b0d
--- /dev/null
+++ b/client/public/globe.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/client/public/mcp.svg b/client/public/mcp.svg
new file mode 100644
index 000000000..5cd83a8bf
--- /dev/null
+++ b/client/public/mcp.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/client/public/mcp_jam.svg b/client/public/mcp_jam.svg
new file mode 100644
index 000000000..8ae6e7770
--- /dev/null
+++ b/client/public/mcp_jam.svg
@@ -0,0 +1,12 @@
+
diff --git a/client/public/mcp_jam_dark.png b/client/public/mcp_jam_dark.png
new file mode 100644
index 000000000..a3decf590
Binary files /dev/null and b/client/public/mcp_jam_dark.png differ
diff --git a/client/public/mcp_jam_light.png b/client/public/mcp_jam_light.png
new file mode 100644
index 000000000..a06eccdb2
Binary files /dev/null and b/client/public/mcp_jam_light.png differ
diff --git a/client/public/next.svg b/client/public/next.svg
new file mode 100644
index 000000000..5174b28c5
--- /dev/null
+++ b/client/public/next.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/client/public/ollama_dark.png b/client/public/ollama_dark.png
new file mode 100644
index 000000000..1f7a4ddd4
Binary files /dev/null and b/client/public/ollama_dark.png differ
diff --git a/client/public/ollama_logo.svg b/client/public/ollama_logo.svg
new file mode 100644
index 000000000..d7780867b
--- /dev/null
+++ b/client/public/ollama_logo.svg
@@ -0,0 +1,7 @@
+
\ No newline at end of file
diff --git a/client/public/openai_logo.png b/client/public/openai_logo.png
new file mode 100644
index 000000000..ca0ef35c0
Binary files /dev/null and b/client/public/openai_logo.png differ
diff --git a/client/public/vercel.svg b/client/public/vercel.svg
new file mode 100644
index 000000000..770539603
--- /dev/null
+++ b/client/public/vercel.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/client/public/window.svg b/client/public/window.svg
new file mode 100644
index 000000000..b2b2a44f6
--- /dev/null
+++ b/client/public/window.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/client/src/App.tsx b/client/src/App.tsx
index 5a6573963..9cf33ef82 100644
--- a/client/src/App.tsx
+++ b/client/src/App.tsx
@@ -1,30 +1,130 @@
-import { useState } from 'react'
+import { useState } from "react";
-function App() {
- const [count, setCount] = useState(0)
+import { ServersTab } from "./components/ServersTab";
+import { ToolsTab } from "./components/ToolsTab";
+import { ResourcesTab } from "./components/ResourcesTab";
+import { PromptsTab } from "./components/PromptsTab";
+import { ChatTab } from "./components/ChatTab";
+import { SettingsTab } from "./components/SettingsTab";
+import { TracingTab } from "./components/TracingTab";
+import { MCPSidebar } from "./components/mcp-sidebar";
+import { ActiveServerSelector } from "./components/ActiveServerSelector";
+import {
+ SidebarInset,
+ SidebarProvider,
+ SidebarTrigger,
+} from "./components/ui/sidebar";
+import { ThemeSwitcher } from "./components/sidebar/theme-switcher";
+import { useAppState } from "./hooks/use-app-state";
+import { PreferencesStoreProvider } from "./stores/preferences/preferences-provider";
+import { Toaster } from "./components/ui/sonner";
- return (
-
-
-
MCP Inspector
-
-
-
- Click the button to test React functionality
-
+// Import global styles
+import "./index.css";
+
+export default function App() {
+ const [activeTab, setActiveTab] = useState("servers");
+
+ const {
+ appState,
+ isLoading,
+ connectedServerConfigs,
+ selectedMCPConfig,
+ handleConnect,
+ handleDisconnect,
+ handleReconnect,
+ handleUpdate,
+ setSelectedServer,
+ toggleServerSelection,
+ selectedMCPConfigsMap,
+ setSelectedMultipleServersToAllServers,
+ } = useAppState();
+
+ const handleNavigate = (section: string) => {
+ setActiveTab(section);
+ if (section === "chat") {
+ setSelectedMultipleServersToAllServers();
+ }
+ };
+
+ if (isLoading) {
+ return (
+
+
-
- Phase 1 Migration: Hono + Vite foundation is ready!
- Frontend: http://localhost:8080 | Backend: http://localhost:8001
-
-
- )
-}
+ );
+ }
+
+ return (
+
+
+
+
+
+
+
+ {/* Active Server Selector - Only show on Tools, Resources, and Prompts pages */}
+ {(activeTab === "tools" ||
+ activeTab === "resources" ||
+ activeTab === "prompts" ||
+ activeTab === "chat") && (
+
+ )}
+
+ {/* Content Areas */}
+ {activeTab === "servers" && (
+
+ )}
+
+ {activeTab === "tools" && (
+
+ )}
+
+ {activeTab === "resources" && (
+
+ )}
+
+ {activeTab === "prompts" && (
+
+ )}
+
+ {activeTab === "chat" && (
+
+ )}
+
+ {activeTab === "tracing" &&
}
-export default App
\ No newline at end of file
+ {activeTab === "settings" &&
}
+
+
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/client/src/components/ActiveServerSelector.tsx b/client/src/components/ActiveServerSelector.tsx
new file mode 100644
index 000000000..2ce13e489
--- /dev/null
+++ b/client/src/components/ActiveServerSelector.tsx
@@ -0,0 +1,144 @@
+import { useState } from "react";
+import { ServerWithName } from "@/hooks/use-app-state";
+import { cn } from "@/lib/utils";
+import { AddServerModal } from "./connection/AddServerModal";
+import { ServerFormData } from "@/lib/types";
+import { Check } from "lucide-react";
+
+interface ActiveServerSelectorProps {
+ connectedServerConfigs: Record
;
+ selectedServer: string;
+ selectedMultipleServers: string[];
+ isMultiSelectEnabled: boolean;
+ onServerChange: (server: string) => void;
+ onMultiServerToggle: (server: string) => void;
+ onConnect: (formData: ServerFormData) => void;
+}
+
+function getStatusColor(status: string): string {
+ switch (status) {
+ case "connected":
+ return "bg-green-500 dark:bg-green-400";
+ case "connecting":
+ return "bg-yellow-500 dark:bg-yellow-400 animate-pulse";
+ case "failed":
+ return "bg-red-500 dark:bg-red-400";
+ case "disconnected":
+ return "bg-muted-foreground";
+ default:
+ return "bg-muted-foreground";
+ }
+}
+
+function getStatusText(status: string): string {
+ switch (status) {
+ case "connected":
+ return "Connected";
+ case "connecting":
+ return "Connecting...";
+ case "failed":
+ return "Failed";
+ case "disconnected":
+ return "Disconnected";
+ default:
+ return "Unknown";
+ }
+}
+
+export function ActiveServerSelector({
+ connectedServerConfigs,
+ selectedServer,
+ selectedMultipleServers,
+ isMultiSelectEnabled,
+ onServerChange,
+ onMultiServerToggle,
+ onConnect,
+}: ActiveServerSelectorProps) {
+ const [isAddModalOpen, setIsAddModalOpen] = useState(false);
+ const servers = Object.entries(connectedServerConfigs);
+ if (servers.length === 0) {
+ return (
+
+ No servers connected. Add a server to get started.
+
+ );
+ }
+
+ return (
+
+
+ {servers.map(([name, serverConfig]) => {
+ const isSelected = isMultiSelectEnabled
+ ? selectedMultipleServers.includes(name)
+ : selectedServer === name;
+
+ return (
+
+ );
+ })}
+
+ {/* Add Server Button */}
+
+
+
+
setIsAddModalOpen(false)}
+ onConnect={onConnect}
+ />
+
+ );
+}
diff --git a/client/src/components/ChatTab.tsx b/client/src/components/ChatTab.tsx
new file mode 100644
index 000000000..a7bd220f0
--- /dev/null
+++ b/client/src/components/ChatTab.tsx
@@ -0,0 +1,249 @@
+
+import { useRef, useEffect, useState } from "react";
+import { MessageCircle } from "lucide-react";
+import { MastraMCPServerDefinition } from "@/lib/types";
+import { useChat } from "@/hooks/use-chat";
+import { Message } from "./chat/message";
+import { ChatInput } from "./chat/chat-input";
+import { ElicitationDialog } from "./ElicitationDialog";
+import { TooltipProvider } from "./ui/tooltip";
+import { motion, AnimatePresence } from "framer-motion";
+import { toast } from "sonner";
+
+interface ChatTabProps {
+ serverConfigs?: Record;
+ systemPrompt?: string;
+}
+
+export function ChatTab({ serverConfigs, systemPrompt = "" }: ChatTabProps) {
+ const messagesContainerRef = useRef(null);
+ const [isAtBottom, setIsAtBottom] = useState(true);
+
+ const {
+ messages,
+ isLoading,
+ error,
+ input,
+ setInput,
+ sendMessage,
+ stopGeneration,
+ regenerateMessage,
+ clearChat,
+ model,
+ availableModels,
+ setModel,
+ elicitationRequest,
+ elicitationLoading,
+ handleElicitationResponse,
+ } = useChat({
+ serverConfigs: serverConfigs,
+ systemPrompt,
+ onError: (error) => {
+ toast.error(error);
+ },
+ });
+ console.log("availableModels", availableModels);
+ const hasMessages = messages.length > 0;
+ // Auto-scroll to bottom when new messages arrive
+ useEffect(() => {
+ if (isAtBottom && messagesContainerRef.current) {
+ messagesContainerRef.current.scrollTop =
+ messagesContainerRef.current.scrollHeight;
+ }
+ }, [messages, isAtBottom]);
+
+ // Check if user is at bottom
+ const handleScroll = () => {
+ if (!messagesContainerRef.current) return;
+
+ const { scrollTop, scrollHeight, clientHeight } =
+ messagesContainerRef.current;
+ const threshold = 100;
+ const atBottom = scrollHeight - scrollTop - clientHeight < threshold;
+
+ setIsAtBottom(atBottom);
+ };
+
+ const handleCopyMessage = (content: string) => {
+ navigator.clipboard.writeText(content);
+ };
+ console.log(availableModels);
+ // Empty state - centered input
+ if (!hasMessages) {
+ return (
+
+
+ {/* Welcome Message */}
+
+
+
+ Let's test out your MCP servers!
+
+ {serverConfigs && (
+
+
+ Connected servers: {Object.keys(serverConfigs).join(", ")}
+
+
+ )}
+
+
+
+ {/* Centered Input */}
+
+
+ {availableModels.length === 0 && (
+
+ Configure API keys in Settings or start Ollama to enable chat
+
+ )}
+
+
+
+ {/* Elicitation Dialog */}
+
+
+ );
+ }
+
+ // Active state - messages with bottom input
+ return (
+
+
+ {/* Messages Area - Scrollable with bottom padding for input */}
+
+
+
+ {messages.map((message, index) => (
+
+ {}}
+ onRegenerate={regenerateMessage}
+ onCopy={handleCopyMessage}
+ showActions={true}
+ />
+
+ ))}
+ {/* Thinking indicator */}
+ {isLoading &&
+ messages.length > 0 &&
+ messages[messages.length - 1].role === "user" && (
+
+
+
+ )}
+
+
+
+
+ {/* Error Display - Absolute positioned above input */}
+
+ {error && (
+
+
+
+ )}
+
+
+ {/* Fixed Bottom Input - Absolute positioned */}
+
+
+ {/* Elicitation Dialog */}
+
+
+
+ );
+}
diff --git a/client/src/components/ElicitationDialog.tsx b/client/src/components/ElicitationDialog.tsx
new file mode 100644
index 000000000..8dcf9f6d9
--- /dev/null
+++ b/client/src/components/ElicitationDialog.tsx
@@ -0,0 +1,304 @@
+
+import React, { useState } from "react";
+import { Button } from "./ui/button";
+import { Input } from "./ui/input";
+import { Label } from "./ui/label";
+import { Textarea } from "./ui/textarea";
+import { Badge } from "./ui/badge";
+import {
+ Select,
+ SelectContent,
+ SelectItem,
+ SelectTrigger,
+ SelectValue,
+} from "./ui/select";
+import {
+ Dialog,
+ DialogContent,
+ DialogDescription,
+ DialogFooter,
+ DialogHeader,
+ DialogTitle,
+} from "./ui/dialog";
+import { MessageSquare, X, Check, RefreshCw } from "lucide-react";
+
+interface FormField {
+ name: string;
+ type: string;
+ description?: string;
+ required: boolean;
+ value: any;
+ enum?: string[];
+ minimum?: number;
+ maximum?: number;
+ pattern?: string;
+}
+
+interface ElicitationRequest {
+ requestId: string;
+ message: string;
+ schema: any;
+ timestamp: string;
+}
+
+interface ElicitationDialogProps {
+ elicitationRequest: ElicitationRequest | null;
+ onResponse: (
+ action: "accept" | "decline" | "cancel",
+ parameters?: Record,
+ ) => Promise;
+ loading?: boolean;
+}
+
+export function ElicitationDialog({
+ elicitationRequest,
+ onResponse,
+ loading = false,
+}: ElicitationDialogProps) {
+ const [fields, setFields] = useState([]);
+
+ // Generate form fields from schema when request changes
+ React.useEffect(() => {
+ if (elicitationRequest?.schema) {
+ generateFormFields(elicitationRequest.schema);
+ } else {
+ setFields([]);
+ }
+ }, [elicitationRequest]);
+
+ const generateFormFields = (schema: any) => {
+ if (!schema || !schema.properties) {
+ setFields([]);
+ return;
+ }
+
+ const formFields: FormField[] = [];
+ const required = schema.required || [];
+
+ Object.entries(schema.properties).forEach(([key, prop]: [string, any]) => {
+ const fieldType = prop.enum ? "enum" : prop.type || "string";
+ formFields.push({
+ name: key,
+ type: fieldType,
+ description: prop.description,
+ required: required.includes(key),
+ value: getDefaultValue(fieldType, prop.enum),
+ enum: prop.enum,
+ minimum: prop.minimum,
+ maximum: prop.maximum,
+ pattern: prop.pattern,
+ });
+ });
+
+ setFields(formFields);
+ };
+
+ const getDefaultValue = (type: string, enumValues?: string[]) => {
+ switch (type) {
+ case "enum":
+ return enumValues?.[0] || "";
+ case "string":
+ return "";
+ case "number":
+ case "integer":
+ return "";
+ case "boolean":
+ return false;
+ case "array":
+ return [];
+ case "object":
+ return {};
+ default:
+ return "";
+ }
+ };
+
+ const updateFieldValue = (fieldName: string, value: any) => {
+ setFields((prev) =>
+ prev.map((field) =>
+ field.name === fieldName ? { ...field, value } : field,
+ ),
+ );
+ };
+
+ const buildParameters = (): Record => {
+ const params: Record = {};
+ fields.forEach((field) => {
+ if (
+ field.value !== "" &&
+ field.value !== null &&
+ field.value !== undefined
+ ) {
+ let processedValue = field.value;
+
+ if (field.type === "number" || field.type === "integer") {
+ processedValue = Number(field.value);
+ } else if (field.type === "boolean") {
+ processedValue = Boolean(field.value);
+ } else if (field.type === "array" || field.type === "object") {
+ try {
+ processedValue = JSON.parse(field.value);
+ } catch {
+ processedValue = field.value;
+ }
+ }
+
+ params[field.name] = processedValue;
+ }
+ });
+ return params;
+ };
+
+ const handleResponse = async (action: "accept" | "decline" | "cancel") => {
+ if (action === "accept") {
+ // Validate required fields
+ const missingFields = fields.filter(
+ (field) => field.required && (!field.value || field.value === ""),
+ );
+
+ if (missingFields.length > 0) {
+ // You could show validation errors here
+ return;
+ }
+
+ const parameters = buildParameters();
+ await onResponse(action, parameters);
+ } else {
+ await onResponse(action);
+ }
+ };
+
+ const renderField = (field: FormField) => {
+ if (field.type === "enum") {
+ return (
+
+ );
+ } else if (field.type === "boolean") {
+ return (
+
+ updateFieldValue(field.name, e.target.checked)}
+ className="w-4 h-4 text-primary bg-background border-border rounded focus:ring-ring focus:ring-2"
+ />
+
+ {field.value ? "Enabled" : "Disabled"}
+
+
+ );
+ } else if (field.type === "array" || field.type === "object") {
+ return (
+