mirror of
https://github.com/supermemoryai/supermemory.git
synced 2026-08-28 05:25:33 +00:00
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
This commit is contained in:
parent
a622318325
commit
d71ce03c46
2 changed files with 227 additions and 44 deletions
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue