mirror of
https://github.com/anomalyco/opencode.git
synced 2026-05-15 17:13:12 +00:00
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",
|
||||
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>
|
||||
|
||||
@@ -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()}
|
||||
|
||||
@@ -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(),
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -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": "البحث في المجلدات",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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": "フォルダを検索",
|
||||
|
||||
@@ -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": "폴더 검색",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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": "Нет сообщений для ответвления",
|
||||
|
||||
@@ -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": "ไม่มีข้อความให้แตกแขนง",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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": "没有可用于分叉的消息",
|
||||
|
||||
@@ -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": "沒有可用於分支的訊息",
|
||||
|
||||
Reference in New Issue
Block a user