Skip to content

Commit f422141

Browse files
committed
Add repository cache
1 parent a6a34e5 commit f422141

File tree

3 files changed

+177
-73
lines changed

3 files changed

+177
-73
lines changed

src/mcpm/cli.py

Lines changed: 72 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -6,61 +6,66 @@
66
from rich.console import Console
77
from rich.table import Table
88

9-
from mcpm.utils.config import ConfigManager
10-
119
from mcpm import __version__
1210
from mcpm.commands import (
13-
search,
14-
remove,
15-
list,
16-
edit,
17-
stash,
18-
pop,
11+
add,
1912
client,
13+
config,
14+
edit,
2015
inspector,
21-
add,
16+
list,
17+
pop,
18+
remove,
19+
search,
20+
stash,
2221
)
22+
from mcpm.utils.config import ConfigManager
2323

2424
console = Console()
2525
config_manager = ConfigManager()
2626

2727
# Set -h as an alias for --help but we'll handle it ourselves
2828
CONTEXT_SETTINGS = dict(help_option_names=[])
2929

30+
3031
@click.group(context_settings=CONTEXT_SETTINGS, invoke_without_command=True)
31-
@click.option('-h', '--help', 'help_flag', is_flag=True, help='Show this message and exit.')
32+
@click.option(
33+
"-h", "--help", "help_flag", is_flag=True, help="Show this message and exit."
34+
)
3235
@click.version_option(version=__version__)
3336
@click.pass_context
3437
def main(ctx, help_flag):
3538
"""MCPM - Model Context Protocol Manager.
36-
39+
3740
A tool for managing MCP servers across various clients.
3841
"""
3942
# Check if a command is being executed (and it's not help, no command, or the client command)
40-
if ctx.invoked_subcommand and ctx.invoked_subcommand != 'client' and not help_flag:
43+
if ctx.invoked_subcommand and ctx.invoked_subcommand != "client" and not help_flag:
4144
# Check if active client is set
4245
active_client = config_manager.get_active_client()
4346
if not active_client:
4447
console.print("[bold red]Error:[/] No active client set.")
45-
console.print("Please run 'mcpm client <client-name>' to set an active client.")
48+
console.print(
49+
"Please run 'mcpm client <client-name>' to set an active client."
50+
)
4651
console.print("Available clients:")
47-
52+
4853
# Show available clients
4954
from mcpm.utils.client_registry import ClientRegistry
55+
5056
for client in ClientRegistry.get_supported_clients():
5157
console.print(f" - {client}")
52-
58+
5359
# Exit with error
5460
ctx.exit(1)
5561
# If no command was invoked or help is requested, show our custom help
5662
if ctx.invoked_subcommand is None or help_flag:
57-
5863
# Get active client
5964
active_client = config_manager.get_active_client()
60-
65+
6166
# Create a nice ASCII art banner with proper alignment using Rich
6267
from rich.panel import Panel
63-
68+
6469
# Create bold ASCII art with thicker characters for a more striking appearance
6570
logo = [
6671
" ███╗ ███╗ ██████╗██████╗ ███╗ ███╗ ",
@@ -72,65 +77,91 @@ def main(ctx, help_flag):
7277
"",
7378
f"v{__version__}",
7479
"Model Context Protocol Manager",
75-
"Supports Claude Desktop, Windsurf, Cursor, and more"
80+
"Supports Claude Desktop, Windsurf, Cursor, and more",
7681
]
77-
82+
7883
# No need to convert to joined string since we're formatting directly in the panel
79-
84+
8085
# Create a panel with styled content
8186
panel = Panel(
8287
f"[bold green]{logo[0]}\n{logo[1]}\n{logo[2]}\n{logo[3]}\n{logo[4]}\n{logo[5]}[/]\n\n[bold yellow]{logo[7]}[/]\n[italic blue]{logo[8]}[/]\n[bold magenta]{logo[9]}[/]",
8388
border_style="bold cyan",
8489
expand=False,
8590
padding=(0, 2),
8691
)
87-
92+
8893
# Print the panel
8994
console.print(panel)
90-
95+
9196
# Get information about installed clients
9297
from mcpm.utils.client_registry import ClientRegistry
98+
9399
installed_clients = ClientRegistry.detect_installed_clients()
94-
100+
95101
# Display active client information and main help
96102
if active_client:
97-
client_status = "[green]✓[/]" if installed_clients.get(active_client, False) else "[yellow]⚠[/]"
98-
console.print(f"[bold magenta]Active client:[/] [yellow]{active_client}[/] {client_status}")
103+
client_status = (
104+
"[green]✓[/]"
105+
if installed_clients.get(active_client, False)
106+
else "[yellow]⚠[/]"
107+
)
108+
console.print(
109+
f"[bold magenta]Active client:[/] [yellow]{active_client}[/] {client_status}"
110+
)
99111
else:
100-
console.print("[bold red]No active client set![/] Please run 'mcpm client <client-name>' to set one.")
112+
console.print(
113+
"[bold red]No active client set![/] Please run 'mcpm client <client-name>' to set one."
114+
)
101115
console.print("")
102-
116+
103117
# Display usage info
104-
console.print("[bold green]Usage:[/] [white]mcpm [OPTIONS] COMMAND [ARGS]...[/]")
118+
console.print(
119+
"[bold green]Usage:[/] [white]mcpm [OPTIONS] COMMAND [ARGS]...[/]"
120+
)
105121
console.print("")
106-
console.print("[bold green]Description:[/] [white]A tool for managing MCP servers across various clients.[/]")
122+
console.print(
123+
"[bold green]Description:[/] [white]A tool for managing MCP servers across various clients.[/]"
124+
)
107125
console.print("")
108-
126+
109127
# Display options
110128
console.print("[bold]Options:[/]")
111129
console.print(" --version Show the version and exit.")
112130
console.print(" -h, --help Show this message and exit.")
113131
console.print("")
114-
132+
115133
# Display available commands in a table
116134
console.print("[bold]Commands:[/]")
117135
commands_table = Table(show_header=False, box=None, padding=(0, 2, 0, 0))
118-
commands_table.add_row(" [cyan]add[/]", "Add an MCP server directly to a client.")
136+
commands_table.add_row(
137+
" [cyan]add[/]", "Add an MCP server directly to a client."
138+
)
119139
commands_table.add_row(" [cyan]client[/]", "Manage the active MCPM client.")
120-
commands_table.add_row(" [cyan]edit[/]", "View or edit the active MCPM client's configuration file.")
121-
commands_table.add_row(" [cyan]inspector[/]", "Launch the MCPM Inspector UI to examine servers.")
140+
commands_table.add_row(" [cyan]config[/]", "Manage MCPM configuration.")
141+
commands_table.add_row(
142+
" [cyan]edit[/]",
143+
"View or edit the active MCPM client's configuration file.",
144+
)
145+
commands_table.add_row(
146+
" [cyan]inspector[/]", "Launch the MCPM Inspector UI to examine servers."
147+
)
122148
commands_table.add_row(" [cyan]list[/]", "List all installed MCP servers.")
123149
commands_table.add_row(" [cyan]remove[/]", "Remove an installed MCP server.")
124150
commands_table.add_row(" [cyan]search[/]", "Search available MCP servers.")
125-
commands_table.add_row(" [cyan]stash[/]", "Temporarily store a server configuration aside.")
126-
commands_table.add_row(" [cyan]pop[/]", "Restore a previously stashed server configuration.")
151+
commands_table.add_row(
152+
" [cyan]stash[/]", "Temporarily store a server configuration aside."
153+
)
154+
commands_table.add_row(
155+
" [cyan]pop[/]", "Restore a previously stashed server configuration."
156+
)
127157
console.print(commands_table)
128-
129158

