Просмотр исходного кода

feat(desktop): add mcp client registration status and authentication handling (#27525)

OpeOginni 3 месяцев назад
Родитель
Сommit
337993d53e

+ 12 - 6
packages/app/src/components/dialog-select-mcp.tsx

@@ -13,6 +13,7 @@ const statusLabels = {
   connected: "mcp.status.connected",
   failed: "mcp.status.failed",
   needs_auth: "mcp.status.needs_auth",
+  needs_client_registration: "mcp.status.needs_client_registration",
   disabled: "mcp.status.disabled",
 } as const
 
@@ -31,8 +32,16 @@ export const DialogSelectMcp: Component = () => {
 
   const toggle = useMutation(() => ({
     mutationFn: async (name: string) => {
-      if (sync.data.mcp[name]?.status === "connected") await sdk.client.mcp.disconnect({ name })
-      else await sdk.client.mcp.connect({ name })
+      const status = sync.data.mcp[name]
+      if (status?.status === "connected") {
+        await sdk.client.mcp.disconnect({ name })
+        return
+      }
+      if (status?.status === "needs_auth") {
+        await sdk.client.mcp.auth.authenticate({ name })
+        return
+      }
+      await sdk.client.mcp.connect({ name })
     },
     onSuccess: () => queryClient.refetchQueries(queryOptions.mcp(pathKey(sync.directory))),
   }))
@@ -67,7 +76,7 @@ export const DialogSelectMcp: Component = () => {
           }
           const error = () => {
             const s = mcpStatus()
-            return s?.status === "failed" ? s.error : undefined
+            if (s?.status === "failed" || s?.status === "needs_client_registration") return s.error
           }
           const enabled = () => status() === "connected"
           return (
@@ -78,9 +87,6 @@ export const DialogSelectMcp: Component = () => {
                   <Show when={statusLabel()}>
                     <span class="text-11-regular text-text-weaker">{statusLabel()}</span>
                   </Show>
-                  <Show when={toggle.isPending && toggle.variables === i.name}>
-                    <span class="text-11-regular text-text-weak">{language.t("common.loading.ellipsis")}</span>
-                  </Show>
                 </div>
                 <Show when={error()}>
                   <span class="text-11-regular text-text-weaker truncate">{error()}</span>

+ 18 - 3
packages/app/src/components/status-popover-body.tsx

@@ -145,7 +145,15 @@ const useMcpToggleMutation = () => {
   return useMutation(() => ({
     mutationFn: async (name: string) => {
       const status = sync.data.mcp[name]
-      await (status?.status === "connected" ? sdk.client.mcp.disconnect({ name }) : sdk.client.mcp.connect({ name }))
+      if (status?.status === "connected") {
+        await sdk.client.mcp.disconnect({ name })
+        return
+      }
+      if (status?.status === "needs_auth") {
+        await sdk.client.mcp.auth.authenticate({ name })
+        return
+      }
+      await sdk.client.mcp.connect({ name })
     },
     onSuccess: () => queryClient.refetchQueries(queryOptions.mcp(pathKey(sync.directory))),
     onError: (err) => {
@@ -316,7 +324,7 @@ export function StatusPopoverBody(props: { shown: Accessor<boolean> }) {
                     return (
                       <button
                         type="button"
-                        class="flex items-center gap-2 w-full h-8 pl-3 pr-2 py-1 rounded-md hover:bg-surface-raised-base-hover transition-colors text-left"
+                        class="flex items-center gap-2 w-full min-h-8 pl-3 pr-2 py-1 rounded-md hover:bg-surface-raised-base-hover transition-colors text-left"
                         onClick={() => {
                           if (toggleMcp.isPending) return
                           toggleMcp.mutate(name)
@@ -333,7 +341,14 @@ export function StatusPopoverBody(props: { shown: Accessor<boolean> }) {
                               status() === "needs_auth" || status() === "needs_client_registration",
                           }}
                         />
-                        <span class="text-14-regular text-text-base truncate flex-1">{name}</span>
+                        <span class="flex flex-col min-w-0 flex-1">
+                          <span class="flex items-center gap-2 min-w-0">
+                            <span class="text-14-regular text-text-base truncate">{name}</span>
+                          </span>
+                          <Show when={status() === "needs_auth"}>
+                            <span class="text-11-regular text-text-weaker truncate">{language.t("mcp.auth.clickToAuthenticate")}</span>
+                          </Show>
+                        </span>
                         <div onClick={(event) => event.stopPropagation()}>
                           <Switch
                             checked={enabled()}

+ 9 - 5
packages/app/src/components/status-popover.tsx

@@ -14,12 +14,14 @@ export function StatusPopover() {
   const sync = useSync()
   const [shown, setShown] = createSignal(false)
   const ready = createMemo(() => server.healthy() === false || sync.data.mcp_ready)
-  const healthy = createMemo(() => {
-    const serverHealthy = server.healthy() === true
+  const mcpIssue = createMemo(() => {
     const mcp = Object.values(sync.data.mcp ?? {})
-    const issue = mcp.some((item) => item.status !== "connected" && item.status !== "disabled")
-    return serverHealthy && !issue
+    const failed = mcp.some((item) => item.status === "failed" || item.status === "needs_client_registration")
+    const warn = mcp.some((item) => item.status === "needs_auth")
+    if (failed) return "critical" as const
+    if (warn) return "warning" as const
   })
+  const healthy = createMemo(() => server.healthy() === true && !mcpIssue())
 
   return (
     <Popover
@@ -41,7 +43,9 @@ export function StatusPopover() {
             classList={{
               "absolute -top-px -right-px size-1.5 rounded-full": true,
               "bg-icon-success-base": ready() && healthy(),
-              "bg-icon-critical-base": server.healthy() === false || (ready() && !healthy()),
+              "bg-icon-warning-base": ready() && server.healthy() === true && mcpIssue() === "warning",
+              "bg-icon-critical-base":
+                server.healthy() === false || (ready() && server.healthy() === true && mcpIssue() === "critical"),
               "bg-border-weak-base": server.healthy() === undefined || !ready(),
             }}
           />

+ 1 - 0
packages/app/src/i18n/ar.ts

@@ -276,6 +276,7 @@ export const dict = {
   "mcp.status.connected": "متصل",
   "mcp.status.failed": "فشل",
   "mcp.status.needs_auth": "يحتاج إلى مصادقة",
+  "mcp.auth.clickToAuthenticate": "انقر للمصادقة",
   "mcp.status.disabled": "معطل",
   "dialog.fork.empty": "لا توجد رسائل للتفرع منها",
   "dialog.directory.search.placeholder": "البحث في المجلدات",

+ 1 - 0
packages/app/src/i18n/br.ts

@@ -276,6 +276,7 @@ export const dict = {
   "mcp.status.connected": "conectado",
   "mcp.status.failed": "falhou",
   "mcp.status.needs_auth": "precisa de autenticação",
+  "mcp.auth.clickToAuthenticate": "Clique para autenticar",
   "mcp.status.disabled": "desabilitado",
   "dialog.fork.empty": "Nenhuma mensagem para bifurcar",
   "dialog.directory.search.placeholder": "Buscar pastas",

+ 1 - 0
packages/app/src/i18n/bs.ts

@@ -300,6 +300,7 @@ export const dict = {
   "mcp.status.connected": "povezano",
   "mcp.status.failed": "neuspjelo",
   "mcp.status.needs_auth": "potrebna autentifikacija",
+  "mcp.auth.clickToAuthenticate": "Kliknite za autentifikaciju",
   "mcp.status.disabled": "onemogućeno",
 
   "dialog.fork.empty": "Nema poruka za fork",

+ 1 - 0
packages/app/src/i18n/da.ts

@@ -298,6 +298,7 @@ export const dict = {
   "mcp.status.connected": "forbundet",
   "mcp.status.failed": "mislykkedes",
   "mcp.status.needs_auth": "kræver godkendelse",
+  "mcp.auth.clickToAuthenticate": "Klik for at godkende",
   "mcp.status.disabled": "deaktiveret",
 
   "dialog.fork.empty": "Ingen beskeder at forgrene fra",

+ 1 - 0
packages/app/src/i18n/de.ts

@@ -282,6 +282,7 @@ export const dict = {
   "mcp.status.connected": "verbunden",
   "mcp.status.failed": "fehlgeschlagen",
   "mcp.status.needs_auth": "benötigt Authentifizierung",
+  "mcp.auth.clickToAuthenticate": "Zum Authentifizieren klicken",
   "mcp.status.disabled": "deaktiviert",
   "dialog.fork.empty": "Keine Nachrichten zum Abzweigen vorhanden",
   "dialog.directory.search.placeholder": "Ordner durchsuchen",

+ 1 - 0
packages/app/src/i18n/en.ts

@@ -306,6 +306,7 @@ export const dict = {
   "mcp.status.failed": "failed",
   "mcp.status.needs_auth": "needs auth",
   "mcp.status.disabled": "disabled",
+  "mcp.auth.clickToAuthenticate": "Click to authenticate",
 
   "dialog.fork.empty": "No messages to fork from",
 

+ 1 - 0
packages/app/src/i18n/es.ts

@@ -299,6 +299,7 @@ export const dict = {
   "mcp.status.connected": "conectado",
   "mcp.status.failed": "fallido",
   "mcp.status.needs_auth": "necesita auth",
+  "mcp.auth.clickToAuthenticate": "Haz clic para autenticar",
   "mcp.status.disabled": "deshabilitado",
 
   "dialog.fork.empty": "No hay mensajes desde donde bifurcar",

+ 1 - 0
packages/app/src/i18n/fr.ts

@@ -277,6 +277,7 @@ export const dict = {
   "mcp.status.connected": "connecté",
   "mcp.status.failed": "échoué",
   "mcp.status.needs_auth": "nécessite auth",
+  "mcp.auth.clickToAuthenticate": "Cliquez pour vous authentifier",
   "mcp.status.disabled": "désactivé",
   "dialog.fork.empty": "Aucun message à partir duquel bifurquer",
   "dialog.directory.search.placeholder": "Rechercher des dossiers",

+ 1 - 0
packages/app/src/i18n/ja.ts

@@ -275,6 +275,7 @@ export const dict = {
   "mcp.status.connected": "接続済み",
   "mcp.status.failed": "失敗",
   "mcp.status.needs_auth": "認証が必要",
+  "mcp.auth.clickToAuthenticate": "クリックして認証",
   "mcp.status.disabled": "無効",
   "dialog.fork.empty": "フォーク元のメッセージがありません",
   "dialog.directory.search.placeholder": "フォルダを検索",

+ 1 - 0
packages/app/src/i18n/ko.ts

@@ -275,6 +275,7 @@ export const dict = {
   "mcp.status.connected": "연결됨",
   "mcp.status.failed": "실패",
   "mcp.status.needs_auth": "인증 필요",
+  "mcp.auth.clickToAuthenticate": "클릭하여 인증",
   "mcp.status.disabled": "비활성화됨",
   "dialog.fork.empty": "분기할 메시지 없음",
   "dialog.directory.search.placeholder": "폴더 검색",

+ 1 - 0
packages/app/src/i18n/no.ts

@@ -302,6 +302,7 @@ export const dict = {
   "mcp.status.connected": "tilkoblet",
   "mcp.status.failed": "mislyktes",
   "mcp.status.needs_auth": "trenger autentisering",
+  "mcp.auth.clickToAuthenticate": "Klikk for å autentisere",
   "mcp.status.disabled": "deaktivert",
 
   "dialog.fork.empty": "Ingen meldinger å forgrene fra",

+ 1 - 0
packages/app/src/i18n/pl.ts

@@ -277,6 +277,7 @@ export const dict = {
   "mcp.status.connected": "połączono",
   "mcp.status.failed": "niepowodzenie",
   "mcp.status.needs_auth": "wymaga autoryzacji",
+  "mcp.auth.clickToAuthenticate": "Kliknij, aby się uwierzytelnić",
   "mcp.status.disabled": "wyłączone",
   "dialog.fork.empty": "Brak wiadomości do rozwidlenia",
   "dialog.directory.search.placeholder": "Szukaj folderów",

+ 1 - 0
packages/app/src/i18n/ru.ts

@@ -299,6 +299,7 @@ export const dict = {
   "mcp.status.connected": "подключено",
   "mcp.status.failed": "ошибка",
   "mcp.status.needs_auth": "требуется авторизация",
+  "mcp.auth.clickToAuthenticate": "Нажмите, чтобы авторизоваться",
   "mcp.status.disabled": "отключено",
 
   "dialog.fork.empty": "Нет сообщений для ответвления",

+ 1 - 0
packages/app/src/i18n/th.ts

@@ -299,6 +299,7 @@ export const dict = {
   "mcp.status.connected": "เชื่อมต่อแล้ว",
   "mcp.status.failed": "ล้มเหลว",
   "mcp.status.needs_auth": "ต้องการการตรวจสอบสิทธิ์",
+  "mcp.auth.clickToAuthenticate": "คลิกเพื่อยืนยันตัวตน",
   "mcp.status.disabled": "ปิดใช้งาน",
 
   "dialog.fork.empty": "ไม่มีข้อความให้แตกแขนง",

+ 1 - 0
packages/app/src/i18n/tr.ts

@@ -304,6 +304,7 @@ export const dict = {
   "mcp.status.connected": "bağlı",
   "mcp.status.failed": "başarısız",
   "mcp.status.needs_auth": "kimlik doğrulama gerekli",
+  "mcp.auth.clickToAuthenticate": "Kimlik doğrulamak için tıklayın",
   "mcp.status.disabled": "devre dışı",
 
   "dialog.fork.empty": "Dallandırılacak mesaj yok",

+ 1 - 0
packages/app/src/i18n/zh.ts

@@ -319,6 +319,7 @@ export const dict = {
   "mcp.status.connected": "已连接",
   "mcp.status.failed": "失败",
   "mcp.status.needs_auth": "需要授权",
+  "mcp.auth.clickToAuthenticate": "点击进行授权",
   "mcp.status.disabled": "已禁用",
 
   "dialog.fork.empty": "没有可用于分叉的消息",

+ 1 - 0
packages/app/src/i18n/zht.ts

@@ -299,6 +299,7 @@ export const dict = {
   "mcp.status.connected": "已連線",
   "mcp.status.failed": "失敗",
   "mcp.status.needs_auth": "需要授權",
+  "mcp.auth.clickToAuthenticate": "點擊以進行授權",
   "mcp.status.disabled": "已停用",
 
   "dialog.fork.empty": "沒有可用於分支的訊息",