Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 15 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -41,6 +47,14 @@ brew install mcpm
pipx install mcpm
```

### 🪄 uv tool

```bash
uv tool install mcpm
```

## More Installation Methods

### 🐍 pip

```bash
Expand All @@ -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

Expand Down
22 changes: 15 additions & 7 deletions README.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,13 @@ MCPM 是一个开源的服务和命令行界面(CLI),用于管理模型上下

## 🚀 快速安装

选择您喜欢的安装方式:
### 🔄 Shell 脚本(一行命令)

```bash
curl -sSL https://mcpm.sh/install | bash
```

或选择您喜欢的安装方式:

### 🍺 Homebrew

Expand All @@ -41,6 +47,14 @@ brew install mcpm
pipx install mcpm
```

### 🪄 uv tool

```bash
uv tool install mcpm
```

## 其他安装方式

### 🐍 pip

```bash
Expand All @@ -55,12 +69,6 @@ pip install mcpm
x install mcpm.sh
```

### 🔄 Shell 脚本(一行命令)

```bash
curl -sSL https://mcpm.sh/install | bash
```

## 🔎 概述

MCPM 简化了 MCP 服务器的安装、配置和管理,以及它们在不同应用程序(客户端)中的配置。主要功能包括:
Expand Down
46 changes: 18 additions & 28 deletions scripts/prepare.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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 = []
Expand All @@ -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}) {{
Expand All @@ -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()
Expand All @@ -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}")
Expand Down Expand Up @@ -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
Expand All @@ -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 <source_dir> <target_dir> [--skip-stars]")
error_exit("Usage: prepare.py <source_dir> <target_dir> [--skip-stars]")

source_dir = Path(sys.argv[1])
target_dir = Path(sys.argv[2])
Expand Down
4 changes: 2 additions & 2 deletions src/mcpm/clients/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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:
Expand Down
2 changes: 1 addition & 1 deletion src/mcpm/clients/managers/fiveire.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
4 changes: 2 additions & 2 deletions src/mcpm/commands/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
2 changes: 1 addition & 1 deletion src/mcpm/commands/server_operations/add.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
16 changes: 15 additions & 1 deletion src/mcpm/monitor/duckdb.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down
4 changes: 2 additions & 2 deletions src/mcpm/profile/profile_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand All @@ -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:
Expand Down
2 changes: 1 addition & 1 deletion src/mcpm/router/watcher.py
Original file line number Diff line number Diff line change
Expand Up @@ -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}")
Expand Down
4 changes: 2 additions & 2 deletions src/mcpm/utils/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -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}")
Expand All @@ -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]:
Expand Down
4 changes: 2 additions & 2 deletions src/mcpm/utils/repository.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")

Expand All @@ -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}")
Expand Down