feat: usability updates for the mcp page (#291)

* feat: added new mcp icons

* feat: added missing icons

* feat: change mcp text to apps

* feat: added custom app tip

* fix: airtable icon color

* feat: display unauthenticated apps in add mcp dialog

* feat: app selector for the Newtab search

* chore: update apps dropdown to use plug zap icon

* feat: setup app selector on sidepanel chat

* feat: compact apps selector dropdown

* fix: settings url in the app selector

* feat: added tooltip for app selector

* feat: added new label to the apps section
This commit is contained in:
Dani Akash
2026-02-03 18:20:33 +05:30
committed by GitHub
parent 09b71c02ce
commit a6e2845778
26 changed files with 721 additions and 48 deletions

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" aria-label="Airtable" role="img" viewBox="0 0 512 512"><rect width="512" height="512" rx="0" fill="#fff"/><g transform="translate(64 64) scale(16)"><path fill="#191d25" d="M11.992 1.966c-.434 0-.87.086-1.28.257L1.779 5.917c-.503.208-.49.908.012 1.116l8.982 3.558a3.266 3.266 0 0 0 2.454 0l8.982-3.558c.503-.196.503-.908.012-1.116l-8.957-3.694a3.255 3.255 0 0 0-1.272-.257z"/><path fill="#191d25" d="M23.4 8.056a.589.589 0 0 0-.222.045l-10.012 3.877a.612.612 0 0 0-.38.564v8.896a.6.6 0 0 0 .821.552L23.62 18.1a.583.583 0 0 0 .38-.551V8.653a.6.6 0 0 0-.6-.596z"/><path fill="#191d25" d="M.676 8.095a.644.644 0 0 0-.48.19C.086 8.396 0 8.53 0 8.69v8.355c0 .442.515.737.908.54l6.27-3.006.307-.147 2.969-1.436c.466-.22.43-.908-.061-1.092L.883 8.138a.57.57 0 0 0-.207-.044z"/></g></svg>

After

Width:  |  Height:  |  Size: 820 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" aria-label="Canva" role="img" viewBox="0 0 512 512"><rect width="512" height="512" rx="0" fill="#fff"/><g transform="translate(64 64) scale(16)"><path fill="#00C4CC" d="M12 0C5.373 0 0 5.373 0 12s5.373 12 12 12 12-5.373 12-12S18.627 0 12 0zM6.962 7.68c.754 0 1.337.549 1.405 1.2.069.583-.171 1.097-.822 1.406-.343.171-.48.172-.549.069-.034-.069 0-.137.069-.206.617-.514.617-.926.548-1.508-.034-.378-.308-.618-.583-.618-1.2 0-2.914 2.674-2.674 4.629.103.754.549 1.646 1.509 1.646.308 0 .65-.103.96-.24.5-.264.799-.47 1.097-.8-.073-.885.704-2.046 1.851-2.046.515 0 .926.205.96.583.068.514-.377.582-.514.582s-.378-.034-.378-.17c-.034-.138.309-.07.275-.378-.035-.206-.24-.274-.446-.274-.72 0-1.131.994-1.029 1.611.035.275.172.549.447.549.205 0 .514-.31.617-.755.068-.308.343-.514.583-.514.102 0 .17.034.205.171v.138c-.034.137-.137.548-.102.651 0 .069.034.171.17.171.092 0 .436-.18.777-.459.117-.59.253-1.298.253-1.357.034-.24.137-.48.617-.48.103 0 .171.034.205.171v.138l-.136.617c.445-.583 1.097-.994 1.508-.994.172 0 .309.102.309.274 0 .103 0 .274-.069.446-.137.377-.309.96-.412 1.474 0 .137.035.274.207.274.171 0 .685-.206 1.096-.754l.007-.004c-.002-.068-.007-.134-.007-.202 0-.411.035-.754.104-.994.068-.274.411-.514.617-.514.103 0 .205.069.205.171 0 .035 0 .103-.034.137-.137.446-.24.857-.24 1.269 0 .24.034.582.102.788 0 .034.035.069.07.069.068 0 .548-.445.89-1.028-.308-.206-.48-.549-.48-.96 0-.72.446-1.097.858-1.097.343 0 .617.24.617.72 0 .308-.103.65-.274.96h.102a.77.77 0 0 0 .584-.24.293.293 0 0 1 .134-.117c.335-.425.83-.74 1.41-.74.48 0 .924.205.959.582.068.515-.378.618-.515.618l-.002-.002c-.138 0-.377-.035-.377-.172 0-.137.309-.068.274-.376-.034-.206-.24-.275-.446-.275-.686 0-1.13.891-1.028 1.611.034.275.171.583.445.583.206 0 .515-.308.652-.754.068-.274.343-.514.583-.514.103 0 .17.034.205.171 0 .069 0 .206-.137.652-.17.308-.171.48-.137.617.034.274.171.48.309.583.034.034.068.102.068.102 0 .069-.034.138-.137.138-.034 0-.068 0-.103-.035-.514-.205-.72-.548-.789-.891-.205.24-.445.377-.72.377-.445 0-.89-.411-.96-.926a1.609 1.609 0 0 1 .075-.649c-.203.13-.422.203-.623.203h-.17c-.447.652-.927 1.098-1.27 1.303a.896.896 0 0 1-.377.104c-.068 0-.171-.035-.205-.104-.095-.152-.156-.392-.193-.667-.481.527-1.145.805-1.453.805-.343 0-.548-.206-.582-.55v-.376c.102-.754.377-1.2.377-1.337a.074.074 0 0 0-.069-.07c-.24 0-1.028.824-1.166 1.373l-.103.445c-.068.309-.377.515-.582.515-.103 0-.172-.035-.206-.172v-.137l.046-.233c-.435.31-.87.508-1.075.508-.308 0-.48-.172-.514-.412-.206.274-.445.412-.754.412-.352 0-.696-.24-.862-.593-.244.275-.523.553-.852.764-.48.309-1.028.549-1.68.549-.582 0-1.097-.309-1.371-.583-.412-.377-.651-.96-.686-1.509-.205-1.68.823-3.84 2.4-4.8.378-.205.755-.343 1.132-.343zm9.77 3.291c-.104 0-.172.172-.172.343 0 .274.137.583.309.755a1.74 1.74 0 0 0 .102-.583c0-.343-.137-.515-.24-.515z"/></g></svg>

After

Width:  |  Height:  |  Size: 2.8 KiB

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" aria-label="Confluence" role="img" viewBox="0 0 512 512"><rect width="512" height="512" rx="0" fill="#fff"/><g transform="translate(64 64) scale(16)"><path fill="#1868DB" d="M.87 18.257c-.248.382-.53.875-.763 1.245a.764.764 0 0 0 .255 1.04l4.965 3.054a.764.764 0 0 0 1.058-.26c.199-.332.454-.763.733-1.221 1.967-3.247 3.945-2.853 7.508-1.146l4.957 2.337a.764.764 0 0 0 1.028-.382l2.364-5.346a.764.764 0 0 0-.382-1 599.851 599.851 0 0 1-4.965-2.361C10.911 10.97 5.224 11.185.87 18.257z"/><path fill="#1868DB" d="M23.131 5.743c.249-.405.531-.875.764-1.25a.764.764 0 0 0-.256-1.034L18.675.404a.764.764 0 0 0-1.058.26c-.195.335-.451.763-.734 1.225-1.966 3.246-3.945 2.85-7.508 1.146L4.437.694a.764.764 0 0 0-1.027.382L1.046 6.422a.764.764 0 0 0 .382 1c1.039.49 3.105 1.467 4.965 2.361 6.698 3.246 12.392 3.029 16.738-4.04z"/></g></svg>

After

Width:  |  Height:  |  Size: 872 B

View File

@@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg"
aria-label="Figma" role="img"
viewBox="0 0 512 512"><path
d="M0 0H512V512H0"
fill="#fff"/><path fill="#0acf83" d="M196 436c33.12 0 60-26.88 60-60v-60H196c-33.12 0-60 26.88-60 60s26.88 60 60 60z"/><path fill="#a259ff" d="M136 256c0-33.12 26.88-60 60-60h60v120h-60c-33.12 0-60-26.88-60-60z"/><path fill="#f24e1e" d="M136 136c0-33.12 26.88-60 60-60h60v120h-60c-33.12 0-60-26.88-60-60z"/><path fill="#ff7262" d="M256 76h60c33.12 0 60 26.88 60 60s-26.88 60-60 60h-60V76z"/><path fill="#1abcfe" d="M376 256c0 33.12-26.88 60-60 60s-60-26.88-60-60 26.88-60 60-60 60 26.88 60 60z"/></svg>

After

Width:  |  Height:  |  Size: 619 B

View File

@@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg"
aria-label="GitHub" role="img"
viewBox="0 0 512 512"><path
d="m0 0H512V512H0"
fill="#181717"/><path fill="#fff" d="M335 499c-13 0-16-6-16-12l1-70c0-24-8-40-18-48 57-6 117-28 117-126 0-28-10-51-26-69 3-6 11-32-3-67 0 0-21-7-70 26-42-12-86-12-128 0-49-33-70-26-70-26-14 35-6 61-3 67-16 18-26 41-26 69 0 98 59 120 116 126-7 7-14 18-16 35-15 6-52 17-74-22 0 0-14-24-40-26 0 0-25 0-1 16 0 0 16 7 28 37 0 0 15 50 86 34l1 44c0 6-3 12-16 12-14 0-12 17-12 17H347s2-17-12-17Z"/></svg>

After

Width:  |  Height:  |  Size: 514 B

View File

@@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg"
aria-label="GitLab" role="img"
viewBox="0 0 512 512"><path
d="m0 0H512V512H0"
fill="#fff"/><path fill="#e24329" d="m71 222 52-136c5-12 21-12 26 0l35.5 109h143L363 86c5-12 21-12 26 0l52 136-185 120"/><path fill="#fca326" d="m244 442q12 10 24 0l61-46V340.8H183V396"/><path fill="#fc6d26" d="m103 336A97 97 0 0171 222q37 8 65 28l193 146 80-60a97 97 0 0032-114q-37 8-65 28L183 396"/></svg>

After

Width:  |  Height:  |  Size: 425 B

View File

@@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg"
aria-label="Gmail" role="img"
viewBox="0 0 512 512"><path
d="m0 0H512V512H0"
fill="#fff"/><path d="m76 190v171q0 30 30 30h52V190" fill="#4285f4"/><path d="m354 190v201h52q30 0 30-30V190" fill="#34a853"/><path d="m350 255V149l28-21c24-18 58 2 58 30v32" fill="#fbbc04"/><path d="m154 249V143l102 77 98-74v106l-98 74" fill="#ea4335"/><path d="m76 190v-32c0-29 34-48 58-30l24 18v106" fill="#c5221f"/></svg>

After

Width:  |  Height:  |  Size: 442 B

View File

@@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg"
aria-label="Google" role="img"
viewBox="0 0 512 512"><path
d="m0 0H512V512H0"
fill="#fff"/><path fill="#34a853" d="M153 292c30 82 118 95 171 60h62v48A192 192 0 0190 341"/><path fill="#4285f4" d="m386 400a140 175 0 0053-179H260v74h102q-7 37-38 57"/><path fill="#fbbc02" d="m90 341a208 200 0 010-171l63 49q-12 37 0 73"/><path fill="#ea4335" d="m153 219c22-69 116-109 179-50l55-54c-78-75-230-72-297 55"/></svg>

After

Width:  |  Height:  |  Size: 447 B

View File

@@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg"
aria-label="Google Calendar" role="img"
viewBox="0 0 512 512"><path
d="m0 0H512V512H0"
fill="#fff"/><path d="M100 135q0-35 37-35H340v74H174V340H100" fill="#4285f4"/><path d="m338 100v76h74v-41q0-35-35-35" fill="#1967d2"/><path d="m100 338v39q0 35 35 35h41v-74" fill="#188038"/><path d="M348 338H174v74H338" fill="#34a853"/><path d="m338 339V174h74V338" fill="#fbbc04"/><path d="M338 412v-74h74" fill="#ea4335"/><path d="m204 229a25 22 1 1125 27h-9 9a25 22 1 11-25 27m66-52 27-19h4v96" stroke="#4285f4" stroke-width="15" fill="none"/></svg>

After

Width:  |  Height:  |  Size: 579 B

View File

@@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg"
aria-label="Google Docs Editors" role="img"
viewBox="0 0 512 512"><path
d="m0 0H512V512H0"
fill="#fff"/><path d="M131.5 344h83V170h83V84h-136q-30 0-30 30" fill="#ffba00"/><path d="M297.5 84v86h83" fill="#ea4335"/><path d="M297.5 170h83v174h-83" fill="#2684fc"/><path d="M131.5 342v56q0 30 30 30h55v-86" fill="#00832d"/><path d="M214.5 342h85v86h-85" fill="#00ac47"/><path d="M297.5 342h83v56q0 30-30 30h-53" fill="#0066da"/></svg>

After

Width:  |  Height:  |  Size: 470 B

View File

@@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg"
aria-label="Google Drive" role="img"
viewBox="0 0 512 512"><path
d="m0 0H512V512H0"
fill="#fff"/><path d="m126 417q9 5 19 5H367q9 0 19-5V313H99" fill="#4285f4"/><path d="m196 87q9-5 20-5h80q12 0 20 5v104h-68" fill="#188038"/><path d="m66 313q0-10 5-19l110-191q6-11 15-16l60 104-72 125" fill="#34a853"/><path d="M316 87q10 6 15 16L438 289q8 12 8 24l-118 3-72-125" fill="#fbbc04"/><path d="m185.714 313H66q0 8 3 15l40 70q7 13 17 19" fill="#1967d2"/><path d="m386 417q9-5 16-17l38-66q6-10 6-21H326.3" fill="#ea4335"/></svg>

After

Width:  |  Height:  |  Size: 560 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" aria-label="Jira" role="img" viewBox="0 0 512 512"><rect width="512" height="512" rx="0" fill="#fff"/><g transform="translate(64 64) scale(16)"><path fill="#2684FF" d="M11.571 11.513H0a5.218 5.218 0 0 0 5.232 5.215h2.13v2.057A5.215 5.215 0 0 0 12.575 24V12.518a1.005 1.005 0 0 0-1.005-1.005zm5.723-5.756H5.736a5.215 5.215 0 0 0 5.215 5.214h2.129v2.058a5.218 5.218 0 0 0 5.215 5.214V6.758a1.001 1.001 0 0 0-1.001-1.001zM23.013 0H11.455a5.215 5.215 0 0 0 5.215 5.215h2.129v2.057A5.215 5.215 0 0 0 24 12.483V1.005A1.001 1.001 0 0 0 23.013 0Z"/></g></svg>

After

Width:  |  Height:  |  Size: 592 B

View File

@@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg"
aria-label="Linear" role="img"
viewBox="0 0 512 512"><path
d="m0 0H512V512H0"
fill="#5e6ad2"/><path fill="#fff" stroke="#fff" stroke-width="7" stroke-linejoin="round" d="m72.2 299.3s20 111 140.4 140.5zm195.4 145.2q16-1 26.6-3.6L71.3 217.8q-2 7-3.8 26.6zM83.4 179.7q4-9 10.3-19.9l258.5 258.5q-9 6-20 10.2zM381.3 397A188.6 188.6 0 10115 130.8z"/></svg>

After

Width:  |  Height:  |  Size: 390 B

View File

@@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg"
aria-label="LinkedIn" role="img"
viewBox="0 0 512 512"
fill="#fff"><path
d="m0 0H512V512H0"
fill="#0077b5"/><circle cx="142" cy="138" r="37"/><path stroke="#fff" stroke-width="66" d="M244 194v198M142 194v198"/><path d="M276 282c0-20 13-40 36-40 24 0 33 18 33 45v105h66V279c0-61-32-89-76-89-34 0-51 19-59 32"/></svg>

After

Width:  |  Height:  |  Size: 355 B

View File

@@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg"
aria-label="Notion" role="img"
viewBox="0 0 512 512"><path
d="M0 0h512v512H0"
fill="#fff"/><path d="M307.3 66.8 96.8 82.3C80.1 83.8 74 94.8 74 108.1v230.7c0 10.3 3.8 19.4 12.5 31.2l49.4 64.2c8 10.3 15.6 12.5 31.2 11.8l244.3-14.8c20.5-1.5 26.6-11 26.6-27.4V144.3c0-8.4-3.4-10.6-12.9-17.9L356 77.8c-16.4-11.8-23.2-13.3-48.7-11m-134.9 73.3c-19.8 1.5-24.3 1.5-35.7-7.6l-28.9-22.8c-3-3-1.5-6.8 6.1-7.2l202.2-14.8c17.1-1.5 25.8 4.6 32.3 9.5l34.6 25.1c1.5.8 5.3 5.3.8 5.3l-208.6 12.5h-2.7zm-23.2 261.4v-220c0-9.5 3-14.1 11.8-14.8l239.8-14.1c8-.8 11.8 4.6 11.8 14.1v218.5c0 9.5-1.5 17.9-14.8 18.6l-229.5 13.3c-13.3.8-19-3.8-19-15.6zm226.5-208.2c1.5 6.8 0 13.3-6.8 14.1l-11 2.3v162.6c-9.5 5.3-18.6 8-25.8 8-11.8 0-14.8-3.8-23.6-14.8l-72.2-113.6v109.8l22.8 5.3s0 13.3-18.6 13.3l-50.9 3c-1.5-3 0-10.3 5.3-11.8l13.3-3.8V222.2l-18.6-1.5c-1.5-6.8 2.3-16.3 12.5-17.1l54.7-3.8L332 314.9V213.1l-19-2.3c-1.5-8 4.6-14.1 11.8-14.8l50.9-3z"/></svg>

After

Width:  |  Height:  |  Size: 967 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" aria-label="Salesforce" role="img" viewBox="0 0 512 512"><rect width="512" height="512" rx="0" fill="#fff"/><g transform="translate(64 64) scale(16)"><path fill="#00A1E0" d="M10.006 5.415a4.195 4.195 0 013.045-1.306c1.56 0 2.954.9 3.69 2.205.63-.3 1.35-.45 2.1-.45 2.85 0 5.159 2.34 5.159 5.22s-2.31 5.22-5.176 5.22c-.345 0-.69-.044-1.02-.104a3.75 3.75 0 01-3.3 1.95c-.6 0-1.155-.15-1.65-.375A4.314 4.314 0 018.88 20.4a4.302 4.302 0 01-4.05-2.82c-.27.062-.54.076-.825.076-2.204 0-4.005-1.8-4.005-4.05 0-1.5.811-2.805 2.01-3.51-.255-.57-.39-1.2-.39-1.846 0-2.58 2.1-4.65 4.65-4.65 1.53 0 2.85.705 3.72 1.8"/></g></svg>

After

Width:  |  Height:  |  Size: 658 B

View File

@@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg"
aria-label="Slack" role="img"
viewBox="0 0 512 512"
stroke-width="78" stroke-linecap="round"><path
d="m0 0H512V512H0"
fill="#fff"/><path stroke="#36c5f0" d="m110 207h97m0-97h.1v-.1"/><path stroke="#2eb67d" d="m305 110v97m97 0v.1h.1"/><path stroke="#ecb22e" d="m402 305h-97m0 97h-.1v.1"/><path stroke="#e01e5a" d="M110 305h.1v.1m97 0v97"/></svg>

After

Width:  |  Height:  |  Size: 384 B

View File

@@ -0,0 +1,259 @@
import { KeyRound, Plus, Settings } from 'lucide-react'
import type { FC, ReactNode } from 'react'
import { useState } from 'react'
import { toast } from 'sonner'
import {
Command,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem,
CommandList,
} from '@/components/ui/command'
import {
Popover,
PopoverContent,
PopoverTrigger,
} from '@/components/ui/popover'
import { McpServerIcon } from '@/entrypoints/app/connect-mcp/McpServerIcon'
import { useAddManagedServer } from '@/entrypoints/app/connect-mcp/useAddManagedServer'
import { useGetMCPServersList } from '@/entrypoints/app/connect-mcp/useGetMCPServersList'
import { useGetUserMCPIntegrations } from '@/entrypoints/app/connect-mcp/useGetUserMCPIntegrations'
import { MANAGED_MCP_ADDED_EVENT } from '@/lib/constants/analyticsEvents'
import { useMcpServers } from '@/lib/mcp/mcpServerStorage'
import { track } from '@/lib/metrics/track'
import { sentry } from '@/lib/sentry/sentry'
interface AppSelectorProps {
children: ReactNode
side?: 'top' | 'bottom' | 'left' | 'right'
}
export const AppSelector: FC<AppSelectorProps> = ({
children,
side = 'bottom',
}) => {
const [open, setOpen] = useState(false)
const [filterText, setFilterText] = useState('')
const { servers: createdServers, addServer } = useMcpServers()
const { trigger: addManagedServerMutation } = useAddManagedServer()
const { data: serversList } = useGetMCPServersList()
const { data: userMCPIntegrations, isLoading: isIntegrationsLoading } =
useGetUserMCPIntegrations()
const query = filterText.toLowerCase()
const connectedServers = createdServers.filter((s) => {
if (s.type !== 'managed' || !s.managedServerName) return false
const integration = userMCPIntegrations?.integrations?.find(
(i) => i.name === s.managedServerName,
)
return integration?.is_authenticated === true
})
const unauthenticatedServers = createdServers.filter((s) => {
if (s.type !== 'managed' || !s.managedServerName) return false
if (isIntegrationsLoading) return false
const integration = userMCPIntegrations?.integrations?.find(
(i) => i.name === s.managedServerName,
)
return !integration?.is_authenticated
})
const availableServers =
serversList?.servers.filter((s) => {
return !createdServers.find(
(created) => created.managedServerName === s.name,
)
}) ?? []
const filteredConnected = connectedServers.filter(
(s) =>
s.displayName.toLowerCase().includes(query) ||
s.managedServerDescription?.toLowerCase().includes(query),
)
const filteredUnauthenticated = unauthenticatedServers.filter(
(s) =>
s.displayName.toLowerCase().includes(query) ||
s.managedServerDescription?.toLowerCase().includes(query),
)
const filteredAvailable = availableServers.filter(
(s) =>
s.name.toLowerCase().includes(query) ||
s.description.toLowerCase().includes(query),
)
const hasResults =
filteredConnected.length > 0 ||
filteredUnauthenticated.length > 0 ||
filteredAvailable.length > 0
const openAuthUrl = async (serverName: string) => {
try {
const response = await addManagedServerMutation({ serverName })
if (!response.oauthUrl) {
toast.error(`Failed to add app: ${serverName}`)
return
}
window.open(response.oauthUrl, '_blank')?.focus()
} catch (e) {
toast.error(`Failed to add app: ${serverName}`)
sentry.captureException(e)
}
}
const handleAddServer = async (name: string, description: string) => {
try {
const response = await addManagedServerMutation({ serverName: name })
if (!response.oauthUrl) {
toast.error(`Failed to add app: ${name}`)
return
}
window.open(response.oauthUrl, '_blank')?.focus()
addServer({
id: Date.now().toString(),
displayName: name,
type: 'managed',
managedServerName: name,
managedServerDescription: description,
})
track(MANAGED_MCP_ADDED_EVENT, { server_name: name })
} catch (e) {
toast.error(`Failed to add app: ${name}`)
sentry.captureException(e)
}
}
return (
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild>{children}</PopoverTrigger>
<PopoverContent
side={side}
align="end"
className="w-72 p-0"
role="dialog"
aria-label="Connect apps"
>
<Command
className="[&_svg:not([class*='text-'])]:text-muted-foreground"
shouldFilter={false}
>
<CommandInput
placeholder="Search apps..."
className="h-9"
value={filterText}
onValueChange={setFilterText}
/>
<CommandList className="max-h-64 overflow-auto">
<CommandEmpty>No apps found</CommandEmpty>
{filteredConnected.length > 0 && (
<CommandGroup>
<div className="my-2 px-2 font-semibold text-muted-foreground text-xs uppercase tracking-wide">
Connected
</div>
<div className="flex flex-wrap items-center gap-2 px-3 py-2">
{filteredConnected.map((server) => (
<div
key={server.id}
title={server.displayName}
className="flex h-8 w-8 items-center justify-center rounded-lg border border-border bg-accent/50"
>
<McpServerIcon
serverName={server.managedServerName ?? ''}
size={18}
/>
</div>
))}
<button
type="button"
onClick={() => {
const appUrl = chrome.runtime.getURL(
'/app.html#/settings/connect-mcp',
)
window.open(appUrl, '_blank')
setOpen(false)
}}
title="Manage apps"
className="flex h-8 w-8 cursor-pointer items-center justify-center rounded-lg border border-border border-dashed text-muted-foreground transition-colors hover:bg-accent hover:text-foreground"
>
<Settings className="h-3.5 w-3.5" />
</button>
</div>
</CommandGroup>
)}
{filteredUnauthenticated.length > 0 && (
<CommandGroup>
<div className="my-2 px-2 font-semibold text-muted-foreground text-xs uppercase tracking-wide">
Needs authentication
</div>
{filteredUnauthenticated.map((server) => (
<CommandItem
key={server.id}
value={`${server.id} ${server.displayName}`}
onSelect={() =>
server.managedServerName &&
openAuthUrl(server.managedServerName)
}
className="flex cursor-pointer items-center gap-3 px-3 py-2"
>
<McpServerIcon
serverName={server.managedServerName ?? ''}
size={18}
className="shrink-0"
/>
<span className="flex-1 truncate text-sm">
{server.displayName}
</span>
<KeyRound className="h-3.5 w-3.5 shrink-0 text-amber-500" />
</CommandItem>
))}
</CommandGroup>
)}
{filteredAvailable.length > 0 && (
<CommandGroup>
<div className="my-2 px-2 font-semibold text-muted-foreground text-xs uppercase tracking-wide">
Available
</div>
{filteredAvailable.map((server) => (
<CommandItem
key={server.name}
value={`${server.name} ${server.description}`}
onSelect={() =>
handleAddServer(server.name, server.description)
}
className="flex cursor-pointer items-center gap-3 px-3 py-2"
>
<McpServerIcon
serverName={server.name}
size={18}
className="shrink-0"
/>
<div className="min-w-0 flex-1">
<span className="block truncate text-sm">
{server.name}
</span>
{server.description && (
<span className="block truncate text-muted-foreground text-xs">
{server.description}
</span>
)}
</div>
<Plus className="h-3.5 w-3.5 shrink-0 text-muted-foreground" />
</CommandItem>
))}
</CommandGroup>
)}
{!hasResults && filterText && null}
</CommandList>
</Command>
</PopoverContent>
</Popover>
)
}

View File

@@ -27,7 +27,7 @@ const settingsNavItems: NavItem[] = [
{ name: 'BrowserOS AI', to: '/settings/ai', icon: Bot },
{ name: 'LLM Chat & Hub', to: '/settings/chat', icon: MessageSquare },
{
name: 'Connect to MCPs',
name: 'Connect Apps',
to: '/settings/connect-mcp',
icon: PlugZap,
feature: Feature.MANAGED_MCP_SUPPORT,

View File

@@ -1,8 +1,14 @@
import { zodResolver } from '@hookform/resolvers/zod'
import { ChevronRight, Lightbulb } from 'lucide-react'
import type { FC } from 'react'
import { useForm } from 'react-hook-form'
import { z } from 'zod/v3'
import { Button } from '@/components/ui/button'
import {
Collapsible,
CollapsibleContent,
CollapsibleTrigger,
} from '@/components/ui/collapsible'
import {
Dialog,
DialogContent,
@@ -76,9 +82,9 @@ export const AddCustomMCPDialog: FC<AddCustomMCPDialogProps> = ({
<Dialog open={open} onOpenChange={handleOpenChange}>
<DialogContent className="max-w-md">
<DialogHeader>
<DialogTitle>Add Custom MCP Server</DialogTitle>
<DialogTitle>Add Custom App</DialogTitle>
<DialogDescription>
Configure your custom MCP server connection
Configure your custom app connection
</DialogDescription>
</DialogHeader>
@@ -91,7 +97,7 @@ export const AddCustomMCPDialog: FC<AddCustomMCPDialogProps> = ({
<FormItem>
<FormLabel>Server Name</FormLabel>
<FormControl>
<Input placeholder="My Custom MCP" {...field} />
<Input placeholder="My Custom App" {...field} />
</FormControl>
<FormMessage />
</FormItem>
@@ -103,7 +109,7 @@ export const AddCustomMCPDialog: FC<AddCustomMCPDialogProps> = ({
name="url"
render={({ field }) => (
<FormItem>
<FormLabel>Server URL</FormLabel>
<FormLabel>MCP Server URL</FormLabel>
<FormDescription>(only supports HTTP)</FormDescription>
<FormControl>
<Input
@@ -136,6 +142,25 @@ export const AddCustomMCPDialog: FC<AddCustomMCPDialogProps> = ({
)}
/>
<Collapsible>
<CollapsibleTrigger className="group flex w-full cursor-pointer items-center gap-2 rounded-md border border-[var(--accent-orange)]/30 bg-[var(--accent-orange)]/5 px-3 py-2 text-left text-sm transition-colors hover:bg-[var(--accent-orange)]/10">
<Lightbulb className="h-4 w-4 shrink-0 text-[var(--accent-orange)]" />
<span className="flex-1 font-medium">
How do I find the URL?
</span>
<ChevronRight className="h-4 w-4 shrink-0 text-muted-foreground transition-transform duration-200 group-data-[state=open]:rotate-90" />
</CollapsibleTrigger>
<CollapsibleContent className="mt-2 rounded-md border border-[var(--accent-orange)]/30 bg-[var(--accent-orange)]/5 px-3 py-2 text-muted-foreground text-sm">
Many apps like Notion, Slack, or Stripe offer an MCP server you
can run locally. Check the app's docs for an MCP setup guide —
you'll get a URL (usually starting with{' '}
<code className="inline rounded bg-muted px-1 text-xs">
http://
</code>
) to paste here.
</CollapsibleContent>
</Collapsible>
<DialogFooter>
<Button
type="button"

View File

@@ -1,5 +1,5 @@
import { Plus, Server } from 'lucide-react'
import type { FC } from 'react'
import { KeyRound, Plus, Search } from 'lucide-react'
import { type FC, useState } from 'react'
import { Button } from '@/components/ui/button'
import {
Dialog,
@@ -8,58 +8,151 @@ import {
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog'
import { Input } from '@/components/ui/input'
import { McpServerIcon } from './McpServerIcon'
interface UnauthenticatedServer {
name: string
description: string
}
interface AddManagedMCPDialogProps {
open: boolean
onOpenChange: (open: boolean) => void
serversList?: { name: string; description: string }[]
unauthenticatedServers: UnauthenticatedServer[]
onAddServer: (args: { name: string; description: string }) => void
onAuthenticate: (serverName: string) => void
}
export const AddManagedMCPDialog: FC<AddManagedMCPDialogProps> = ({
open,
onOpenChange,
serversList,
unauthenticatedServers,
onAddServer,
onAuthenticate,
}) => {
const [search, setSearch] = useState('')
const handleAddServer = (args: { name: string; description: string }) => {
onAddServer(args)
onOpenChange(false)
}
const handleOpenChange = (isOpen: boolean) => {
if (!isOpen) setSearch('')
onOpenChange(isOpen)
}
const query = search.toLowerCase()
const filteredUnauthenticated = unauthenticatedServers.filter(
(s) =>
s.name.toLowerCase().includes(query) ||
s.description.toLowerCase().includes(query),
)
const filteredAvailable = serversList?.filter(
(s) =>
s.name.toLowerCase().includes(query) ||
s.description.toLowerCase().includes(query),
)
const hasResults =
filteredUnauthenticated.length > 0 || (filteredAvailable?.length ?? 0) > 0
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<Dialog open={open} onOpenChange={handleOpenChange}>
<DialogContent className="max-w-md">
<DialogHeader>
<DialogTitle>Enable built-in MCP Server</DialogTitle>
<DialogTitle>Add built-in app</DialogTitle>
<DialogDescription>
Select a built-in MCP server to enable
Select a built-in app to connect
</DialogDescription>
</DialogHeader>
<div className="styled-scrollbar max-h-[400px] w-full space-y-2 overflow-y-auto">
{serversList?.map((args) => {
const { name: serverName, description } = args
return (
<Button
key={serverName}
variant="outline"
onClick={() => handleAddServer(args)}
className="group h-auto w-full items-center gap-3 p-3 hover:border-[var(--accent-orange)] hover:bg-[var(--accent-orange)]/5"
>
<Server className="h-5 w-5 shrink-0 text-muted-foreground" />
<div className="flex w-[calc(100%-64px)] flex-1 flex-col items-start gap-1">
<span className="font-medium">{serverName}</span>
{description && (
<p className="line-clamp-1 w-full text-ellipsis text-left text-muted-foreground text-xs">
{description}
</p>
)}
</div>
<Plus className="h-4 w-4 shrink-0 text-muted-foreground" />
</Button>
)
}) ?? null}
<div className="relative">
<Search className="absolute top-1/2 left-3 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
<Input
placeholder="Search apps..."
value={search}
onChange={(e) => setSearch(e.target.value)}
className="pl-9"
/>
</div>
<div className="styled-scrollbar max-h-[400px] w-full space-y-4 overflow-y-auto">
{filteredUnauthenticated.length > 0 && (
<div className="space-y-2">
<p className="px-1 font-medium text-muted-foreground text-xs uppercase tracking-wider">
Needs authentication
</p>
{filteredUnauthenticated.map((server) => (
<Button
key={server.name}
variant="outline"
onClick={() => onAuthenticate(server.name)}
className="group h-auto w-full items-center gap-3 border-amber-500/30 bg-amber-500/5 p-3 hover:border-amber-500 hover:bg-amber-500/10"
>
<McpServerIcon
serverName={server.name}
size={20}
className="shrink-0 text-muted-foreground"
/>
<div className="flex w-[calc(100%-64px)] flex-1 flex-col items-start gap-1">
<span className="font-medium">{server.name}</span>
{server.description && (
<p className="line-clamp-1 w-full text-ellipsis text-left text-muted-foreground text-xs">
{server.description}
</p>
)}
</div>
<KeyRound className="h-4 w-4 shrink-0 text-amber-500" />
</Button>
))}
</div>
)}
{(filteredAvailable?.length ?? 0) > 0 && (
<div className="space-y-2">
{filteredUnauthenticated.length > 0 && (
<p className="px-1 font-medium text-muted-foreground text-xs uppercase tracking-wider">
Available
</p>
)}
{filteredAvailable?.map((args) => {
const { name: serverName, description } = args
return (
<Button
key={serverName}
variant="outline"
onClick={() => handleAddServer(args)}
className="group h-auto w-full items-center gap-3 p-3 hover:border-[var(--accent-orange)] hover:bg-[var(--accent-orange)]/5"
>
<McpServerIcon
serverName={serverName}
size={20}
className="shrink-0 text-muted-foreground"
/>
<div className="flex w-[calc(100%-64px)] flex-1 flex-col items-start gap-1">
<span className="font-medium">{serverName}</span>
{description && (
<p className="line-clamp-1 w-full text-ellipsis text-left text-muted-foreground text-xs">
{description}
</p>
)}
</div>
<Plus className="h-4 w-4 shrink-0 text-muted-foreground" />
</Button>
)
})}
</div>
)}
{!hasResults && (
<p className="py-6 text-center text-muted-foreground text-sm">
No apps found
</p>
)}
</div>
</DialogContent>
</Dialog>

View File

@@ -1,4 +1,4 @@
import { ChevronDown, Loader2, Plus, Server } from 'lucide-react'
import { ChevronDown, Loader2, Plus } from 'lucide-react'
import { type FC, useState } from 'react'
import { Button } from '@/components/ui/button'
import {
@@ -6,6 +6,7 @@ import {
CollapsibleContent,
CollapsibleTrigger,
} from '@/components/ui/collapsible'
import { McpServerIcon } from './McpServerIcon'
interface AvailableManagedServersProps {
availableServers?: { name: string; description: string }[]
@@ -18,18 +19,18 @@ export const AvailableManagedServers: FC<AvailableManagedServersProps> = ({
onAddServer,
isLoading,
}) => {
const [isOpen, setIsOpen] = useState(false)
const [isOpen, setIsOpen] = useState(true)
return (
<Collapsible open={isOpen} onOpenChange={setIsOpen}>
<div className="rounded-xl border border-border bg-card p-6 shadow-sm transition-all hover:shadow-md">
<CollapsibleTrigger className="flex w-full cursor-pointer items-center justify-between hover:opacity-50">
<div className="flex flex-col items-start">
<h3 className="font-semibold text-lg">Built-in Servers</h3>
<h3 className="font-semibold text-lg">Built-in Apps</h3>
<p className="text-muted-foreground text-sm">
{isLoading
? 'Loading...'
: `${availableServers?.length} servers available`}
: `${availableServers?.length} apps available`}
</p>
</div>
<ChevronDown
@@ -57,7 +58,11 @@ export const AvailableManagedServers: FC<AvailableManagedServersProps> = ({
>
<div className="flex w-full items-center justify-between">
<div className="flex items-center gap-3">
<Server className="h-5 w-5 text-muted-foreground transition-colors group-hover:text-[var(--accent-orange)]" />
<McpServerIcon
serverName={serverName}
size={20}
className="text-muted-foreground transition-colors group-hover:text-[var(--accent-orange)]"
/>
<span className="font-medium">{serverName}</span>
</div>
<Plus className="h-4 w-4 text-muted-foreground transition-colors group-hover:text-[var(--accent-orange)]" />

View File

@@ -12,18 +12,19 @@ import { sentry } from '@/lib/sentry/sentry'
import { AddCustomMCPDialog } from './AddCustomMCPDialog'
import { AddManagedMCPDialog } from './AddManagedMCPDialog'
import { AvailableManagedServers } from './AvailableManagedServers'
import { McpServerIcon } from './McpServerIcon'
import { useAddManagedServer } from './useAddManagedServer'
import { useGetMCPServersList } from './useGetMCPServersList'
import { useGetUserMCPIntegrations } from './useGetUserMCPIntegrations'
import { useRemoveManagedServer } from './useRemoveManagedServer'
const failedToAddMcp = (serverName: string, e: unknown) => {
toast.error(`Failed to add MCP Server: ${serverName}`)
toast.error(`Failed to add app: ${serverName}`)
sentry.captureException(e)
}
const failedToRemoveMcp = (serverName: string, e: unknown) => {
toast.error(`Failed to remove MCP Server: ${serverName}`)
toast.error(`Failed to remove app: ${serverName}`)
sentry.captureException(e)
}
@@ -82,7 +83,7 @@ export const ConnectMCP: FC = () => {
addServer({
id: Date.now().toString(),
displayName: `${name} MCP`,
displayName: name,
type: 'managed',
managedServerName: name,
managedServerDescription: description,
@@ -144,6 +145,22 @@ export const ConnectMCP: FC = () => {
return true
})
const unauthenticatedServers: { name: string; description: string }[] = []
if (!isUserMCPIntegrationsLoading) {
for (const server of createdServers) {
if (server.type !== 'managed' || !server.managedServerName) continue
const integration = userMCPIntegrations?.integrations?.find(
(i) => i.name === server.managedServerName,
)
if (!integration?.is_authenticated) {
unauthenticatedServers.push({
name: server.managedServerName,
description: server.managedServerDescription ?? '',
})
}
}
}
return (
<div className="fade-in slide-in-from-bottom-5 animate-in space-y-6 duration-500">
{/* Header */}
@@ -153,9 +170,9 @@ export const ConnectMCP: FC = () => {
<Server className="h-6 w-6 text-[var(--accent-orange)]" />
</div>
<div className="flex-1">
<h2 className="mb-1 font-semibold text-xl">MCP Servers</h2>
<h2 className="mb-1 font-semibold text-xl">Connected Apps</h2>
<p className="mb-6 text-muted-foreground text-sm">
Connect BrowserOS assistant to MCP servers to send email, schedule
Connect BrowserOS assistant to apps to send email, schedule
calendar events, write docs, and more
</p>
@@ -166,7 +183,7 @@ export const ConnectMCP: FC = () => {
className="border-[var(--accent-orange)] bg-[var(--accent-orange)]/10 text-[var(--accent-orange)] hover:bg-[var(--accent-orange)]/20"
>
<Plus className="h-4 w-4" />
<span>Enable built-in MCP</span>
<span>Add built-in app</span>
</Button>
<Button
@@ -174,7 +191,7 @@ export const ConnectMCP: FC = () => {
onClick={() => setAddingCustomMcp(true)}
>
<Plus className="h-4 w-4" />
<span>Add Custom MCP</span>
<span>Add custom app</span>
</Button>
</div>
</div>
@@ -184,7 +201,7 @@ export const ConnectMCP: FC = () => {
{/* Created Servers */}
{createdServers.length > 0 && (
<div className="rounded-xl border border-border bg-card p-6 shadow-sm transition-all hover:shadow-md">
<h3 className="mb-4 font-semibold text-lg">Your MCP Servers</h3>
<h3 className="mb-4 font-semibold text-lg">Your Connected Apps</h3>
<div className="space-y-3">
{createdServers.map((server) => (
<div
@@ -192,7 +209,11 @@ export const ConnectMCP: FC = () => {
className="flex items-center gap-4 rounded-lg border border-border bg-background p-4 transition-all hover:border-[var(--accent-orange)]/50 hover:shadow-sm"
>
<div className="flex h-10 w-10 shrink-0 items-center justify-center rounded-lg bg-[var(--accent-orange)]/10">
<Server className="h-5 w-5 text-[var(--accent-orange)]" />
<McpServerIcon
serverName={server.managedServerName ?? ''}
size={20}
className="text-[var(--accent-orange)]"
/>
</div>
<div className="flex-1">
<div className="mb-1 flex items-center gap-2">
@@ -274,7 +295,9 @@ export const ConnectMCP: FC = () => {
open={addingManagedMcp}
onOpenChange={setAddingManagedMcp}
serversList={availableServers}
unauthenticatedServers={unauthenticatedServers}
onAddServer={addManagedServer}
onAuthenticate={openAuthUrlForMCP}
/>
<AddCustomMCPDialog

View File

@@ -0,0 +1,67 @@
import { Server } from 'lucide-react'
import type { FC } from 'react'
import AirtableSvg from '@/assets/mcp-icons/airtable.svg'
import CanvaSvg from '@/assets/mcp-icons/canva.svg'
import ConfluenceSvg from '@/assets/mcp-icons/confluence.svg'
import FigmaSvg from '@/assets/mcp-icons/figma.svg'
import GithubSvg from '@/assets/mcp-icons/github.svg'
import GitlabSvg from '@/assets/mcp-icons/gitlab.svg'
import GmailSvg from '@/assets/mcp-icons/gmail.svg'
import GoogleSvg from '@/assets/mcp-icons/google.svg'
import GoogleCalendarSvg from '@/assets/mcp-icons/google_calendar.svg'
import GoogleDocsSvg from '@/assets/mcp-icons/google_docs_editors.svg'
import GoogleDriveSvg from '@/assets/mcp-icons/google_drive.svg'
import JiraSvg from '@/assets/mcp-icons/jira.svg'
import LinearSvg from '@/assets/mcp-icons/linear.svg'
import LinkedinSvg from '@/assets/mcp-icons/linkedin.svg'
import NotionSvg from '@/assets/mcp-icons/notion.svg'
import SalesforceSvg from '@/assets/mcp-icons/salesforce.svg'
import SlackSvg from '@/assets/mcp-icons/slack.svg'
const mcpIconMap: Record<string, string> = {
Gmail: GmailSvg,
'Google Calendar': GoogleCalendarSvg,
'Google Docs': GoogleDocsSvg,
'Google Drive': GoogleDriveSvg,
'Google Sheets': GoogleSvg,
Slack: SlackSvg,
LinkedIn: LinkedinSvg,
Notion: NotionSvg,
Airtable: AirtableSvg,
Confluence: ConfluenceSvg,
GitHub: GithubSvg,
GitLab: GitlabSvg,
Linear: LinearSvg,
Jira: JiraSvg,
Figma: FigmaSvg,
Canva: CanvaSvg,
Salesforce: SalesforceSvg,
}
interface McpServerIconProps {
serverName: string
size?: number
className?: string
}
export const McpServerIcon: FC<McpServerIconProps> = ({
serverName,
size = 20,
className,
}) => {
const iconSrc = mcpIconMap[serverName]
if (iconSrc) {
return (
<img
src={iconSrc}
alt={serverName}
width={size}
height={size}
className={`rounded-md ${className ?? ''}`}
/>
)
}
return <Server size={size} className={className} />
}

View File

@@ -5,11 +5,13 @@ import {
Folder,
Globe,
Layers,
PlugZap,
Search,
X,
} from 'lucide-react'
import { AnimatePresence, motion } from 'motion/react'
import { useEffect, useRef, useState } from 'react'
import { AppSelector } from '@/components/elements/AppSelector'
import {
GlowingBorder,
GlowingElement,
@@ -17,6 +19,13 @@ import {
import { TabSelector } from '@/components/elements/tab-selector'
import { WorkspaceSelector } from '@/components/elements/workspace-selector'
import { Button } from '@/components/ui/button'
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from '@/components/ui/tooltip'
import { McpServerIcon } from '@/entrypoints/app/connect-mcp/McpServerIcon'
import { useGetUserMCPIntegrations } from '@/entrypoints/app/connect-mcp/useGetUserMCPIntegrations'
import { Feature } from '@/lib/browseros/capabilities'
import { useCapabilities } from '@/lib/browseros/useCapabilities'
import {
@@ -28,6 +37,7 @@ import {
NEWTAB_OPENED_EVENT,
NEWTAB_SEARCH_EXECUTED_EVENT,
} from '@/lib/constants/analyticsEvents'
import { useMcpServers } from '@/lib/mcp/mcpServerStorage'
import { openSidePanelWithSearch } from '@/lib/messaging/sidepanel/openSidepanelWithSearch'
import { track } from '@/lib/metrics/track'
import { cn } from '@/lib/utils'
@@ -56,6 +66,15 @@ export const NewTab = () => {
const [shortcutsDialogOpen, setShortcutsDialogOpen] = useState(false)
const { selectedFolder } = useWorkspace()
const { supports } = useCapabilities()
const { servers: mcpServers } = useMcpServers()
const { data: userMCPIntegrations } = useGetUserMCPIntegrations()
const connectedManagedServers = mcpServers.filter((s) => {
if (s.type !== 'managed' || !s.managedServerName) return false
return userMCPIntegrations?.integrations?.find(
(i) => i.name === s.managedServerName,
)?.is_authenticated
})
const toggleTab = (tab: chrome.tabs.Tab) => {
setSelectedTabs((prev) => {
@@ -352,6 +371,72 @@ export const NewTab = () => {
</TabSelector>
</div>
</div>
{supports(Feature.MANAGED_MCP_SUPPORT) && (
<div className="ml-auto flex items-center gap-1.5">
{connectedManagedServers.length === 0 && (
<span className="flex items-center gap-1 font-semibold text-[var(--accent-orange)] text-sm">
New! 👉
</span>
)}
{connectedManagedServers.length === 0 ? (
<Tooltip>
<AppSelector side="bottom">
<TooltipTrigger asChild>
<Button
variant="ghost"
className={cn(
'flex items-center gap-2 rounded-lg px-3 py-1.5 font-medium text-sm transition-all',
'bg-transparent text-muted-foreground hover:bg-accent hover:text-accent-foreground',
'data-[state=open]:bg-accent',
)}
>
<PlugZap className="h-4 w-4" />
<span>Apps</span>
<ChevronDown className="h-3 w-3" />
</Button>
</TooltipTrigger>
</AppSelector>
<TooltipContent side="left" className="max-w-56">
Apps directly connected will have more accurate and
faster responses for your queries!
</TooltipContent>
</Tooltip>
) : (
<AppSelector side="bottom">
<Button
variant="ghost"
className={cn(
'flex items-center gap-2 rounded-lg px-3 py-1.5 font-medium text-sm transition-all',
'bg-transparent text-muted-foreground hover:bg-accent hover:text-accent-foreground',
'data-[state=open]:bg-accent',
)}
>
<div className="flex items-center -space-x-1.5">
{connectedManagedServers.slice(0, 4).map((s) => (
<div
key={s.id}
className="rounded-full ring-2 ring-card"
>
<McpServerIcon
serverName={s.managedServerName ?? ''}
size={16}
/>
</div>
))}
</div>
{connectedManagedServers.length > 4 && (
<span className="text-xs">
+{connectedManagedServers.length - 4}
</span>
)}
<span>Apps</span>
<ChevronDown className="h-3 w-3" />
</Button>
</AppSelector>
)}
</div>
)}
</div>
)}
</div>

View File

@@ -1,9 +1,13 @@
import { ChevronDown, Folder, Layers } from 'lucide-react'
import { ChevronDown, Folder, Layers, PlugZap } from 'lucide-react'
import type { FC, FormEvent } from 'react'
import { AppSelector } from '@/components/elements/AppSelector'
import { TabSelector } from '@/components/elements/tab-selector'
import { WorkspaceSelector } from '@/components/elements/workspace-selector'
import { McpServerIcon } from '@/entrypoints/app/connect-mcp/McpServerIcon'
import { useGetUserMCPIntegrations } from '@/entrypoints/app/connect-mcp/useGetUserMCPIntegrations'
import { Feature } from '@/lib/browseros/capabilities'
import { useCapabilities } from '@/lib/browseros/useCapabilities'
import { useMcpServers } from '@/lib/mcp/mcpServerStorage'
import { cn } from '@/lib/utils'
import { useWorkspace } from '@/lib/workspace/use-workspace'
import { ChatAttachedTabs } from './ChatAttachedTabs'
@@ -38,6 +42,15 @@ export const ChatFooter: FC<ChatFooterProps> = ({
}) => {
const { selectedFolder } = useWorkspace()
const { supports } = useCapabilities()
const { servers: mcpServers } = useMcpServers()
const { data: userMCPIntegrations } = useGetUserMCPIntegrations()
const connectedManagedServers = mcpServers.filter((s) => {
if (s.type !== 'managed' || !s.managedServerName) return false
return userMCPIntegrations?.integrations?.find(
(i) => i.name === s.managedServerName,
)?.is_authenticated
})
return (
<footer className="border-border/40 border-t bg-background/80 backdrop-blur-md">
@@ -96,6 +109,42 @@ export const ChatFooter: FC<ChatFooterProps> = ({
</button>
</WorkspaceSelector>
)}
{supports(Feature.MANAGED_MCP_SUPPORT) && (
<AppSelector side="top">
<button
type="button"
className="flex cursor-pointer items-center gap-1 rounded-lg p-1.5 text-muted-foreground transition-colors hover:bg-muted/50 hover:text-foreground data-[state=open]:bg-accent"
title="Connect apps"
>
{connectedManagedServers.length > 0 ? (
<>
<div className="flex items-center -space-x-1">
{connectedManagedServers.slice(0, 3).map((s) => (
<div
key={s.id}
className="rounded-full ring-2 ring-background"
>
<McpServerIcon
serverName={s.managedServerName ?? ''}
size={14}
/>
</div>
))}
</div>
{connectedManagedServers.length > 3 && (
<span className="font-medium text-xs">
+{connectedManagedServers.length - 3}
</span>
)}
</>
) : (
<PlugZap className="h-4 w-4" />
)}
<ChevronDown className="h-3 w-3" />
</button>
</AppSelector>
)}
</div>
</div>