diff --git a/apps/web/components/integrations-view.tsx b/apps/web/components/integrations-view.tsx index 706be0ad2..9263bbe55 100644 --- a/apps/web/components/integrations-view.tsx +++ b/apps/web/components/integrations-view.tsx @@ -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) @@ -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 @@ -1036,6 +1077,31 @@ function ActiveButton({ ) } +function McpConnectedPill({ + connectedAt, + lastActive, +}: { + connectedAt?: string | null + lastActive?: string | null +}) { + return ( + + + Connected + {(lastActive ?? connectedAt) && ( + + · {formatRelativeTime(lastActive ?? connectedAt)} + + )} + + ) +} + function FinishSetupButton({ onClick }: { onClick: () => void }) { return ( @@ -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 | undefined @@ -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 ( + setExpanded((v) => !v)} + statusLine={ +
+ + {suffix && ( + + · {suffix} + + )} +
+ } + > + {entry.createdAt && ( + + )} + {entry.lastActive && ( + + )} + +
+ +
+
+ ) +} + const SKELETON_KEYS = ["s1", "s2", "s3", "s4", "s5"] function RailSkeleton({ rows }: { rows: number }) { @@ -1521,6 +1652,8 @@ function ActiveConnectionsRail({ {entries.map((entry) => entry.kind === "plugin" ? ( + ) : entry.kind === "mcp" ? ( + ) : ( ), @@ -1879,6 +2012,8 @@ function MobileActivityPanel({ {entries.map((entry) => entry.kind === "plugin" ? ( + ) : entry.kind === "mcp" ? ( + ) : ( ), @@ -2321,9 +2456,11 @@ function CategoryFilterToggle({ function SectionRail({ label, children, + headerSlot, }: { label: string children: ReactNode + headerSlot?: ReactNode }) { const scrollRef = useRef(null) const [canScrollLeft, setCanScrollLeft] = useState(false) @@ -2374,6 +2511,7 @@ function SectionRail({ {label}
+ {headerSlot} ) case "import": return ( @@ -3574,7 +3760,18 @@ export function IntegrationsView({ ) if (items.length === 0) return null return ( - + + ) : null + } + > {items.map((item) => (