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
1
apps/agent/assets/mcp-icons/airtable.svg
Normal 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 |
1
apps/agent/assets/mcp-icons/canva.svg
Normal 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 |
1
apps/agent/assets/mcp-icons/confluence.svg
Normal 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 |
5
apps/agent/assets/mcp-icons/figma.svg
Normal 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 |
5
apps/agent/assets/mcp-icons/github.svg
Normal 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 |
5
apps/agent/assets/mcp-icons/gitlab.svg
Normal 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 |
5
apps/agent/assets/mcp-icons/gmail.svg
Normal 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 |
5
apps/agent/assets/mcp-icons/google.svg
Normal 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 |
5
apps/agent/assets/mcp-icons/google_calendar.svg
Normal 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 |
5
apps/agent/assets/mcp-icons/google_docs_editors.svg
Normal 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 |
5
apps/agent/assets/mcp-icons/google_drive.svg
Normal 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 |
1
apps/agent/assets/mcp-icons/jira.svg
Normal 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 |
5
apps/agent/assets/mcp-icons/linear.svg
Normal 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 |
6
apps/agent/assets/mcp-icons/linkedin.svg
Normal 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 |
5
apps/agent/assets/mcp-icons/notion.svg
Normal 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 |
1
apps/agent/assets/mcp-icons/salesforce.svg
Normal 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 |
6
apps/agent/assets/mcp-icons/slack.svg
Normal 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 |
259
apps/agent/components/elements/AppSelector.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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)]" />
|
||||
|
||||
@@ -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
|
||||
|
||||
67
apps/agent/entrypoints/app/connect-mcp/McpServerIcon.tsx
Normal 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} />
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||