feat(desktop): add mcp client registration status and authentication handling (#27525)
This commit is contained in:
@@ -13,6 +13,7 @@ const statusLabels = {
|
|||||||
connected: "mcp.status.connected",
|
connected: "mcp.status.connected",
|
||||||
failed: "mcp.status.failed",
|
failed: "mcp.status.failed",
|
||||||
needs_auth: "mcp.status.needs_auth",
|
needs_auth: "mcp.status.needs_auth",
|
||||||
|
needs_client_registration: "mcp.status.needs_client_registration",
|
||||||
disabled: "mcp.status.disabled",
|
disabled: "mcp.status.disabled",
|
||||||
} as const
|
} as const
|
||||||
|
|
||||||
@@ -31,8 +32,16 @@ export const DialogSelectMcp: Component = () => {
|
|||||||
|
|
||||||
const toggle = useMutation(() => ({
|
const toggle = useMutation(() => ({
|
||||||
mutationFn: async (name: string) => {
|
mutationFn: async (name: string) => {
|
||||||
if (sync.data.mcp[name]?.status === "connected") await sdk.client.mcp.disconnect({ name })
|
const status = sync.data.mcp[name]
|
||||||
else await 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))),
|
onSuccess: () => queryClient.refetchQueries(queryOptions.mcp(pathKey(sync.directory))),
|
||||||
}))
|
}))
|
||||||
@@ -67,7 +76,7 @@ export const DialogSelectMcp: Component = () => {
|
|||||||
}
|
}
|
||||||
const error = () => {
|
const error = () => {
|
||||||
const s = mcpStatus()
|
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"
|
const enabled = () => status() === "connected"
|
||||||
return (
|
return (
|
||||||
@@ -78,9 +87,6 @@ export const DialogSelectMcp: Component = () => {
|
|||||||
<Show when={statusLabel()}>
|
<Show when={statusLabel()}>
|
||||||
<span class="text-11-regular text-text-weaker">{statusLabel()}</span>
|
<span class="text-11-regular text-text-weaker">{statusLabel()}</span>
|
||||||
</Show>
|
</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>
|
</div>
|
||||||
<Show when={error()}>
|
<Show when={error()}>
|
||||||
<span class="text-11-regular text-text-weaker truncate">{error()}</span>
|
<span class="text-11-regular text-text-weaker truncate">{error()}</span>
|
||||||
|
|||||||
@@ -145,7 +145,15 @@ const useMcpToggleMutation = () => {
|
|||||||
return useMutation(() => ({
|
return useMutation(() => ({
|
||||||
mutationFn: async (name: string) => {
|
mutationFn: async (name: string) => {
|
||||||
const status = sync.data.mcp[name]
|
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))),
|
onSuccess: () => queryClient.refetchQueries(queryOptions.mcp(pathKey(sync.directory))),
|
||||||
onError: (err) => {
|
onError: (err) => {
|
||||||
@@ -316,7 +324,7 @@ export function StatusPopoverBody(props: { shown: Accessor<boolean> }) {
|
|||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
type="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={() => {
|
onClick={() => {
|
||||||
if (toggleMcp.isPending) return
|
if (toggleMcp.isPending) return
|
||||||
toggleMcp.mutate(name)
|
toggleMcp.mutate(name)
|
||||||
@@ -333,7 +341,14 @@ export function StatusPopoverBody(props: { shown: Accessor<boolean> }) {
|
|||||||
status() === "needs_auth" || status() === "needs_client_registration",
|
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()}>
|
<div onClick={(event) => event.stopPropagation()}>
|
||||||
<Switch
|
<Switch
|
||||||
checked={enabled()}
|
checked={enabled()}
|
||||||
|
|||||||
@@ -14,12 +14,14 @@ export function StatusPopover() {
|
|||||||
const sync = useSync()
|
const sync = useSync()
|
||||||
const [shown, setShown] = createSignal(false)
|
const [shown, setShown] = createSignal(false)
|
||||||
const ready = createMemo(() => server.healthy() === false || sync.data.mcp_ready)
|
const ready = createMemo(() => server.healthy() === false || sync.data.mcp_ready)
|
||||||
const healthy = createMemo(() => {
|
const mcpIssue = createMemo(() => {
|
||||||
const serverHealthy = server.healthy() === true
|
|
||||||
const mcp = Object.values(sync.data.mcp ?? {})
|
const mcp = Object.values(sync.data.mcp ?? {})
|
||||||
const issue = mcp.some((item) => item.status !== "connected" && item.status !== "disabled")
|
const failed = mcp.some((item) => item.status === "failed" || item.status === "needs_client_registration")
|
||||||
return serverHealthy && !issue
|
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 (
|
return (
|
||||||
<Popover
|
<Popover
|
||||||
@@ -41,7 +43,9 @@ export function StatusPopover() {
|
|||||||
classList={{
|
classList={{
|
||||||
"absolute -top-px -right-px size-1.5 rounded-full": true,
|
"absolute -top-px -right-px size-1.5 rounded-full": true,
|
||||||
"bg-icon-success-base": ready() && healthy(),
|
"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(),
|
"bg-border-weak-base": server.healthy() === undefined || !ready(),
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -276,6 +276,7 @@ export const dict = {
|
|||||||
"mcp.status.connected": "متصل",
|
"mcp.status.connected": "متصل",
|
||||||
"mcp.status.failed": "فشل",
|
"mcp.status.failed": "فشل",
|
||||||
"mcp.status.needs_auth": "يحتاج إلى مصادقة",
|
"mcp.status.needs_auth": "يحتاج إلى مصادقة",
|
||||||
|
"mcp.auth.clickToAuthenticate": "انقر للمصادقة",
|
||||||
"mcp.status.disabled": "معطل",
|
"mcp.status.disabled": "معطل",
|
||||||
"dialog.fork.empty": "لا توجد رسائل للتفرع منها",
|
"dialog.fork.empty": "لا توجد رسائل للتفرع منها",
|
||||||
"dialog.directory.search.placeholder": "البحث في المجلدات",
|
"dialog.directory.search.placeholder": "البحث في المجلدات",
|
||||||
|
|||||||
@@ -276,6 +276,7 @@ export const dict = {
|
|||||||
"mcp.status.connected": "conectado",
|
"mcp.status.connected": "conectado",
|
||||||
"mcp.status.failed": "falhou",
|
"mcp.status.failed": "falhou",
|
||||||
"mcp.status.needs_auth": "precisa de autenticação",
|
"mcp.status.needs_auth": "precisa de autenticação",
|
||||||
|
"mcp.auth.clickToAuthenticate": "Clique para autenticar",
|
||||||
"mcp.status.disabled": "desabilitado",
|
"mcp.status.disabled": "desabilitado",
|
||||||
"dialog.fork.empty": "Nenhuma mensagem para bifurcar",
|
"dialog.fork.empty": "Nenhuma mensagem para bifurcar",
|
||||||
"dialog.directory.search.placeholder": "Buscar pastas",
|
"dialog.directory.search.placeholder": "Buscar pastas",
|
||||||
|
|||||||
@@ -300,6 +300,7 @@ export const dict = {
|
|||||||
"mcp.status.connected": "povezano",
|
"mcp.status.connected": "povezano",
|
||||||
"mcp.status.failed": "neuspjelo",
|
"mcp.status.failed": "neuspjelo",
|
||||||
"mcp.status.needs_auth": "potrebna autentifikacija",
|
"mcp.status.needs_auth": "potrebna autentifikacija",
|
||||||
|
"mcp.auth.clickToAuthenticate": "Kliknite za autentifikaciju",
|
||||||
"mcp.status.disabled": "onemogućeno",
|
"mcp.status.disabled": "onemogućeno",
|
||||||
|
|
||||||
"dialog.fork.empty": "Nema poruka za fork",
|
"dialog.fork.empty": "Nema poruka za fork",
|
||||||
|
|||||||
@@ -298,6 +298,7 @@ export const dict = {
|
|||||||
"mcp.status.connected": "forbundet",
|
"mcp.status.connected": "forbundet",
|
||||||
"mcp.status.failed": "mislykkedes",
|
"mcp.status.failed": "mislykkedes",
|
||||||
"mcp.status.needs_auth": "kræver godkendelse",
|
"mcp.status.needs_auth": "kræver godkendelse",
|
||||||
|
"mcp.auth.clickToAuthenticate": "Klik for at godkende",
|
||||||
"mcp.status.disabled": "deaktiveret",
|
"mcp.status.disabled": "deaktiveret",
|
||||||
|
|
||||||
"dialog.fork.empty": "Ingen beskeder at forgrene fra",
|
"dialog.fork.empty": "Ingen beskeder at forgrene fra",
|
||||||
|
|||||||
@@ -282,6 +282,7 @@ export const dict = {
|
|||||||
"mcp.status.connected": "verbunden",
|
"mcp.status.connected": "verbunden",
|
||||||
"mcp.status.failed": "fehlgeschlagen",
|
"mcp.status.failed": "fehlgeschlagen",
|
||||||
"mcp.status.needs_auth": "benötigt Authentifizierung",
|
"mcp.status.needs_auth": "benötigt Authentifizierung",
|
||||||
|
"mcp.auth.clickToAuthenticate": "Zum Authentifizieren klicken",
|
||||||
"mcp.status.disabled": "deaktiviert",
|
"mcp.status.disabled": "deaktiviert",
|
||||||
"dialog.fork.empty": "Keine Nachrichten zum Abzweigen vorhanden",
|
"dialog.fork.empty": "Keine Nachrichten zum Abzweigen vorhanden",
|
||||||
"dialog.directory.search.placeholder": "Ordner durchsuchen",
|
"dialog.directory.search.placeholder": "Ordner durchsuchen",
|
||||||
|
|||||||
@@ -306,6 +306,7 @@ export const dict = {
|
|||||||
"mcp.status.failed": "failed",
|
"mcp.status.failed": "failed",
|
||||||
"mcp.status.needs_auth": "needs auth",
|
"mcp.status.needs_auth": "needs auth",
|
||||||
"mcp.status.disabled": "disabled",
|
"mcp.status.disabled": "disabled",
|
||||||
|
"mcp.auth.clickToAuthenticate": "Click to authenticate",
|
||||||
|
|
||||||
"dialog.fork.empty": "No messages to fork from",
|
"dialog.fork.empty": "No messages to fork from",
|
||||||
|
|
||||||
|
|||||||
@@ -299,6 +299,7 @@ export const dict = {
|
|||||||
"mcp.status.connected": "conectado",
|
"mcp.status.connected": "conectado",
|
||||||
"mcp.status.failed": "fallido",
|
"mcp.status.failed": "fallido",
|
||||||
"mcp.status.needs_auth": "necesita auth",
|
"mcp.status.needs_auth": "necesita auth",
|
||||||
|
"mcp.auth.clickToAuthenticate": "Haz clic para autenticar",
|
||||||
"mcp.status.disabled": "deshabilitado",
|
"mcp.status.disabled": "deshabilitado",
|
||||||
|
|
||||||
"dialog.fork.empty": "No hay mensajes desde donde bifurcar",
|
"dialog.fork.empty": "No hay mensajes desde donde bifurcar",
|
||||||
|
|||||||
@@ -277,6 +277,7 @@ export const dict = {
|
|||||||
"mcp.status.connected": "connecté",
|
"mcp.status.connected": "connecté",
|
||||||
"mcp.status.failed": "échoué",
|
"mcp.status.failed": "échoué",
|
||||||
"mcp.status.needs_auth": "nécessite auth",
|
"mcp.status.needs_auth": "nécessite auth",
|
||||||
|
"mcp.auth.clickToAuthenticate": "Cliquez pour vous authentifier",
|
||||||
"mcp.status.disabled": "désactivé",
|
"mcp.status.disabled": "désactivé",
|
||||||
"dialog.fork.empty": "Aucun message à partir duquel bifurquer",
|
"dialog.fork.empty": "Aucun message à partir duquel bifurquer",
|
||||||
"dialog.directory.search.placeholder": "Rechercher des dossiers",
|
"dialog.directory.search.placeholder": "Rechercher des dossiers",
|
||||||
|
|||||||
@@ -275,6 +275,7 @@ export const dict = {
|
|||||||
"mcp.status.connected": "接続済み",
|
"mcp.status.connected": "接続済み",
|
||||||
"mcp.status.failed": "失敗",
|
"mcp.status.failed": "失敗",
|
||||||
"mcp.status.needs_auth": "認証が必要",
|
"mcp.status.needs_auth": "認証が必要",
|
||||||
|
"mcp.auth.clickToAuthenticate": "クリックして認証",
|
||||||
"mcp.status.disabled": "無効",
|
"mcp.status.disabled": "無効",
|
||||||
"dialog.fork.empty": "フォーク元のメッセージがありません",
|
"dialog.fork.empty": "フォーク元のメッセージがありません",
|
||||||
"dialog.directory.search.placeholder": "フォルダを検索",
|
"dialog.directory.search.placeholder": "フォルダを検索",
|
||||||
|
|||||||
@@ -275,6 +275,7 @@ export const dict = {
|
|||||||
"mcp.status.connected": "연결됨",
|
"mcp.status.connected": "연결됨",
|
||||||
"mcp.status.failed": "실패",
|
"mcp.status.failed": "실패",
|
||||||
"mcp.status.needs_auth": "인증 필요",
|
"mcp.status.needs_auth": "인증 필요",
|
||||||
|
"mcp.auth.clickToAuthenticate": "클릭하여 인증",
|
||||||
"mcp.status.disabled": "비활성화됨",
|
"mcp.status.disabled": "비활성화됨",
|
||||||
"dialog.fork.empty": "분기할 메시지 없음",
|
"dialog.fork.empty": "분기할 메시지 없음",
|
||||||
"dialog.directory.search.placeholder": "폴더 검색",
|
"dialog.directory.search.placeholder": "폴더 검색",
|
||||||
|
|||||||
@@ -302,6 +302,7 @@ export const dict = {
|
|||||||
"mcp.status.connected": "tilkoblet",
|
"mcp.status.connected": "tilkoblet",
|
||||||
"mcp.status.failed": "mislyktes",
|
"mcp.status.failed": "mislyktes",
|
||||||
"mcp.status.needs_auth": "trenger autentisering",
|
"mcp.status.needs_auth": "trenger autentisering",
|
||||||
|
"mcp.auth.clickToAuthenticate": "Klikk for å autentisere",
|
||||||
"mcp.status.disabled": "deaktivert",
|
"mcp.status.disabled": "deaktivert",
|
||||||
|
|
||||||
"dialog.fork.empty": "Ingen meldinger å forgrene fra",
|
"dialog.fork.empty": "Ingen meldinger å forgrene fra",
|
||||||
|
|||||||
@@ -277,6 +277,7 @@ export const dict = {
|
|||||||
"mcp.status.connected": "połączono",
|
"mcp.status.connected": "połączono",
|
||||||
"mcp.status.failed": "niepowodzenie",
|
"mcp.status.failed": "niepowodzenie",
|
||||||
"mcp.status.needs_auth": "wymaga autoryzacji",
|
"mcp.status.needs_auth": "wymaga autoryzacji",
|
||||||
|
"mcp.auth.clickToAuthenticate": "Kliknij, aby się uwierzytelnić",
|
||||||
"mcp.status.disabled": "wyłączone",
|
"mcp.status.disabled": "wyłączone",
|
||||||
"dialog.fork.empty": "Brak wiadomości do rozwidlenia",
|
"dialog.fork.empty": "Brak wiadomości do rozwidlenia",
|
||||||
"dialog.directory.search.placeholder": "Szukaj folderów",
|
"dialog.directory.search.placeholder": "Szukaj folderów",
|
||||||
|
|||||||
@@ -299,6 +299,7 @@ export const dict = {
|
|||||||
"mcp.status.connected": "подключено",
|
"mcp.status.connected": "подключено",
|
||||||
"mcp.status.failed": "ошибка",
|
"mcp.status.failed": "ошибка",
|
||||||
"mcp.status.needs_auth": "требуется авторизация",
|
"mcp.status.needs_auth": "требуется авторизация",
|
||||||
|
"mcp.auth.clickToAuthenticate": "Нажмите, чтобы авторизоваться",
|
||||||
"mcp.status.disabled": "отключено",
|
"mcp.status.disabled": "отключено",
|
||||||
|
|
||||||
"dialog.fork.empty": "Нет сообщений для ответвления",
|
"dialog.fork.empty": "Нет сообщений для ответвления",
|
||||||
|
|||||||
@@ -299,6 +299,7 @@ export const dict = {
|
|||||||
"mcp.status.connected": "เชื่อมต่อแล้ว",
|
"mcp.status.connected": "เชื่อมต่อแล้ว",
|
||||||
"mcp.status.failed": "ล้มเหลว",
|
"mcp.status.failed": "ล้มเหลว",
|
||||||
"mcp.status.needs_auth": "ต้องการการตรวจสอบสิทธิ์",
|
"mcp.status.needs_auth": "ต้องการการตรวจสอบสิทธิ์",
|
||||||
|
"mcp.auth.clickToAuthenticate": "คลิกเพื่อยืนยันตัวตน",
|
||||||
"mcp.status.disabled": "ปิดใช้งาน",
|
"mcp.status.disabled": "ปิดใช้งาน",
|
||||||
|
|
||||||
"dialog.fork.empty": "ไม่มีข้อความให้แตกแขนง",
|
"dialog.fork.empty": "ไม่มีข้อความให้แตกแขนง",
|
||||||
|
|||||||
@@ -304,6 +304,7 @@ export const dict = {
|
|||||||
"mcp.status.connected": "bağlı",
|
"mcp.status.connected": "bağlı",
|
||||||
"mcp.status.failed": "başarısız",
|
"mcp.status.failed": "başarısız",
|
||||||
"mcp.status.needs_auth": "kimlik doğrulama gerekli",
|
"mcp.status.needs_auth": "kimlik doğrulama gerekli",
|
||||||
|
"mcp.auth.clickToAuthenticate": "Kimlik doğrulamak için tıklayın",
|
||||||
"mcp.status.disabled": "devre dışı",
|
"mcp.status.disabled": "devre dışı",
|
||||||
|
|
||||||
"dialog.fork.empty": "Dallandırılacak mesaj yok",
|
"dialog.fork.empty": "Dallandırılacak mesaj yok",
|
||||||
|
|||||||
@@ -319,6 +319,7 @@ export const dict = {
|
|||||||
"mcp.status.connected": "已连接",
|
"mcp.status.connected": "已连接",
|
||||||
"mcp.status.failed": "失败",
|
"mcp.status.failed": "失败",
|
||||||
"mcp.status.needs_auth": "需要授权",
|
"mcp.status.needs_auth": "需要授权",
|
||||||
|
"mcp.auth.clickToAuthenticate": "点击进行授权",
|
||||||
"mcp.status.disabled": "已禁用",
|
"mcp.status.disabled": "已禁用",
|
||||||
|
|
||||||
"dialog.fork.empty": "没有可用于分叉的消息",
|
"dialog.fork.empty": "没有可用于分叉的消息",
|
||||||
|
|||||||
@@ -299,6 +299,7 @@ export const dict = {
|
|||||||
"mcp.status.connected": "已連線",
|
"mcp.status.connected": "已連線",
|
||||||
"mcp.status.failed": "失敗",
|
"mcp.status.failed": "失敗",
|
||||||
"mcp.status.needs_auth": "需要授權",
|
"mcp.status.needs_auth": "需要授權",
|
||||||
|
"mcp.auth.clickToAuthenticate": "點擊以進行授權",
|
||||||
"mcp.status.disabled": "已停用",
|
"mcp.status.disabled": "已停用",
|
||||||
|
|
||||||
"dialog.fork.empty": "沒有可用於分支的訊息",
|
"dialog.fork.empty": "沒有可用於分支的訊息",
|
||||||
|
|||||||
Reference in New Issue
Block a user