diff --git a/package-lock.json b/package-lock.json
index 827f342..a591547 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "shardx-launcher",
- "version": "0.1.5",
+ "version": "0.1.11",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "shardx-launcher",
- "version": "0.1.5",
+ "version": "0.1.11",
"dependencies": {
"@tauri-apps/api": "^2",
"@tauri-apps/plugin-dialog": "^2.7.1",
diff --git a/package.json b/package.json
index 1f05384..1dcf5eb 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
{
"name": "shardx-launcher",
"private": true,
- "version": "0.1.10",
+ "version": "0.1.11",
"type": "module",
"scripts": {
"dev": "vite",
diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock
index 67562e6..a1975cb 100644
--- a/src-tauri/Cargo.lock
+++ b/src-tauri/Cargo.lock
@@ -3863,7 +3863,7 @@ dependencies = [
[[package]]
name = "shardx-launcher"
-version = "0.1.10"
+version = "0.1.11"
dependencies = [
"aes",
"aes-gcm",
diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml
index 2a6a707..131bbd5 100644
--- a/src-tauri/Cargo.toml
+++ b/src-tauri/Cargo.toml
@@ -1,6 +1,6 @@
[package]
name = "shardx-launcher"
-version = "0.1.10"
+version = "0.1.11"
description = "A Tauri App"
authors = ["you"]
edition = "2021"
diff --git a/src-tauri/src/launch.rs b/src-tauri/src/launch.rs
index 0b7ac92..ec022c7 100644
--- a/src-tauri/src/launch.rs
+++ b/src-tauri/src/launch.rs
@@ -200,7 +200,6 @@ pub async fn launch_profile(
cmd.stdout(Stdio::null()).stderr(Stdio::null());
#[cfg(target_os = "windows")]
{
- use std::os::windows::process::CommandExt;
// 0x08000000 = CREATE_NO_WINDOW — suppress the brief console flash
// when a Tauri GUI app spawns the engine binary.
cmd.creation_flags(0x08000000);
diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs
index 66bcabd..7a1b6df 100644
--- a/src-tauri/src/lib.rs
+++ b/src-tauri/src/lib.rs
@@ -48,10 +48,75 @@ pub fn notify_store_changed(kind: &str) {
/// Download MCP server source into `
/mcp`; user manages registration.
#[tauri::command]
async fn mcp_download(dir: String) -> Result {
- mcp_setup::download_mcp(std::path::Path::new(&dir))
+ let path = mcp_setup::download_mcp(std::path::Path::new(&dir))
.await
- .map(|p| p.display().to_string())
- .map_err(|e| e.to_string())
+ .map_err(|e| e.to_string())?;
+ let path_string = path.display().to_string();
+ let mut s = settings::load().map_err(|e| e.to_string())?;
+ s.mcp_path = Some(path_string.clone());
+ settings::save(&s).map_err(|e| e.to_string())?;
+ notify_store_changed("settings");
+ Ok(path_string)
+}
+
+#[tauri::command]
+fn mcp_set_path(dir: String) -> Result {
+ let path = mcp_setup::resolve_mcp_dir(std::path::Path::new(&dir))
+ .ok_or_else(|| "Selected folder is not a ShardX MCP server folder.".to_string())?;
+ let path = path.display().to_string();
+ let mut s = settings::load().map_err(|e| e.to_string())?;
+ s.mcp_path = Some(path.clone());
+ settings::save(&s).map_err(|e| e.to_string())?;
+ notify_store_changed("settings");
+ Ok(serde_json::json!({
+ "path": path,
+ "installed": true,
+ "state": "ready",
+ "message": "Using existing MCP server files.",
+ }))
+}
+
+#[tauri::command]
+fn mcp_status() -> Result {
+ let mut s = settings::load().map_err(|e| e.to_string())?;
+ if let Some(path) = &s.mcp_path {
+ let installed = mcp_setup::is_mcp_dir(std::path::Path::new(path));
+ if installed {
+ return Ok(serde_json::json!({
+ "path": path,
+ "installed": true,
+ "state": "ready",
+ "message": "MCP server files are downloaded.",
+ }));
+ }
+ }
+
+ if let Some(path) = mcp_setup::find_existing_mcp() {
+ let path = path.display().to_string();
+ s.mcp_path = Some(path.clone());
+ settings::save(&s).map_err(|e| e.to_string())?;
+ return Ok(serde_json::json!({
+ "path": path,
+ "installed": true,
+ "state": "ready",
+ "message": "Found existing MCP server files from a previous download.",
+ }));
+ }
+
+ Ok(match s.mcp_path {
+ Some(path) => serde_json::json!({
+ "path": path,
+ "installed": false,
+ "state": "missing",
+ "message": "Saved MCP folder is missing index.js or package.json.",
+ }),
+ None => serde_json::json!({
+ "path": null,
+ "installed": false,
+ "state": "not_downloaded",
+ "message": "MCP server has not been downloaded yet.",
+ }),
+ })
}
// ---- Profiles ----
@@ -1203,6 +1268,8 @@ pub fn run() {
cookies_export_to_file,
cookies_import,
mcp_download,
+ mcp_set_path,
+ mcp_status,
runtime::runtime_status,
runtime::runtime_install,
runtime::launcher_update_check,
diff --git a/src-tauri/src/mcp_setup.rs b/src-tauri/src/mcp_setup.rs
index c28ee79..c6106be 100644
--- a/src-tauri/src/mcp_setup.rs
+++ b/src-tauri/src/mcp_setup.rs
@@ -19,9 +19,22 @@ const MCP_ARCHIVE_URL: &str =
/// Top-level directory inside the tarball that wraps the actual files.
const MCP_TOP_DIR: &str = "ShardX-MCP";
+fn download_destination(dir: &Path) -> PathBuf {
+ // The picker text says "choose a folder" while the status box shows the
+ // actual `mcp` folder. If the user repairs by selecting that shown folder,
+ // update it in place instead of creating `mcp/mcp`.
+ if is_mcp_dir(dir) || dir.file_name().is_some_and(|name| name == "mcp") {
+ dir.to_path_buf()
+ } else {
+ dir.join("mcp")
+ }
+}
+
/// Download the MCP server into `/mcp` and return that path.
+///
+/// If `` already is the MCP folder, repair/update it in place.
pub async fn download_mcp(dir: &Path) -> Result {
- let dest = dir.join("mcp");
+ let dest = download_destination(dir);
let bytes = reqwest::get(MCP_ARCHIVE_URL)
.await
.context("download MCP archive")?
@@ -64,3 +77,82 @@ pub async fn download_mcp(dir: &Path) -> Result {
}
Ok(dest)
}
+
+pub fn is_mcp_dir(dir: &Path) -> bool {
+ if !dir.join("index.js").is_file() || !dir.join("package.json").is_file() {
+ return false;
+ }
+ std::fs::read_to_string(dir.join("package.json"))
+ .map(|s| s.contains("\"shardx-mcp\""))
+ .unwrap_or(false)
+}
+
+pub fn resolve_mcp_dir(dir: &Path) -> Option {
+ if is_mcp_dir(dir) {
+ return Some(dir.to_path_buf());
+ }
+ let nested = dir.join("mcp");
+ is_mcp_dir(&nested).then_some(nested)
+}
+
+pub fn find_existing_mcp() -> Option {
+ let mut candidates = Vec::new();
+ if let Some(dir) = dirs::document_dir() {
+ candidates.extend([
+ dir.join("MCP").join("ShardBrowser").join("mcp"),
+ dir.join("MCP").join("mcp"),
+ dir.join("ShardBrowser").join("mcp"),
+ dir.join("GitHub").join("ShardBrowser").join("mcp"),
+ dir.join("mcp"),
+ ]);
+ }
+ if let Some(dir) = dirs::download_dir() {
+ candidates.push(dir.join("mcp"));
+ }
+ if let Some(dir) = dirs::desktop_dir() {
+ candidates.push(dir.join("mcp"));
+ }
+ candidates.into_iter().find_map(|p| resolve_mcp_dir(&p))
+}
+
+#[cfg(test)]
+mod tests {
+ use super::{download_destination, is_mcp_dir};
+
+ #[test]
+ fn detects_downloaded_mcp_folder() {
+ let dir = std::env::temp_dir().join(format!("shardx-mcp-test-{}", std::process::id()));
+ let _ = std::fs::remove_dir_all(&dir);
+ std::fs::create_dir_all(&dir).unwrap();
+ std::fs::write(dir.join("index.js"), "").unwrap();
+ std::fs::write(dir.join("package.json"), r#"{ "name": "shardx-mcp" }"#).unwrap();
+
+ assert!(is_mcp_dir(&dir));
+ assert_eq!(super::resolve_mcp_dir(&dir).as_deref(), Some(dir.as_path()));
+
+ let _ = std::fs::remove_dir_all(&dir);
+ }
+
+ #[test]
+ fn download_destination_avoids_nested_mcp_when_repairing() {
+ let base = std::env::temp_dir().join(format!(
+ "shardx-mcp-destination-test-{}",
+ std::process::id()
+ ));
+ let normal_parent = base.join("ShardBrowser");
+ let existing_mcp = normal_parent.join("mcp");
+ let _ = std::fs::remove_dir_all(&base);
+ std::fs::create_dir_all(&existing_mcp).unwrap();
+ std::fs::write(existing_mcp.join("index.js"), "").unwrap();
+ std::fs::write(
+ existing_mcp.join("package.json"),
+ r#"{ "name": "shardx-mcp" }"#,
+ )
+ .unwrap();
+
+ assert_eq!(download_destination(&normal_parent), existing_mcp);
+ assert_eq!(download_destination(&existing_mcp), existing_mcp);
+
+ let _ = std::fs::remove_dir_all(&base);
+ }
+}
diff --git a/src-tauri/src/settings.rs b/src-tauri/src/settings.rs
index ad04929..a8e9421 100644
--- a/src-tauri/src/settings.rs
+++ b/src-tauri/src/settings.rs
@@ -33,6 +33,9 @@ pub struct Settings {
/// (see `ensure_secret`); rotating it invalidates issued tokens.
#[serde(default)]
pub api_secret: String,
+ /// Last downloaded MCP server folder, if any.
+ #[serde(default)]
+ pub mcp_path: Option,
}
fn default_theme() -> String {
@@ -63,6 +66,7 @@ pub fn load() -> Result {
api_enabled: default_api_enabled(),
api_port: default_api_port(),
api_secret: String::new(),
+ mcp_path: None,
});
}
let body = fs::read_to_string(&path)?;
diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json
index 9d3cbd0..423d96d 100644
--- a/src-tauri/tauri.conf.json
+++ b/src-tauri/tauri.conf.json
@@ -1,7 +1,7 @@
{
"$schema": "https://schema.tauri.app/config/2",
"productName": "ShardX Launcher",
- "version": "0.1.10",
+ "version": "0.1.11",
"identifier": "com.shardx.launcher",
"build": {
"beforeDevCommand": "npm run dev",
diff --git a/src/App.css b/src/App.css
index 854018e..2d0e7c5 100644
--- a/src/App.css
+++ b/src/App.css
@@ -282,6 +282,11 @@ textarea { resize: vertical; min-height: 60px; line-height: 1.5; }
}
.side-auto-btn:hover { border-color: var(--accent); }
.side-auto-btn:disabled { opacity: .5; cursor: default; }
+.side-auto-btn-ok {
+ background: #4ade8014;
+ color: var(--ok);
+}
+.side-auto-btn-ok:hover { border-color: #4ade8055; }
/* Theme switch — a 2-segment pill (Light | Dark) sitting just above
the user pill. The active half gets the accent fill; clicking
@@ -332,6 +337,7 @@ textarea { resize: vertical; min-height: 60px; line-height: 1.5; }
.version-pill .shard-mini { color: var(--accent); flex: 0 0 auto; }
.version-pill-text { display: flex; flex-direction: column; min-width: 0; }
.version-pill-current { font-size: 12px; font-weight: 500; }
+.version-pill-label { color: var(--accent); font-size: 10.5px; font-weight: 600; }
.version-pill-sub { font-size: 10.5px; color: var(--tx-4); }
.version-pill.update-available {
@@ -1118,6 +1124,44 @@ textarea { resize: vertical; min-height: 60px; line-height: 1.5; }
}
.copy-icon:hover { color: var(--tx-1); background: var(--bg-1); }
+.settings-steps {
+ margin: 10px 0 12px;
+ padding-left: 20px;
+ color: var(--tx-2);
+ font-size: 12px;
+ line-height: 1.5;
+}
+.settings-steps code { color: var(--tx-1); }
+.mcp-status {
+ margin-top: 10px;
+ padding: 9px 10px;
+ border: 1px solid var(--bd-1);
+ border-radius: var(--radius);
+ background: var(--bg-2);
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 10px;
+ font-size: 12px;
+}
+.mcp-status strong { color: var(--tx-1); white-space: nowrap; }
+.mcp-status span { color: var(--tx-3); min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
+.mcp-status-ready { border-color: #4ade8033; }
+.mcp-status-ready strong { color: var(--ok); }
+.mcp-status-missing { border-color: #f59e0b55; }
+.mcp-status-missing strong { color: #fbbf24; }
+.mcp-setup-box {
+ margin-top: 10px;
+ padding: 10px;
+ border: 1px solid var(--bd-1);
+ border-radius: var(--radius);
+ background: var(--bg-2);
+}
+.mcp-setup-actions {
+ margin-top: 10px;
+ flex-wrap: wrap;
+}
+
/* Confirm modal */
.dialog-confirm { max-width: 440px; }
.confirm-msg { color: var(--tx-2); font-size: 13px; line-height: 1.5; margin: 0; }
diff --git a/src/App.tsx b/src/App.tsx
index 585118e..3953eae 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -16,6 +16,7 @@ function detectHostOs(): "macOS" | "Windows" | "Linux" {
return "macOS";
}
const HOST_OS = detectHostOs();
+const CUSTOM_BUILD_LABEL = "custom build";
// OS clipboard via Tauri plugin (webview navigator.clipboard throws).
const clip = {
@@ -280,6 +281,7 @@ type Settings = {
api_enabled?: boolean;
api_port?: number;
api_secret?: string;
+ mcp_path?: string | null;
};
type ApiInfo = {
enabled: boolean;
@@ -287,6 +289,12 @@ type ApiInfo = {
base_url: string;
token: string;
};
+type McpStatus = {
+ path: string | null;
+ installed: boolean;
+ state: "not_downloaded" | "ready" | "missing";
+ message: string;
+};
type Section = "browsers" | "proxies" | "proxyshard" | "fingerprints" | "settings";
/// Library fingerprint backing the editor GPU select; payload supplies the coherent base.
@@ -701,22 +709,15 @@ function Sidebar({
// Automation/MCP quick widget (fills the sidebar's lower space).
const [autoUrl, setAutoUrl] = useState("");
- const [mcpBusy, setMcpBusy] = useState(false);
+ const [mcp, setMcp] = useState(null);
+ const refreshMcp = () => invoke("mcp_status").then(setMcp).catch(() => {});
useEffect(() => {
invoke<{ base_url: string; enabled: boolean }>("api_info")
.then((i) => setAutoUrl(i.enabled ? i.base_url : ""))
.catch(() => {});
+ refreshMcp();
}, []);
- const downloadMcp = async () => {
- const dir = await open({ directory: true, title: "Where to download the MCP server" });
- if (typeof dir !== "string") return;
- setMcpBusy(true);
- try {
- const p = await invoke("mcp_download", { dir });
- toast.ok(`MCP downloaded to ${p}`);
- } catch (e) { toast.err("MCP download failed: " + String(e)); }
- finally { setMcpBusy(false); }
- };
+ useStoreChanged(refreshMcp);
return (