130-
131159
# Additional helpful information
132160
console.print("")
133-
console.print("[italic]Run [bold]mcpm CLIENT -h[/] for more information on a command.[/]")
161+
console.print(
162+
"[italic]Run [bold]mcpm CLIENT -h[/] for more information on a command.[/]"
163+
)
164+
134165

135166
# Register commands
136167
main.add_command(search.search)
@@ -143,6 +174,7 @@ def main(ctx, help_flag):
143174
main.add_command(pop.pop)
144175

145176
main.add_command(client.client)
177+
main.add_command(config.config)
146178
main.add_command(inspector.inspector, name="inspector")
147179

148180
if __name__ == "__main__":

src/mcpm/commands/config.py

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
"""Config command for MCPM - Manage MCPM configuration"""
2+
3+
import os
4+
5+
import click
6+
from rich.console import Console
7+
8+
from mcpm.utils.repository import RepositoryManager
9+
10+
console = Console()
11+
repo_manager = RepositoryManager()
12+
13+
14+
@click.group()
15+
def config():
16+
"""Manage MCPM configuration.
17+
18+
Commands for managing MCPM configuration and cache.
19+
"""
20+
pass
21+
22+
23+
@config.command()
24+
def clear_cache():
25+
"""Clear the local repository cache.
26+
27+
Removes the cached server information, forcing a fresh download on next search.
28+
29+
Examples:
30+
mcpm config clear-cache # Clear the local repository cache
31+
"""
32+
try:
33+
# Check if cache file exists
34+
if os.path.exists(repo_manager.cache_file):
35+
# Remove the cache file
36+
os.remove(repo_manager.cache_file)
37+
console.print(
38+
f"[green]Successfully cleared repository cache at:[/] {repo_manager.cache_file}"
39+
)
40+
console.print("Cache will be rebuilt on next search.")
41+
else:
42+
console.print(
43+
f"[yellow]Cache file not found at:[/] {repo_manager.cache_file}"
44+
)
45+
console.print("No action needed.")
46+
except Exception as e:
47+
console.print(f"[bold red]Error clearing cache:[/] {str(e)}")

