diff --git a/README.md b/README.md index e36153d3..c363a890 100644 --- a/README.md +++ b/README.md @@ -27,7 +27,13 @@ MCPM is an open source service and a CLI package management tool for MCP servers ## 🚀 Quick Installation -Choose your preferred installation method: +### 🔄 Shell Script (One-liner) + +```bash +curl -sSL https://mcpm.sh/install | bash +``` + +Or choose your preferred installation method: ### 🍺 Homebrew @@ -41,6 +47,14 @@ brew install mcpm pipx install mcpm ``` +### 🪄 uv tool + +```bash +uv tool install mcpm +``` + +## More Installation Methods + ### 🐍 pip ```bash @@ -55,11 +69,6 @@ If you are a user of [x-cmd](https://x-cmd.com), you can run: x install mcpm.sh ``` -### 🔄 Shell Script (One-liner) - -```bash -curl -sSL https://mcpm.sh/install | bash -``` ## 🔎 Overview diff --git a/README.zh-CN.md b/README.zh-CN.md index fb395868..fff3239e 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -27,7 +27,13 @@ MCPM 是一个开源的服务和命令行界面(CLI),用于管理模型上下 ## 🚀 快速安装 -选择您喜欢的安装方式: +### 🔄 Shell 脚本(一行命令) + +```bash +curl -sSL https://mcpm.sh/install | bash +``` + +或选择您喜欢的安装方式: ### 🍺 Homebrew @@ -41,6 +47,14 @@ brew install mcpm pipx install mcpm ``` +### 🪄 uv tool + +```bash +uv tool install mcpm +``` + +## 其他安装方式 + ### 🐍 pip ```bash @@ -55,12 +69,6 @@ pip install mcpm x install mcpm.sh ``` -### 🔄 Shell 脚本(一行命令) - -```bash -curl -sSL https://mcpm.sh/install | bash -``` - ## 🔎 概述 MCPM 简化了 MCP 服务器的安装、配置和管理,以及它们在不同应用程序(客户端)中的配置。主要功能包括: diff --git a/scripts/prepare.py b/scripts/prepare.py index 2dfe5899..23174041 100755 --- a/scripts/prepare.py +++ b/scripts/prepare.py @@ -54,8 +54,7 @@ def load_manifest(manifest_path: Path) -> Dict[str, Any]: except jsonschema.exceptions.ValidationError: # If validation fails, we continue but log a warning # This allows the site to build even with some schema issues - print( - f"⚠️ Warning: {manifest_path} does not fully conform to the schema") + print(f"⚠️ Warning: {manifest_path} does not fully conform to the schema") return manifest except json.JSONDecodeError as e: @@ -92,9 +91,12 @@ def extract_github_repos(server_manifests: List[Path]) -> Dict[str, str]: # Handle both string and dictionary repository formats if isinstance(repo_url, str) and repo_url.startswith("https://github.com/"): github_repos[server_name] = repo_url - elif (isinstance(repo_url, dict) and "url" in repo_url and - isinstance(repo_url["url"], str) and - repo_url["url"].startswith("https://github.com/")): + elif ( + isinstance(repo_url, dict) + and "url" in repo_url + and isinstance(repo_url["url"], str) + and repo_url["url"].startswith("https://github.com/") + ): github_repos[server_name] = repo_url["url"] return github_repos @@ -130,7 +132,7 @@ def fetch_github_stars_batch(repo_urls: List[str]) -> Dict[str, int]: # Process repositories in batches for batch_start in range(0, len(repos), BATCH_SIZE): - batch = repos[batch_start:batch_start + BATCH_SIZE] + batch = repos[batch_start : batch_start + BATCH_SIZE] # Construct GraphQL query query_parts = [] @@ -147,8 +149,7 @@ def fetch_github_stars_batch(repo_urls: List[str]) -> Dict[str, int]: variables[f"repo{i}"] = repo # Join the query parts with proper line length - variable_defs = ", ".join(f"$owner{i}: String!, $repo{i}: String!" - for i in range(len(batch))) + variable_defs = ", ".join(f"$owner{i}: String!, $repo{i}: String!" for i in range(len(batch))) query_body = " ".join(query_parts) query = f"""query ({variable_defs}) {{ @@ -157,23 +158,16 @@ def fetch_github_stars_batch(repo_urls: List[str]) -> Dict[str, int]: # Send GraphQL request try: - response = requests.post( - GITHUB_API_URL, - headers=headers, - json={"query": query, "variables": variables} - ) + response = requests.post(GITHUB_API_URL, headers=headers, json={"query": query, "variables": variables}) # Check for errors if response.status_code != 200: if response.status_code == 401: - print( - "⚠️ GitHub API authentication failed. Set GITHUB_TOKEN for higher rate limits.") + print("⚠️ GitHub API authentication failed. Set GITHUB_TOKEN for higher rate limits.") elif response.status_code == 403: - print( - "⚠️ GitHub API rate limit exceeded. Set GITHUB_TOKEN for higher rate limits.") + print("⚠️ GitHub API rate limit exceeded. Set GITHUB_TOKEN for higher rate limits.") else: - print( - f"⚠️ GitHub API request failed: status {response.status_code}") + print(f"⚠️ GitHub API request failed: status {response.status_code}") continue data = response.json() @@ -191,16 +185,13 @@ def fetch_github_stars_batch(repo_urls: List[str]) -> Dict[str, int]: star_count = data["data"][repo_key]["stargazerCount"] stars[url] = star_count if url.startswith("https://github.com/"): - returned_parts = url.replace( - "https://github.com/", "").split("/") + returned_parts = url.replace("https://github.com/", "").split("/") if len(returned_parts) >= 2: returned_owner, returned_repo = returned_parts[0], returned_parts[1] if owner != returned_owner: - print( - f"⚠️owner mismatch:: {owner} != {returned_owner}") + print(f"⚠️owner mismatch:: {owner} != {returned_owner}") if repo != returned_repo: - print( - f"⚠️repo mismatch:: {repo} != {returned_repo}") + print(f"⚠️repo mismatch:: {repo} != {returned_repo}") except Exception as e: print(f"⚠️ Error fetching GitHub stars for batch: {e}") @@ -249,7 +240,7 @@ def generate_servers_json(server_manifests: List[Path], output_path: Path) -> Di servers_data[server_name] = manifest # Write servers.json - with open(output_path, "w") as f: + with open(output_path, "w", encoding="utf-8") as f: json.dump(servers_data, f, indent=2) return servers_data @@ -267,8 +258,7 @@ def generate_stars_json(stars: Dict[str, int], output_path: Path) -> None: def main() -> None: """Main function to prepare site data""" if len(sys.argv) < 3: - error_exit( - "Usage: prepare.py [--skip-stars]") + error_exit("Usage: prepare.py [--skip-stars]") source_dir = Path(sys.argv[1]) target_dir = Path(sys.argv[2]) diff --git a/src/mcpm/clients/base.py b/src/mcpm/clients/base.py index bb4280e9..a9f2134d 100644 --- a/src/mcpm/clients/base.py +++ b/src/mcpm/clients/base.py @@ -218,7 +218,7 @@ def _load_config(self) -> Dict[str, Any]: return empty_config try: - with open(self.config_path, "r") as f: + with open(self.config_path, "r", encoding="utf-8") as f: config = json.load(f) # Ensure mcpServers section exists if self.configure_key_name not in config: @@ -252,7 +252,7 @@ def _save_config(self, config: Dict[str, Any]) -> bool: # Create directory if it doesn't exist os.makedirs(os.path.dirname(self.config_path), exist_ok=True) - with open(self.config_path, "w") as f: + with open(self.config_path, "w", encoding="utf-8") as f: json.dump(config, f, indent=2) return True except Exception as e: diff --git a/src/mcpm/clients/managers/fiveire.py b/src/mcpm/clients/managers/fiveire.py index eb91cda3..3a9510d5 100644 --- a/src/mcpm/clients/managers/fiveire.py +++ b/src/mcpm/clients/managers/fiveire.py @@ -62,7 +62,7 @@ def _load_config(self) -> Dict[str, Any]: return empty_config try: - with open(self.config_path, "r") as f: + with open(self.config_path, "r", encoding="utf-8") as f: config = json.load(f) # Ensure servers section exists if self.configure_key_name not in config: diff --git a/src/mcpm/commands/client.py b/src/mcpm/commands/client.py index 6f2ac363..b6dbde42 100644 --- a/src/mcpm/commands/client.py +++ b/src/mcpm/commands/client.py @@ -176,7 +176,7 @@ def edit_client(): # Write the template to file try: - with open(config_path, "w") as f: + with open(config_path, "w", encoding="utf-8") as f: json.dump(basic_config, f, indent=2) console.print("[green]Successfully created config file![/]\n") config_exists = True @@ -187,7 +187,7 @@ def edit_client(): # Show the current configuration if it exists if config_exists: try: - with open(config_path, "r") as f: + with open(config_path, "r", encoding="utf-8") as f: config_content = f.read() # Display the content diff --git a/src/mcpm/commands/server_operations/add.py b/src/mcpm/commands/server_operations/add.py index 04c0e787..6dbae323 100644 --- a/src/mcpm/commands/server_operations/add.py +++ b/src/mcpm/commands/server_operations/add.py @@ -262,7 +262,7 @@ def add(server_name, force=False, alias=None, target: str | None = None): # Save metadata to server directory progress.add_task("Saving server metadata...", total=None) metadata_path = os.path.join(server_dir, "metadata.json") - with open(metadata_path, "w") as f: + with open(metadata_path, "w", encoding="utf-8") as f: json.dump(server_metadata, f, indent=2) # Configure the server diff --git a/src/mcpm/monitor/duckdb.py b/src/mcpm/monitor/duckdb.py index 2b79e0b0..4eedb1b3 100644 --- a/src/mcpm/monitor/duckdb.py +++ b/src/mcpm/monitor/duckdb.py @@ -8,7 +8,18 @@ from datetime import datetime from typing import Any, Dict, Optional, Union -import duckdb +try: + import duckdb +except ImportError as e: + duckdb = None + if "DLL load failed while importing duckdb" in str(e): + print("The DuckDB Python package requires the Microsoft Visual C++ Redistributable. ") + print( + "Please install it from: https://learn.microsoft.com/en-us/cpp/windows/latest-supported-vc-redist?view=msvc-170" + ) + print("See https://duckdb.org/docs/installation/?version=stable&environment=python for more information.") + else: + raise from mcpm.monitor.base import AccessEventType, AccessMonitor, MCPEvent, Pagination, QueryEventResponse from mcpm.utils.config import ConfigManager @@ -57,6 +68,9 @@ async def initialize_storage(self) -> bool: def _initialize_storage_impl(self) -> bool: """Internal implementation of storage initialization.""" + if duckdb is None: + print("DuckDB is not available.") + return False try: # Create the directory if it doesn't exist os.makedirs(os.path.dirname(self.db_path), exist_ok=True) diff --git a/src/mcpm/profile/profile_config.py b/src/mcpm/profile/profile_config.py index 764af818..6fb62877 100644 --- a/src/mcpm/profile/profile_config.py +++ b/src/mcpm/profile/profile_config.py @@ -17,7 +17,7 @@ def __init__(self, profile_path: str = DEFAULT_PROFILE_PATH): def _load_profiles(self) -> Dict[str, list[ServerConfig]]: if not os.path.exists(self.profile_path): return {} - with open(self.profile_path, "r") as f: + with open(self.profile_path, "r", encoding="utf-8") as f: profiles = json.load(f) or {} return { name: [TypeAdapter(ServerConfig).validate_python(config) for config in configs] @@ -26,7 +26,7 @@ def _load_profiles(self) -> Dict[str, list[ServerConfig]]: def _save_profiles(self) -> None: profile_info = {name: [config.model_dump() for config in configs] for name, configs in self._profiles.items()} - with open(self.profile_path, "w") as f: + with open(self.profile_path, "w", encoding="utf-8") as f: json.dump(profile_info, f, indent=2) def new_profile(self, profile_name: str) -> bool: diff --git a/src/mcpm/router/watcher.py b/src/mcpm/router/watcher.py index b4e12892..566b4c96 100644 --- a/src/mcpm/router/watcher.py +++ b/src/mcpm/router/watcher.py @@ -52,7 +52,7 @@ async def _reload(self): def _validate_config(self): try: - with open(self.config_path, "r") as f: + with open(self.config_path, "r", encoding="utf-8") as f: _ = json.load(f) except json.JSONDecodeError: logger.error(f"Error parsing config file: {self.config_path}") diff --git a/src/mcpm/utils/config.py b/src/mcpm/utils/config.py index 77a7c466..02d43976 100644 --- a/src/mcpm/utils/config.py +++ b/src/mcpm/utils/config.py @@ -46,7 +46,7 @@ def _load_config(self) -> None: """Load configuration from file or create default""" if os.path.exists(self.config_path): try: - with open(self.config_path, "r") as f: + with open(self.config_path, "r", encoding="utf-8") as f: self._config = json.load(f) except json.JSONDecodeError: logger.error(f"Error parsing config file: {self.config_path}") @@ -62,7 +62,7 @@ def _default_config(self) -> Dict[str, Any]: def _save_config(self) -> None: """Save current configuration to file""" - with open(self.config_path, "w") as f: + with open(self.config_path, "w", encoding="utf-8") as f: json.dump(self._config, f, indent=2) def get_config(self) -> Dict[str, Any]: diff --git a/src/mcpm/utils/repository.py b/src/mcpm/utils/repository.py index f250e6ad..5c6a0011 100644 --- a/src/mcpm/utils/repository.py +++ b/src/mcpm/utils/repository.py @@ -43,7 +43,7 @@ def _load_cache_from_file(self) -> None: """ if os.path.exists(self.cache_file): try: - with open(self.cache_file, "r") as f: + with open(self.cache_file, "r", encoding="utf-8") as f: cache_data = json.load(f) self.servers_cache = cache_data.get("servers") @@ -66,7 +66,7 @@ def _save_cache_to_file(self) -> None: try: cache_data = {"servers": self.servers_cache, "last_refresh": self.last_refresh.isoformat()} - with open(self.cache_file, "w") as f: + with open(self.cache_file, "w", encoding="utf-8") as f: json.dump(cache_data, f, indent=2) logger.debug(f"Saved servers cache to {self.cache_file}")