feat(settings): add MCP server status check
This commit is contained in:
parent
088eaccecc
commit
17504be027
|
|
@ -688,7 +688,7 @@ function openGitHub() {
|
|||
openUrl("https://github.com/t8y2/dbx");
|
||||
}
|
||||
function openMcpGuide() {
|
||||
openUrl("https://github.com/t8y2/dbx/blob/main/docs/mcp-guide.md");
|
||||
openUrl("https://dbxio.com/cn/docs/mcp");
|
||||
}
|
||||
|
||||
function ensureQueryTab(): string {
|
||||
|
|
|
|||
|
|
@ -3,14 +3,19 @@ import { ref, watch, shallowRef, computed, onMounted } from "vue";
|
|||
import type { EditorView as EditorViewType } from "@codemirror/view";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import {
|
||||
AlertTriangle,
|
||||
CheckCircle2,
|
||||
CircleHelp,
|
||||
Cloud,
|
||||
Copy,
|
||||
Download,
|
||||
ExternalLink,
|
||||
Loader2,
|
||||
PackageSearch,
|
||||
Pencil,
|
||||
RefreshCw,
|
||||
Settings,
|
||||
Terminal,
|
||||
Trash2,
|
||||
Upload,
|
||||
X,
|
||||
|
|
@ -41,9 +46,11 @@ import {
|
|||
import { loadEditorTheme, editorFontTheme } from "@/lib/editorThemes";
|
||||
import { isTauriRuntime } from "@/lib/tauriRuntime";
|
||||
import { useTheme } from "@/composables/useTheme";
|
||||
import { copyToClipboard } from "@/lib/clipboard";
|
||||
import {
|
||||
aiListModels,
|
||||
aiTestConnection,
|
||||
checkMcpServerStatus,
|
||||
forgetWebdavSavedPassword,
|
||||
listSystemFonts,
|
||||
saveWebdavSavedPassword,
|
||||
|
|
@ -52,6 +59,7 @@ import {
|
|||
webdavSyncTest,
|
||||
webdavSyncUpload,
|
||||
type AiModelInfo,
|
||||
type McpServerStatus,
|
||||
type WebDavConfig,
|
||||
} from "@/lib/api";
|
||||
import { eventToShortcut } from "@/lib/keyboardShortcuts";
|
||||
|
|
@ -425,6 +433,7 @@ type SettingsCategory =
|
|||
| "snippets"
|
||||
| "sync"
|
||||
| "ai"
|
||||
| "mcp"
|
||||
| "security"
|
||||
| "about";
|
||||
const settingsCategoryNav = computed<{ value: SettingsCategory; label: string }[]>(() => [
|
||||
|
|
@ -436,6 +445,7 @@ const settingsCategoryNav = computed<{ value: SettingsCategory; label: string }[
|
|||
{ value: "snippets", label: t("settings.snippetsTab") },
|
||||
...(isWeb ? [] : [{ value: "sync" as const, label: t("settings.syncTab") }]),
|
||||
{ value: "ai", label: t("settings.aiTab") },
|
||||
...(isWeb ? [] : [{ value: "mcp" as const, label: t("settings.mcpTab") }]),
|
||||
...(isWeb ? [{ value: "security" as const, label: t("settings.securityTab") }] : []),
|
||||
{ value: "about", label: t("settings.aboutTab") },
|
||||
]);
|
||||
|
|
@ -469,6 +479,66 @@ function openExternalUrl(url: string) {
|
|||
}
|
||||
}
|
||||
|
||||
// ---------- MCP Server ----------
|
||||
const mcpStatus = ref<McpServerStatus | null>(null);
|
||||
const mcpStatusLoading = ref(false);
|
||||
const mcpStatusError = ref("");
|
||||
const mcpCopied = ref<"" | "install" | "config">("");
|
||||
|
||||
const mcpRecommendedConfig = `{
|
||||
"mcpServers": {
|
||||
"dbx": {
|
||||
"command": "dbx-mcp-server"
|
||||
}
|
||||
}
|
||||
}`;
|
||||
|
||||
const mcpStatusTone = computed<"ok" | "warning" | "muted">(() => {
|
||||
if (!mcpStatus.value) return "muted";
|
||||
if (!mcpStatus.value.installed || mcpStatus.value.update_available || mcpStatus.value.error) return "warning";
|
||||
return "ok";
|
||||
});
|
||||
|
||||
const mcpStatusLabel = computed(() => {
|
||||
if (mcpStatusLoading.value) return t("settings.mcpChecking");
|
||||
if (mcpStatusError.value) return t("settings.mcpStatusError");
|
||||
if (!mcpStatus.value) return t("settings.mcpStatusUnknown");
|
||||
if (!mcpStatus.value.installed) return t("settings.mcpNotInstalled");
|
||||
if (mcpStatus.value.update_available) return t("settings.mcpUpdateAvailable");
|
||||
return t("settings.mcpReady");
|
||||
});
|
||||
|
||||
const mcpCommand = computed(() => {
|
||||
if (!mcpStatus.value) return "npm install -g @dbx-app/mcp-server@latest";
|
||||
return mcpStatus.value.installed ? mcpStatus.value.update_command : mcpStatus.value.install_command;
|
||||
});
|
||||
|
||||
async function refreshMcpStatus() {
|
||||
if (mcpStatusLoading.value) return;
|
||||
mcpStatusLoading.value = true;
|
||||
mcpStatusError.value = "";
|
||||
try {
|
||||
mcpStatus.value = await checkMcpServerStatus();
|
||||
} catch (e: any) {
|
||||
mcpStatusError.value = e?.message || String(e);
|
||||
} finally {
|
||||
mcpStatusLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function copyMcpText(kind: "install" | "config", value: string) {
|
||||
mcpCopied.value = kind;
|
||||
try {
|
||||
await copyToClipboard(value);
|
||||
} catch {
|
||||
mcpCopied.value = "";
|
||||
return;
|
||||
}
|
||||
window.setTimeout(() => {
|
||||
if (mcpCopied.value === kind) mcpCopied.value = "";
|
||||
}, 1500);
|
||||
}
|
||||
|
||||
// ---------- WebDAV Sync ----------
|
||||
const webdavEndpoint = ref(localStorage.getItem("dbx-webdav-endpoint") || "");
|
||||
const webdavUsername = ref(localStorage.getItem("dbx-webdav-username") || "");
|
||||
|
|
@ -617,6 +687,7 @@ watch(
|
|||
webdavPassword.value = "";
|
||||
await refreshWebDavPasswordStatus();
|
||||
syncAiEditState();
|
||||
if (!isWeb && activeSettingsTab.value === "mcp") void refreshMcpStatus();
|
||||
}
|
||||
},
|
||||
{ immediate: true },
|
||||
|
|
@ -629,6 +700,10 @@ watch(webdavRememberPassword, (val) => {
|
|||
localStorage.setItem("dbx-webdav-remember-password", String(val));
|
||||
});
|
||||
|
||||
watch(activeSettingsTab, (tab) => {
|
||||
if (tab === "mcp" && !mcpStatus.value && !mcpStatusLoading.value) void refreshMcpStatus();
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
void refreshWebDavPasswordStatus();
|
||||
});
|
||||
|
|
@ -1849,6 +1924,123 @@ watch(
|
|||
</div>
|
||||
</section>
|
||||
|
||||
<section v-else-if="activeSettingsTab === 'mcp' && !isWeb" class="flex flex-col gap-5 py-2">
|
||||
<div class="rounded-md border bg-muted/20 p-4">
|
||||
<div class="flex items-start justify-between gap-4">
|
||||
<div class="min-w-0 space-y-2">
|
||||
<div class="flex items-center gap-2">
|
||||
<PackageSearch class="h-4 w-4 text-muted-foreground" />
|
||||
<Label class="text-base">{{ t("settings.mcpTitle") }}</Label>
|
||||
</div>
|
||||
<p class="text-sm text-muted-foreground">{{ t("settings.mcpDescription") }}</p>
|
||||
</div>
|
||||
<Badge
|
||||
variant="outline"
|
||||
class="shrink-0 rounded-md"
|
||||
:class="
|
||||
mcpStatusTone === 'ok'
|
||||
? 'border-green-500/40 text-green-600 dark:text-green-400'
|
||||
: mcpStatusTone === 'warning'
|
||||
? 'border-amber-500/40 text-amber-600 dark:text-amber-400'
|
||||
: 'text-muted-foreground'
|
||||
"
|
||||
>
|
||||
<Loader2 v-if="mcpStatusLoading" class="mr-1 h-3 w-3 animate-spin" />
|
||||
<CheckCircle2 v-else-if="mcpStatusTone === 'ok'" class="mr-1 h-3 w-3" />
|
||||
<AlertTriangle v-else-if="mcpStatusTone === 'warning'" class="mr-1 h-3 w-3" />
|
||||
{{ mcpStatusLabel }}
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid gap-3 sm:grid-cols-2">
|
||||
<div class="rounded-md border p-3">
|
||||
<div class="text-xs font-medium uppercase text-muted-foreground">{{ t("settings.mcpCurrent") }}</div>
|
||||
<div class="mt-2 font-mono text-sm">
|
||||
{{ mcpStatus?.current_version ? `v${mcpStatus.current_version}` : t("settings.mcpVersionMissing") }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="rounded-md border p-3">
|
||||
<div class="text-xs font-medium uppercase text-muted-foreground">{{ t("settings.mcpLatest") }}</div>
|
||||
<div class="mt-2 font-mono text-sm">
|
||||
{{ mcpStatus?.latest_version ? `v${mcpStatus.latest_version}` : t("settings.mcpVersionUnknown") }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="rounded-md border p-3">
|
||||
<div class="text-xs font-medium uppercase text-muted-foreground">Node.js</div>
|
||||
<div class="mt-2 font-mono text-sm">
|
||||
{{ mcpStatus?.node_version || t("settings.mcpVersionUnknown") }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="rounded-md border p-3">
|
||||
<div class="text-xs font-medium uppercase text-muted-foreground">npm</div>
|
||||
<div class="mt-2 font-mono text-sm">
|
||||
{{ mcpStatus?.npm_available ? t("settings.mcpAvailable") : t("settings.mcpUnavailable") }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="mcpStatus?.bin_path" class="space-y-2">
|
||||
<Label>{{ t("settings.mcpBinPath") }}</Label>
|
||||
<div class="rounded-md border bg-muted/20 px-3 py-2 font-mono text-xs text-muted-foreground">
|
||||
{{ mcpStatus.bin_path }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
<Label>{{
|
||||
mcpStatus?.installed ? t("settings.mcpUpdateCommand") : t("settings.mcpInstallCommand")
|
||||
}}</Label>
|
||||
<div class="flex min-w-0 items-center gap-2">
|
||||
<div class="min-w-0 flex-1 rounded-md border bg-background px-3 py-2 font-mono text-xs">
|
||||
{{ mcpCommand }}
|
||||
</div>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="icon"
|
||||
:title="t('common.copy')"
|
||||
@click="copyMcpText('install', mcpCommand)"
|
||||
>
|
||||
<CheckCircle2 v-if="mcpCopied === 'install'" class="h-4 w-4 text-green-500" />
|
||||
<Copy v-else class="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
<Label>{{ t("settings.mcpConfig") }}</Label>
|
||||
<div class="relative rounded-md border bg-background p-3">
|
||||
<pre
|
||||
class="overflow-x-auto whitespace-pre text-xs leading-relaxed"
|
||||
><code>{{ mcpRecommendedConfig }}</code></pre>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="icon"
|
||||
class="absolute right-2 top-2 h-7 w-7"
|
||||
:title="t('common.copy')"
|
||||
@click="copyMcpText('config', mcpRecommendedConfig)"
|
||||
>
|
||||
<CheckCircle2 v-if="mcpCopied === 'config'" class="h-3.5 w-3.5 text-green-500" />
|
||||
<Copy v-else class="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="mcpStatus?.error || mcpStatusError"
|
||||
class="rounded-md border border-amber-500/30 bg-amber-500/10 px-3 py-2 text-xs text-amber-700 dark:text-amber-300"
|
||||
>
|
||||
{{ mcpStatusError || mcpStatus?.error }}
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-2 text-xs text-muted-foreground">
|
||||
<Terminal class="h-3.5 w-3.5" />
|
||||
<span>{{ t("settings.mcpDetectionTiming") }} {{ t("settings.mcpNpmBoundary") }}</span>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section v-else-if="activeSettingsTab === 'security' && isWeb" class="flex flex-col gap-5 py-2">
|
||||
<div class="space-y-3">
|
||||
<Label class="text-base">{{ t("auth.changePassword") }}</Label>
|
||||
|
|
@ -2082,6 +2274,25 @@ watch(
|
|||
</Button>
|
||||
</DialogFooter>
|
||||
|
||||
<DialogFooter
|
||||
v-else-if="activeSettingsTab === 'mcp' && !isWeb"
|
||||
class="mx-0 mb-0 shrink-0 rounded-none border-t border-border/60 bg-transparent px-0 pb-0 pt-3"
|
||||
>
|
||||
<Button variant="outline" @click="emit('update:open', false)">
|
||||
{{ t("common.close") }}
|
||||
</Button>
|
||||
<div class="flex-1" />
|
||||
<Button variant="outline" :disabled="mcpStatusLoading" @click="refreshMcpStatus">
|
||||
<Loader2 v-if="mcpStatusLoading" class="mr-1 h-3 w-3 animate-spin" />
|
||||
<RefreshCw v-else class="mr-1 h-3 w-3" />
|
||||
{{ t("settings.mcpRefresh") }}
|
||||
</Button>
|
||||
<Button variant="outline" @click="openExternalUrl('https://dbxio.com/cn/docs/mcp')">
|
||||
<ExternalLink class="mr-1 h-3 w-3" />
|
||||
{{ t("settings.mcpGuide") }}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
|
||||
<DialogFooter
|
||||
v-else-if="activeSettingsTab === 'security' && isWeb"
|
||||
class="mx-0 mb-0 shrink-0 rounded-none border-t border-border/60 bg-transparent px-0 pb-0 pt-3"
|
||||
|
|
|
|||
|
|
@ -1496,6 +1496,7 @@ export default {
|
|||
snippetsTab: "Snippets",
|
||||
syncTab: "Sync",
|
||||
aiTab: "AI",
|
||||
mcpTab: "MCP",
|
||||
jdbcTab: "JDBC Drivers",
|
||||
securityTab: "Security",
|
||||
aboutTab: "About",
|
||||
|
|
@ -1651,6 +1652,28 @@ export default {
|
|||
jdbcImportSuccess: "Imported {count} driver file(s)",
|
||||
jdbcDeleteSuccess: "Driver removed",
|
||||
jdbcNoDrivers: "No JDBC drivers imported yet.",
|
||||
mcpTitle: "MCP Server",
|
||||
mcpDescription: "Check the DBX MCP Server installation and version used by Claude Code, Cursor, and other agents.",
|
||||
mcpChecking: "Checking",
|
||||
mcpStatusUnknown: "Not checked",
|
||||
mcpStatusError: "Check failed",
|
||||
mcpReady: "Ready",
|
||||
mcpNotInstalled: "Not installed",
|
||||
mcpUpdateAvailable: "Update available",
|
||||
mcpCurrent: "Current version",
|
||||
mcpLatest: "Latest version",
|
||||
mcpVersionMissing: "No global install detected",
|
||||
mcpVersionUnknown: "Unknown",
|
||||
mcpAvailable: "Available",
|
||||
mcpUnavailable: "Unavailable",
|
||||
mcpBinPath: "Command path",
|
||||
mcpInstallCommand: "Install command",
|
||||
mcpUpdateCommand: "Upgrade command",
|
||||
mcpConfig: "Claude Code config",
|
||||
mcpDetectionTiming: "DBX checks automatically when this page opens; use Check again to refresh.",
|
||||
mcpNpmBoundary: "DBX only checks and explains MCP status; installation and upgrades still run through npm.",
|
||||
mcpRefresh: "Check again",
|
||||
mcpGuide: "MCP guide",
|
||||
aboutDescription: "An open-source, lightweight database management tool.",
|
||||
community: "Community",
|
||||
qqGroup: "QQ Group",
|
||||
|
|
|
|||
|
|
@ -1386,6 +1386,7 @@ export default {
|
|||
snippetsTab: "Fragmentos",
|
||||
syncTab: "Sync",
|
||||
aiTab: "IA",
|
||||
mcpTab: "MCP",
|
||||
jdbcTab: "Drivers JDBC",
|
||||
securityTab: "Seguridad",
|
||||
aboutTab: "Acerca de",
|
||||
|
|
@ -1540,6 +1541,28 @@ export default {
|
|||
jdbcImportSuccess: "Se importaron {count} archivo(s) de driver",
|
||||
jdbcDeleteSuccess: "Driver eliminado",
|
||||
jdbcNoDrivers: "Aún no se han importado drivers JDBC.",
|
||||
mcpTitle: "MCP Server",
|
||||
mcpDescription: "Comprueba la instalación y versión de DBX MCP Server para Claude Code, Cursor y otros agentes.",
|
||||
mcpChecking: "Comprobando",
|
||||
mcpStatusUnknown: "Sin comprobar",
|
||||
mcpStatusError: "Error al comprobar",
|
||||
mcpReady: "Listo",
|
||||
mcpNotInstalled: "No instalado",
|
||||
mcpUpdateAvailable: "Actualización disponible",
|
||||
mcpCurrent: "Versión actual",
|
||||
mcpLatest: "Última versión",
|
||||
mcpVersionMissing: "No se detectó instalación global",
|
||||
mcpVersionUnknown: "Desconocida",
|
||||
mcpAvailable: "Disponible",
|
||||
mcpUnavailable: "No disponible",
|
||||
mcpBinPath: "Ruta del comando",
|
||||
mcpInstallCommand: "Comando de instalación",
|
||||
mcpUpdateCommand: "Comando de actualización",
|
||||
mcpConfig: "Configuración de Claude Code",
|
||||
mcpDetectionTiming: "DBX comprueba automáticamente al abrir esta página; usa Comprobar de nuevo para actualizar.",
|
||||
mcpNpmBoundary: "DBX solo comprueba y explica el estado de MCP; la instalación y actualización siguen usando npm.",
|
||||
mcpRefresh: "Comprobar de nuevo",
|
||||
mcpGuide: "Guía MCP",
|
||||
aboutDescription: "Una herramienta de administración de bases de datos liviana y de código abierto.",
|
||||
community: "Comunidad",
|
||||
qqGroup: "Grupo QQ",
|
||||
|
|
|
|||
|
|
@ -1469,6 +1469,7 @@ export default {
|
|||
snippetsTab: "代码片段",
|
||||
syncTab: "同步",
|
||||
aiTab: "AI",
|
||||
mcpTab: "MCP",
|
||||
jdbcTab: "JDBC 驱动",
|
||||
securityTab: "安全",
|
||||
aboutTab: "关于我们",
|
||||
|
|
@ -1612,6 +1613,28 @@ export default {
|
|||
jdbcImportSuccess: "已导入 {count} 个驱动文件",
|
||||
jdbcDeleteSuccess: "驱动已删除",
|
||||
jdbcNoDrivers: "还没有导入 JDBC 驱动。",
|
||||
mcpTitle: "MCP Server",
|
||||
mcpDescription: "检查 Claude Code、Cursor 等编程助手使用的 DBX MCP Server 安装与版本状态。",
|
||||
mcpChecking: "检查中",
|
||||
mcpStatusUnknown: "未检查",
|
||||
mcpStatusError: "检查失败",
|
||||
mcpReady: "可用",
|
||||
mcpNotInstalled: "未安装",
|
||||
mcpUpdateAvailable: "有更新",
|
||||
mcpCurrent: "当前版本",
|
||||
mcpLatest: "最新版本",
|
||||
mcpVersionMissing: "未检测到全局安装",
|
||||
mcpVersionUnknown: "未知",
|
||||
mcpAvailable: "可用",
|
||||
mcpUnavailable: "不可用",
|
||||
mcpBinPath: "命令路径",
|
||||
mcpInstallCommand: "安装命令",
|
||||
mcpUpdateCommand: "升级命令",
|
||||
mcpConfig: "Claude Code 配置",
|
||||
mcpDetectionTiming: "打开此页时会自动检测,点击重新检查可刷新。",
|
||||
mcpNpmBoundary: "DBX 只检测和提示 MCP 状态;安装与升级仍由 npm 完成。",
|
||||
mcpRefresh: "重新检查",
|
||||
mcpGuide: "MCP 指南",
|
||||
aboutDescription: "开源、轻量的数据库管理工具。",
|
||||
community: "社区",
|
||||
qqGroup: "QQ 群",
|
||||
|
|
|
|||
|
|
@ -227,6 +227,7 @@ export const clearHistory = forward("clearHistory");
|
|||
export const deleteHistoryEntry = forward("deleteHistoryEntry");
|
||||
|
||||
// Updates
|
||||
export const checkMcpServerStatus = forward("checkMcpServerStatus");
|
||||
export const checkForUpdates = forward("checkForUpdates");
|
||||
export const getSystemProxyUrl = forward("getSystemProxyUrl");
|
||||
export const getAppVersion = forward("getAppVersion");
|
||||
|
|
@ -256,6 +257,7 @@ export type {
|
|||
WebDavPasswordStatus,
|
||||
WebDavSyncSummary,
|
||||
WebDavDownloadResult,
|
||||
McpServerStatus,
|
||||
UpdateInfo,
|
||||
RedisDatabaseInfo,
|
||||
RedisKeyInfo,
|
||||
|
|
|
|||
|
|
@ -1381,6 +1381,21 @@ export async function checkForUpdates(): Promise<UpdateInfo> {
|
|||
return get("/api/update/check");
|
||||
}
|
||||
|
||||
export async function checkMcpServerStatus(): Promise<import("./tauri").McpServerStatus> {
|
||||
return {
|
||||
installed: false,
|
||||
npm_available: false,
|
||||
node_version: null,
|
||||
current_version: null,
|
||||
latest_version: null,
|
||||
update_available: false,
|
||||
bin_path: null,
|
||||
install_command: "npm install -g @dbx-app/mcp-server@latest",
|
||||
update_command: "npm install -g @dbx-app/mcp-server@latest",
|
||||
error: "MCP Server status is only available in the desktop app.",
|
||||
};
|
||||
}
|
||||
|
||||
export async function getSystemProxyUrl(): Promise<string | null> {
|
||||
return null;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -915,6 +915,23 @@ export interface UpdateInfo {
|
|||
release_notes: string;
|
||||
}
|
||||
|
||||
export interface McpServerStatus {
|
||||
installed: boolean;
|
||||
npm_available: boolean;
|
||||
node_version: string | null;
|
||||
current_version: string | null;
|
||||
latest_version: string | null;
|
||||
update_available: boolean;
|
||||
bin_path: string | null;
|
||||
install_command: string;
|
||||
update_command: string;
|
||||
error: string | null;
|
||||
}
|
||||
|
||||
export async function checkMcpServerStatus(): Promise<McpServerStatus> {
|
||||
return invoke("check_mcp_server_status");
|
||||
}
|
||||
|
||||
export async function checkForUpdates(): Promise<UpdateInfo> {
|
||||
return invoke("check_for_updates");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,105 @@
|
|||
use std::process::Command;
|
||||
use std::time::Duration;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
const MCP_PACKAGE_NAME: &str = "@dbx-app/mcp-server";
|
||||
const MCP_LATEST_URL: &str = "https://registry.npmjs.org/@dbx-app%2fmcp-server/latest";
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct McpServerStatus {
|
||||
pub installed: bool,
|
||||
pub npm_available: bool,
|
||||
pub node_version: Option<String>,
|
||||
pub current_version: Option<String>,
|
||||
pub latest_version: Option<String>,
|
||||
pub update_available: bool,
|
||||
pub bin_path: Option<String>,
|
||||
pub install_command: String,
|
||||
pub update_command: String,
|
||||
pub error: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct NpmLatestPackage {
|
||||
version: String,
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn check_mcp_server_status() -> Result<McpServerStatus, String> {
|
||||
let npm_available = command_success("npm", &["--version"]);
|
||||
let node_version = command_stdout("node", &["--version"]).ok().and_then(first_non_empty_line);
|
||||
let current_version = if npm_available { installed_mcp_version() } else { None };
|
||||
let bin_path = locate_mcp_bin();
|
||||
let latest_version = fetch_latest_mcp_version().await.ok();
|
||||
let update_available = current_version
|
||||
.as_deref()
|
||||
.zip(latest_version.as_deref())
|
||||
.is_some_and(|(current, latest)| dbx_core::update::is_newer_version(latest, current));
|
||||
let error = if npm_available { None } else { Some("npm is not available in PATH.".to_string()) };
|
||||
|
||||
Ok(McpServerStatus {
|
||||
installed: current_version.is_some() || bin_path.is_some(),
|
||||
npm_available,
|
||||
node_version,
|
||||
current_version,
|
||||
latest_version,
|
||||
update_available,
|
||||
bin_path,
|
||||
install_command: format!("npm install -g {MCP_PACKAGE_NAME}@latest"),
|
||||
update_command: format!("npm install -g {MCP_PACKAGE_NAME}@latest"),
|
||||
error,
|
||||
})
|
||||
}
|
||||
|
||||
async fn fetch_latest_mcp_version() -> Result<String, String> {
|
||||
let mut builder = reqwest::Client::builder().timeout(Duration::from_secs(10)).user_agent("dbx-mcp-status-checker");
|
||||
if let Some(proxy_url) = dbx_core::update::system_proxy_url() {
|
||||
let proxy = reqwest::Proxy::all(&proxy_url).map_err(|e| format!("Invalid system proxy URL: {e}"))?;
|
||||
builder = builder.proxy(proxy);
|
||||
}
|
||||
let client = builder.build().map_err(|e| format!("Failed to create HTTP client: {e}"))?;
|
||||
let package = client
|
||||
.get(MCP_LATEST_URL)
|
||||
.send()
|
||||
.await
|
||||
.and_then(|r| r.error_for_status())
|
||||
.map_err(|e| format!("Failed to check MCP Server updates: {e}"))?
|
||||
.json::<NpmLatestPackage>()
|
||||
.await
|
||||
.map_err(|e| format!("Failed to parse MCP Server update response: {e}"))?;
|
||||
Ok(package.version)
|
||||
}
|
||||
|
||||
fn installed_mcp_version() -> Option<String> {
|
||||
let stdout = command_stdout("npm", &["list", "-g", MCP_PACKAGE_NAME, "--json", "--depth=0"]).ok()?;
|
||||
let value = serde_json::from_str::<serde_json::Value>(&stdout).ok()?;
|
||||
value
|
||||
.get("dependencies")
|
||||
.and_then(|dependencies| dependencies.get(MCP_PACKAGE_NAME))
|
||||
.and_then(|package| package.get("version"))
|
||||
.and_then(|version| version.as_str())
|
||||
.map(ToOwned::to_owned)
|
||||
}
|
||||
|
||||
fn locate_mcp_bin() -> Option<String> {
|
||||
let (command, args): (&str, &[&str]) =
|
||||
if cfg!(windows) { ("where", &["dbx-mcp-server"]) } else { ("which", &["dbx-mcp-server"]) };
|
||||
command_stdout(command, args).ok().and_then(first_non_empty_line)
|
||||
}
|
||||
|
||||
fn command_success(command: &str, args: &[&str]) -> bool {
|
||||
Command::new(command).args(args).output().is_ok_and(|output| output.status.success())
|
||||
}
|
||||
|
||||
fn command_stdout(command: &str, args: &[&str]) -> Result<String, String> {
|
||||
let output = Command::new(command).args(args).output().map_err(|e| e.to_string())?;
|
||||
if !output.status.success() {
|
||||
return Err(String::from_utf8_lossy(&output.stderr).trim().to_string());
|
||||
}
|
||||
Ok(String::from_utf8_lossy(&output.stdout).trim().to_string())
|
||||
}
|
||||
|
||||
fn first_non_empty_line(value: String) -> Option<String> {
|
||||
value.lines().map(str::trim).find(|line| !line.is_empty()).map(ToOwned::to_owned)
|
||||
}
|
||||
|
|
@ -12,6 +12,7 @@ pub mod deep_link;
|
|||
pub mod external_db;
|
||||
pub mod external_sql;
|
||||
pub mod history;
|
||||
pub mod mcp;
|
||||
pub mod mcp_bridge;
|
||||
pub mod mongo_cmd;
|
||||
pub mod plugins;
|
||||
|
|
|
|||
|
|
@ -489,6 +489,7 @@ pub fn run() {
|
|||
commands::history::load_history,
|
||||
commands::history::clear_history,
|
||||
commands::history::delete_history_entry,
|
||||
commands::mcp::check_mcp_server_status,
|
||||
commands::update::check_for_updates,
|
||||
commands::update::get_system_proxy_url,
|
||||
commands::transfer::start_transfer,
|
||||
|
|
|
|||
Loading…
Reference in New Issue