Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
209 changes: 203 additions & 6 deletions apps/web/components/integrations-view.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,19 @@ interface ConnectedKey {
createdAt?: string | null
}

interface ConnectedMcpKey {
keyId: string
keyStart: string | null
lastRequest?: string | null
createdAt?: string | null
}

function isMcpAuthMetadata(metadata: { sm_source?: string; sm_kind?: string }) {
return (
metadata.sm_source === "mcp" || metadata.sm_kind === "mcp_oauth_exchange"
)
}

function toIsoDate(value: string | Date | null | undefined): string | null {
if (!value) return null
const d = value instanceof Date ? value : new Date(value)
Expand Down Expand Up @@ -142,6 +155,34 @@ function parsePluginAuthKeys(
return { active, setup }
}

function parseMcpAuthKeys(
apiKeys: ListedApiKey[],
keyPrefix: (key: ListedApiKey) => string | null,
): ConnectedMcpKey[] {
const keys: ConnectedMcpKey[] = []
for (const key of apiKeys) {
if (key.enabled === false) continue
if (!key.metadata) continue
try {
const metadata =
typeof key.metadata === "string"
? (JSON.parse(key.metadata) as {
sm_source?: string
sm_kind?: string
})
: (key.metadata as { sm_source?: string; sm_kind?: string })
if (!isMcpAuthMetadata(metadata)) continue
keys.push({
keyId: key.id,
keyStart: keyPrefix(key),
lastRequest: toIsoDate(key.lastRequest),
createdAt: toIsoDate(key.createdAt),
})
} catch {}
}
return keys
}

type ListedApiKey = {
id: string
name?: string | null
Expand Down Expand Up @@ -1036,6 +1077,31 @@ function ActiveButton({
)
}

function McpConnectedPill({
connectedAt,
lastActive,
}: {
connectedAt?: string | null
lastActive?: string | null
}) {
return (
<span
className={cn(
dmSans125ClassName(),
"flex shrink-0 items-center gap-1.5 text-[12px] font-medium text-[#00AC3F] sm:text-[13px]",
)}
>
<span className="size-[7px] rounded-full bg-[#00AC3F]" />
Connected
{(lastActive ?? connectedAt) && (
<span className="text-[11px] font-normal text-[#737373]">
· {formatRelativeTime(lastActive ?? connectedAt)}
</span>
)}
</span>
)
}

function FinishSetupButton({ onClick }: { onClick: () => void }) {
return (
<PillButton onClick={onClick}>
Expand Down Expand Up @@ -1137,7 +1203,18 @@ interface ConnectorEntry {
onReconnect: () => void
}

type RailEntry = PluginEntry | ConnectorEntry
interface McpEntry {
kind: "mcp"
id: string
name: string
icon: ReactNode
connectionCount: number
createdAt: string | null
lastActive: string | null
onManage: () => void
}

type RailEntry = PluginEntry | ConnectorEntry | McpEntry

function railConnectionMeta(connection: Connection) {
const m = connection.metadata as Record<string, unknown> | undefined
Expand Down Expand Up @@ -1423,6 +1500,60 @@ function ConnectorRailRow({ entry }: { entry: ConnectorEntry }) {
)
}

function McpRailRow({ entry }: { entry: McpEntry }) {
const [expanded, setExpanded] = useState(false)
const lastTime = entry.lastActive ?? entry.createdAt
const suffix = [
entry.connectionCount > 1 ? `${entry.connectionCount} connections` : null,
lastTime ? formatRelativeTime(lastTime) : null,
]
.filter(Boolean)
.join(" · ")
return (
<RailRow
icon={entry.icon}
name={entry.name}
expanded={expanded}
onToggle={() => setExpanded((v) => !v)}
statusLine={
<div className="flex min-w-0 items-center gap-1.5">
<ActiveStatusDot />
{suffix && (
<span
className={cn(
dmSans125ClassName(),
"min-w-0 truncate text-[11px] text-[#737373]",
)}
>
· {suffix}
</span>
)}
</div>
}
>
{entry.createdAt && (
<RailDetail
label="Connected"
value={formatRelativeTime(entry.createdAt)}
/>
)}
{entry.lastActive && (
<RailDetail
label="Last active"
value={formatRelativeTime(entry.lastActive)}
/>
)}
<RailDetail
label="MCP keys"
value={`${entry.connectionCount} connected`}
/>
<div className="mt-1 flex flex-wrap gap-1.5">
<RailAction label="Manage" onClick={entry.onManage} />
</div>
</RailRow>
)
}

const SKELETON_KEYS = ["s1", "s2", "s3", "s4", "s5"]

function RailSkeleton({ rows }: { rows: number }) {
Expand Down Expand Up @@ -1521,6 +1652,8 @@ function ActiveConnectionsRail({
{entries.map((entry) =>
entry.kind === "plugin" ? (
<PluginRailRow key={entry.id} entry={entry} />
) : entry.kind === "mcp" ? (
<McpRailRow key={entry.id} entry={entry} />
) : (
<ConnectorRailRow key={entry.id} entry={entry} />
),
Expand Down Expand Up @@ -1879,6 +2012,8 @@ function MobileActivityPanel({
{entries.map((entry) =>
entry.kind === "plugin" ? (
<PluginRailRow key={entry.id} entry={entry} />
) : entry.kind === "mcp" ? (
<McpRailRow key={entry.id} entry={entry} />
) : (
<ConnectorRailRow key={entry.id} entry={entry} />
),
Expand Down Expand Up @@ -2321,9 +2456,11 @@ function CategoryFilterToggle({
function SectionRail({
label,
children,
headerSlot,
}: {
label: string
children: ReactNode
headerSlot?: ReactNode
}) {
const scrollRef = useRef<HTMLDivElement>(null)
const [canScrollLeft, setCanScrollLeft] = useState(false)
Expand Down Expand Up @@ -2374,6 +2511,7 @@ function SectionRail({
{label}
</h3>
<div className="hidden items-center gap-1.5 sm:flex">
{headerSlot}
<button
type="button"
aria-label="Show previous"
Expand Down Expand Up @@ -2524,6 +2662,11 @@ export function IntegrationsView({
[apiKeys, keyPrefix],
)

const activeMcpKeys = useMemo(
() => parseMcpAuthKeys(apiKeys, keyPrefix),
[apiKeys, keyPrefix],
)

const activePluginById = useMemo(() => {
const map = new Map<string, ConnectedKey>()
for (const key of activePlugins) {
Expand All @@ -2541,6 +2684,20 @@ export function IntegrationsView({
return map
}, [activePlugins])

const activeMcpKey = useMemo(() => {
let latest: ConnectedMcpKey | null = null
for (const key of activeMcpKeys) {
if (!latest) {
latest = key
continue
}
const a = toMs(key.lastRequest ?? key.createdAt)
const b = toMs(latest.lastRequest ?? latest.createdAt)
if (a >= b) latest = key
}
return latest
}, [activeMcpKeys])

const activeCountByPlugin = useMemo(() => {
const map = new Map<string, number>()
for (const key of activePlugins) {
Expand Down Expand Up @@ -2792,6 +2949,24 @@ export function IntegrationsView({
},
})
}
if (activeMcpKey) {
rows.push({
ts: toMs(activeMcpKey.lastRequest ?? activeMcpKey.createdAt),
entry: {
kind: "mcp",
id: "mcp",
name: "Supermemory MCP",
icon: <MCPIcon className="size-6" />,
connectionCount: activeMcpKeys.length,
createdAt: activeMcpKey.createdAt ?? null,
lastActive: activeMcpKey.lastRequest ?? null,
onManage: () => {
void setMcpClient("mcp-url")
setMcpModalOpen(true)
},
},
})
}
for (const provider of [
"google-drive",
"notion",
Expand Down Expand Up @@ -2829,11 +3004,14 @@ export function IntegrationsView({
rows.sort((a, b) => b.ts - a.ts)
return rows.map((r) => r.entry)
}, [
activeMcpKey,
activeMcpKeys.length,
activePluginById,
activeCountByPlugin,
connectionsByProvider,
allProjects,
setAddDoc,
setMcpClient,
addConnectionMutation,
])

Expand Down Expand Up @@ -2886,6 +3064,7 @@ export function IntegrationsView({
const claudeCodeConnected = activePluginById.has("claude_code")
const claudeCodeNeedsPro =
!isAutumnLoading && !hasProProduct && !isFreeTierPlugin("claude_code")
const mcpConnected = !!activeMcpKey

const featuredPicks: FeaturedPick[] = [
{
Expand Down Expand Up @@ -2939,7 +3118,7 @@ export function IntegrationsView({
/>
),
docsUrl: "https://supermemory.ai/docs/supermemory-mcp/introduction",
ctaLabel: "Connect",
ctaLabel: mcpConnected ? "Connected" : "Connect",
onCta: () => {
if (publicMode) {
redirectToLogin()
Expand Down Expand Up @@ -3234,14 +3413,21 @@ export function IntegrationsView({
}
case "mcp-client":
return (
<PillButton
<button
type="button"
aria-label={`Connect ${item.name}`}
title="Connect"
onClick={() => {
trackCard(item)
openMcpClient(item.clientKey)
}}
className={cn(
"flex size-8 shrink-0 items-center justify-center rounded-full bg-[#0D121A] text-[#A1A1AA] transition-colors hover:text-[#FAFAFA] sm:size-9",
"shadow-[inset_1.5px_1.5px_4.5px_rgba(0,0,0,0.7)]",
)}
>
Connect
</PillButton>
<Plus className="size-4" />
</button>
)
case "import":
return (
Expand Down Expand Up @@ -3485,7 +3671,18 @@ export function IntegrationsView({
)
if (items.length === 0) return null
return (
<SectionRail key={cat} label={CATEGORY_LABEL[cat]}>
<SectionRail
key={cat}
label={CATEGORY_LABEL[cat]}
headerSlot={
cat === "ai-clients" && activeMcpKey ? (
<McpConnectedPill
connectedAt={activeMcpKey.createdAt}
lastActive={activeMcpKey.lastRequest}
/>
) : null
}
>
{items.map((item) => (
<div
key={item.id}
Expand Down
Loading