src/mcpm/utils/repository.py

Lines changed: 58 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -10,18 +10,70 @@
1010
from typing import Dict, List, Optional, Any
1111
import requests
1212

13+
# Import ConfigManager to get the config directory
14+
from mcpm.utils.config import DEFAULT_CONFIG_DIR
15+
1316
logger = logging.getLogger(__name__)
1417

1518
# Default repository URL
1619
DEFAULT_REPO_URL = "https://getmcp.io/api/servers.json"
1720

21+
# Default cache file path
22+
DEFAULT_CACHE_FILE = os.path.join(DEFAULT_CONFIG_DIR, "servers_cache.json")
23+
1824
class RepositoryManager:
1925
"""Manages server repository operations"""
2026

21-
def __init__(self, repo_url: str = DEFAULT_REPO_URL):
27+
def __init__(self, repo_url: str = DEFAULT_REPO_URL, cache_file: str = DEFAULT_CACHE_FILE):
2228
self.repo_url = repo_url
29+
self.cache_file = cache_file
2330
self.servers_cache = None
2431
self.last_refresh = None
32+
33+
# Ensure cache directory exists
34+
os.makedirs(os.path.dirname(self.cache_file), exist_ok=True)
35+
36+
# Load cache from file if available
37+
self._load_cache_from_file()
38+
39+
def _load_cache_from_file(self) -> None:
40+
"""
41+
Load servers cache from file if it exists
42+
"""
43+
if os.path.exists(self.cache_file):
44+
try:
45+
with open(self.cache_file, 'r') as f:
46+
cache_data = json.load(f)
47+
self.servers_cache = cache_data.get('servers')
48+
49+
# Parse the last_refresh timestamp if it exists
50+
last_refresh_str = cache_data.get('last_refresh')
51+
if last_refresh_str:
52+
self.last_refresh = datetime.fromisoformat(last_refresh_str)
53+
54+
logger.debug(f"Loaded servers cache from {self.cache_file}")
55+
except (json.JSONDecodeError, ValueError) as e:
56+
logger.error(f"Error parsing cache file: {self.cache_file}: {e}")
57+
self.servers_cache = None
58+
self.last_refresh = None
59+
60+
def _save_cache_to_file(self) -> None:
61+
"""
62+
Save servers cache to file
63+
"""
64+
if self.servers_cache and self.last_refresh:
65+
try:
66+
cache_data = {
67+
'servers': self.servers_cache,
68+
'last_refresh': self.last_refresh.isoformat()
69+
}
70+
71+
with open(self.cache_file, 'w') as f:
72+
json.dump(cache_data, f, indent=2)
73+
74+
logger.debug(f"Saved servers cache to {self.cache_file}")
75+
except Exception as e:
76+
logger.error(f"Failed to save cache to {self.cache_file}: {e}")
2577

2678
def _fetch_servers(self, force_refresh: bool = False) -> Dict[str, Dict[str, Any]]:
2779
"""
@@ -45,6 +97,10 @@ def _fetch_servers(self, force_refresh: bool = False) -> Dict[str, Dict[str, Any
4597
response.raise_for_status()
4698
self.servers_cache = response.json()
4799
self.last_refresh = datetime.now()
100+
101+
# Save the updated cache to file
102+
self._save_cache_to_file()
103+
48104
return self.servers_cache
49105
except requests.RequestException as e:
50106
logger.error(f"Failed to fetch servers from {self.repo_url}: {e}")
@@ -105,35 +161,4 @@ def get_server_metadata(self, server_name: str) -> Optional[Dict[str, Any]]:
105161
"""
106162
servers_dict = self._fetch_servers()
107163
return servers_dict.get(server_name)
108-
109-
# Version-related method removed
110-
111-
def download_server(self, server_name: str,
112-
dest_dir: Optional[str] = None) -> Optional[Dict[str, Any]]:
113-
"""
114-
Download an MCP server package
115-
116-
Args:
117-
server_name: Name of the server to download
118-
dest_dir: Directory to download to
119-
120-
Returns:
121-
Server metadata if successful, None otherwise
122-
"""
123-
metadata = self.get_server_metadata(server_name)
124-
if not metadata:
125-
logger.error(f"Server not found: {server_name}")
126-
return None
127-
128-
# Create the destination directory if needed
129-
if dest_dir:
130-
os.makedirs(dest_dir, exist_ok=True)
131-
132-
# Store the metadata in the destination directory
133-
if dest_dir:
134-
metadata_path = Path(dest_dir) / "metadata.json"
135-
with open(metadata_path, "w") as f:
136-
json.dump(metadata, f, indent=2)
137-
138-
logger.info(f"Downloaded server {server_name}")
139-
return metadata
164+

0 commit comments

Comments
 (0)