aboutsummaryrefslogtreecommitdiff
path: root/apps
diff options
context:
space:
mode:
authorMaheshtheDev <[email protected]>2025-10-08 19:07:13 +0000
committerMaheshtheDev <[email protected]>2025-10-08 19:07:13 +0000
commitd71ce03c4623c56851248305aecc78d7b21da80e (patch)
treed17d18c8e5d558e53a4efe9349b7dc338206b9c1 /apps
parentremove comments (diff)
downloadsupermemory-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.tsx217
-rw-r--r--apps/web/components/header.tsx54
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">