diff options
| author | MaheshtheDev <[email protected]> | 2025-10-08 19:07:13 +0000 |
|---|---|---|
| committer | MaheshtheDev <[email protected]> | 2025-10-08 19:07:13 +0000 |
| commit | d71ce03c4623c56851248305aecc78d7b21da80e (patch) | |
| tree | d17d18c8e5d558e53a4efe9349b7dc338206b9c1 /apps | |
| parent | remove comments (diff) | |
| download | supermemory-d71ce03c4623c56851248305aecc78d7b21da80e.tar.xz supermemory-d71ce03c4623c56851248305aecc78d7b21da80e.zip | |
feat: manual mcp and quick click to open (#464)
### Enhanced the Connect AI Modal with manual configuration options and improved MCP integration.
### What changed?
- Added a new "Manual Config" tab in the MCP URL section that generates and displays API keys for authentication
- Implemented automatic API key generation for manual MCP configuration
- Added URL parameter support (`?mcp=manual`) to directly open the MCP modal with manual configuration
- Improved the UI with better tab labels and more descriptive instructions
- Added copy functionality for configuration JSON with visual feedback
- Refactored the ConnectAIModal component to accept new props: `openInitialClient` and `openInitialTab`
- Added state management for API keys and copied status
Diffstat (limited to 'apps')
| -rw-r--r-- | apps/web/components/connect-ai-modal.tsx | 217 | ||||
| -rw-r--r-- | apps/web/components/header.tsx | 54 |
2 files changed, 227 insertions, 44 deletions
diff --git a/apps/web/components/connect-ai-modal.tsx b/apps/web/components/connect-ai-modal.tsx index b576934f..eb1597b5 100644 --- a/apps/web/components/connect-ai-modal.tsx +++ b/apps/web/components/connect-ai-modal.tsx @@ -1,6 +1,9 @@ "use client" import { $fetch } from "@lib/api" +import { authClient } from "@lib/auth" +import { useAuth } from "@lib/auth-context" +import { generateId } from "@lib/generate-id" import { useForm } from "@tanstack/react-form" import { useMutation, useQuery } from "@tanstack/react-query" import { Button } from "@ui/components/button" @@ -21,7 +24,7 @@ import { SelectValue, } from "@ui/components/select" import { CopyableCell } from "@ui/copyable-cell" -import { CopyIcon, ExternalLink, Loader2 } from "lucide-react" +import { CheckIcon, CopyIcon, ExternalLink, Loader2 } from "lucide-react" import Image from "next/image" import { useEffect, useState } from "react" import { toast } from "sonner" @@ -65,16 +68,21 @@ interface ConnectAIModalProps { children: React.ReactNode open?: boolean onOpenChange?: (open: boolean) => void + openInitialClient?: "mcp-url" | null + openInitialTab?: "oneClick" | "manual" | null } export function ConnectAIModal({ children, open, onOpenChange, + openInitialClient, + openInitialTab, }: ConnectAIModalProps) { + const { org } = useAuth() const [selectedClient, setSelectedClient] = useState< keyof typeof clients | null - >(null) + >(openInitialClient || null) const [internalIsOpen, setInternalIsOpen] = useState(false) const isOpen = open !== undefined ? open : internalIsOpen const setIsOpen = onOpenChange || setInternalIsOpen @@ -83,6 +91,11 @@ export function ConnectAIModal({ const [cursorInstallTab, setCursorInstallTab] = useState< "oneClick" | "manual" >("oneClick") + const [mcpUrlTab, setMcpUrlTab] = useState<"oneClick" | "manual">( + openInitialTab || "oneClick", + ) + const [manualApiKey, setManualApiKey] = useState<string | null>(null) + const [isCopied, setIsCopied] = useState(false) const [projectId, setProjectId] = useState("default") @@ -163,6 +176,46 @@ export function ConnectAIModal({ }, }) + const createMcpApiKeyMutation = useMutation({ + mutationFn: async () => { + if (!org?.id) { + throw new Error("Organization ID is required") + } + + const res = await authClient.apiKey.create({ + metadata: { + organizationId: org?.id, + type: "mcp-manual", + }, + name: `mcp-manual-${generateId().slice(0, 8)}`, + prefix: `sm_${org?.id}_`, + }) + return res.key + }, + onSuccess: (apiKey) => { + setManualApiKey(apiKey) + toast.success("API key created successfully!") + }, + onError: (error) => { + toast.error("Failed to create API key", { + description: error instanceof Error ? error.message : "Unknown error", + }) + }, + }) + + // biome-ignore lint/correctness/useExhaustiveDependencies(createMcpApiKeyMutation.mutate): we need to mutate the mutation + useEffect(() => { + if (openInitialClient) { + setSelectedClient(openInitialClient as keyof typeof clients) + if (openInitialTab) { + setMcpUrlTab(openInitialTab) + if (org?.id) { + createMcpApiKeyMutation.mutate() + } + } + } + }, [openInitialClient, openInitialTab, org?.id]) + function generateInstallCommand() { if (!selectedClient) return "" @@ -279,7 +332,7 @@ export function ConnectAIModal({ {selectedClient === "cursor" ? "Install Supermemory MCP" : selectedClient === "mcp-url" - ? "MCP Server URL" + ? "MCP Server Configuration" : "Select Target Project (Optional)"} </h3> </div> @@ -287,7 +340,9 @@ export function ConnectAIModal({ <div className={cn( "flex-col gap-2 hidden", - selectedClient === "cursor" && "flex", + (selectedClient === "cursor" || + selectedClient === "mcp-url") && + "flex", )} > {/* Tabs */} @@ -295,25 +350,51 @@ export function ConnectAIModal({ <div className="flex bg-muted/50 rounded-full p-1 border border-border"> <button className={`px-3 py-1.5 text-xs font-medium rounded-full transition-all ${ - cursorInstallTab === "oneClick" + ( + selectedClient === "cursor" + ? cursorInstallTab + : mcpUrlTab + ) === "oneClick" ? "bg-background text-foreground border border-border shadow-sm" : "text-muted-foreground hover:text-foreground" }`} - onClick={() => setCursorInstallTab("oneClick")} + onClick={() => + selectedClient === "cursor" + ? setCursorInstallTab("oneClick") + : setMcpUrlTab("oneClick") + } type="button" > - One Click Install + {selectedClient === "mcp-url" + ? "Quick Setup" + : "One Click Install"} </button> <button className={`px-3 py-1.5 text-xs font-medium rounded-full transition-all ${ - cursorInstallTab === "manual" + ( + selectedClient === "cursor" + ? cursorInstallTab + : mcpUrlTab + ) === "manual" ? "bg-background text-foreground border border-border shadow-sm" : "text-muted-foreground hover:text-foreground" }`} - onClick={() => setCursorInstallTab("manual")} + onClick={() => { + if (selectedClient === "cursor") { + setCursorInstallTab("manual") + } else { + setMcpUrlTab("manual") + if ( + !manualApiKey && + !createMcpApiKeyMutation.isPending + ) { + createMcpApiKeyMutation.mutate() + } + } + }} type="button" > - Manual Install + Manual Config </button> </div> </div> @@ -396,30 +477,98 @@ export function ConnectAIModal({ )} </div> ) : selectedClient === "mcp-url" ? ( - <div className="space-y-2"> - <div className="relative"> - <Input - className="font-mono text-xs w-full pr-10" - readOnly - value="https://api.supermemory.ai/mcp" - /> - <Button - className="absolute top-[-1px] right-0 cursor-pointer" - onClick={() => { - navigator.clipboard.writeText( - "https://api.supermemory.ai/mcp", - ) - analytics.mcpInstallCmdCopied() - toast.success("Copied to clipboard!") - }} - variant="ghost" - > - <CopyIcon className="size-4" /> - </Button> - </div> - <p className="text-xs text-muted-foreground"> - Use this URL to configure supermemory in your AI assistant - </p> + <div className="space-y-4"> + {mcpUrlTab === "oneClick" ? ( + <div className="space-y-2"> + <p className="text-sm text-muted-foreground"> + Use this URL to quickly configure supermemory in your AI + assistant + </p> + <div className="relative"> + <Input + className="font-mono text-xs w-full pr-10" + readOnly + value="https://api.supermemory.ai/mcp" + /> + <Button + className="absolute top-[-1px] right-0 cursor-pointer" + onClick={() => { + navigator.clipboard.writeText( + "https://api.supermemory.ai/mcp", + ) + analytics.mcpInstallCmdCopied() + toast.success("Copied to clipboard!") + }} + variant="ghost" + > + <CopyIcon className="size-4" /> + </Button> + </div> + </div> + ) : ( + <div className="space-y-3"> + <p className="text-sm text-muted-foreground"> + Add this configuration to your MCP settings file with + authentication + </p> + {createMcpApiKeyMutation.isPending ? ( + <div className="flex items-center justify-center p-8"> + <Loader2 className="h-6 w-6 animate-spin text-primary" /> + </div> + ) : ( + <> + <div className="relative"> + <pre className="bg-muted border border-border rounded-lg p-4 pr-12 text-xs overflow-x-auto max-w-full"> + <code className="font-mono block whitespace-pre-wrap break-all"> + {`{ + "supermemory-mcp": { + "command": "npx", + "args": ["-y", "mcp-remote", "https://api.supermemory.ai/mcp"], + "env": {}, + "headers": { + "Authorization": "Bearer ${manualApiKey || "your-api-key-here"}" + } + } +}`} + </code> + </pre> + <Button + className="absolute top-2 right-2 cursor-pointer h-8 w-8 p-0 bg-muted/80 hover:bg-muted" + onClick={() => { + const config = `{ + "supermemory-mcp": { + "command": "npx", + "args": ["-y", "mcp-remote", "https://api.supermemory.ai/mcp"], + "env": {}, + "headers": { + "Authorization": "Bearer ${manualApiKey || "your-api-key-here"}" + } + } +}` + navigator.clipboard.writeText(config) + analytics.mcpInstallCmdCopied() + toast.success("Copied to clipboard!") + setIsCopied(true) + setTimeout(() => setIsCopied(false), 2000) + }} + variant="ghost" + size="icon" + > + {isCopied ? ( + <CheckIcon className="size-3.5 text-green-600" /> + ) : ( + <CopyIcon className="size-3.5" /> + )} + </Button> + </div> + <p className="text-xs text-muted-foreground"> + The API key is included as a Bearer token in the + Authorization header + </p> + </> + )} + </div> + )} </div> ) : ( <div className="max-w-md"> diff --git a/apps/web/components/header.tsx b/apps/web/components/header.tsx index 23fb5cf3..19a6ec3a 100644 --- a/apps/web/components/header.tsx +++ b/apps/web/components/header.tsx @@ -28,7 +28,7 @@ import { Tooltip, TooltipContent, TooltipTrigger } from "@ui/components/tooltip" import { useAuth } from "@lib/auth-context" import { ConnectAIModal } from "./connect-ai-modal" import { useTheme } from "next-themes" -import { usePathname, useRouter } from "next/navigation" +import { usePathname, useRouter, useSearchParams } from "next/navigation" import { MCPIcon } from "./menu" import { authClient } from "@lib/auth" import { analytics } from "@/lib/analytics" @@ -44,10 +44,11 @@ import { import { ScrollArea } from "@ui/components/scroll-area" import { formatDistanceToNow } from "date-fns" import { cn } from "@lib/utils" -import { useMemo, useState } from "react" +import { useEffect, useMemo, useState } from "react" export function Header({ onAddMemory }: { onAddMemory?: () => void }) { const { user } = useAuth() + const searchParams = useSearchParams() const { theme, setTheme } = useTheme() const router = useRouter() const { setIsOpen: setGraphModalOpen } = useGraphModal() @@ -61,6 +62,13 @@ export function Header({ onAddMemory }: { onAddMemory?: () => void }) { const { selectedProject } = useProject() const pathname = usePathname() const [isDialogOpen, setIsDialogOpen] = useState(false) + const [mcpModalOpen, setMcpModalOpen] = useState(false) + const [mcpInitialClient, setMcpInitialClient] = useState<"mcp-url" | null>( + null, + ) + const [mcpInitialTab, setMcpInitialTab] = useState< + "oneClick" | "manual" | null + >(null) const sorted = useMemo(() => { return [...conversations].sort((a, b) => @@ -68,6 +76,22 @@ export function Header({ onAddMemory }: { onAddMemory?: () => void }) { ) }, [conversations]) + useEffect(() => { + console.log("searchParams", searchParams.get("mcp")) + const mcpParam = searchParams.get("mcp") + if (mcpParam === "manual") { + setMcpInitialClient("mcp-url") + setMcpInitialTab("manual") + setMcpModalOpen(true) + const newSearchParams = new URLSearchParams(searchParams.toString()) + newSearchParams.delete("mcp") + const newUrl = `${ + window.location.pathname + }${newSearchParams.toString() ? `?${newSearchParams.toString()}` : ""}` + window.history.replaceState({}, "", newUrl) + } + }, [searchParams]) + function handleNewChat() { analytics.newChatStarted() const newId = crypto.randomUUID() @@ -240,18 +264,28 @@ export function Header({ onAddMemory }: { onAddMemory?: () => void }) { <p>Graph View</p> </TooltipContent> </Tooltip> - <ConnectAIModal> - <Tooltip> + <Tooltip> + <ConnectAIModal + open={mcpModalOpen} + onOpenChange={setMcpModalOpen} + openInitialClient={mcpInitialClient} + openInitialTab={mcpInitialTab} + > <TooltipTrigger asChild> - <Button variant="ghost" size="sm" className="gap-1.5"> + <Button + variant="ghost" + size="sm" + className="gap-1.5" + onClick={() => setMcpModalOpen(true)} + > <MCPIcon className="h-4 w-4" /> </Button> </TooltipTrigger> - <TooltipContent> - <p>Connect to AI (MCP)</p> - </TooltipContent> - </Tooltip> - </ConnectAIModal> + </ConnectAIModal> + <TooltipContent> + <p>Connect to AI (MCP)</p> + </TooltipContent> + </Tooltip> <DropdownMenu> <DropdownMenuTrigger> <Avatar className="border border-border h-8 w-8 md:h-10 md:w-10"> |