Skip to content

Commit 54c6cd1

Browse files
committed
Add cursor client
1 parent b5ac306 commit 54c6cd1

File tree

7 files changed

+211
-8
lines changed

7 files changed

+211
-8
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ mcp status # Show status of MCP servers in Claude Desktop
7070
- [ ] Server repository structure
7171
- [ ] Server management functionality
7272
- [ ] Additional client support
73+
- [ ] MCP profiles - collections of tools that can be added to any clients with a single command
7374

7475
## Development
7576

src/mcp/cli.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ def main(ctx, help_flag):
5757
"",
5858
f"v{__version__}",
5959
"MCP Manager for all AI apps",
60-
"Supports Claude Desktop, Windsurf, and more"
60+
"Supports Claude Desktop, Windsurf, Cursor, and more"
6161
]
6262

6363
# No need to convert to joined string since we're formatting directly in the panel

src/mcp/clients/cursor.py

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
"""
2+
Cursor client configuration for MCP
3+
"""
4+
5+
import os
6+
import json
7+
import logging
8+
from typing import Dict, Any, List, Optional
9+
10+
# Cursor stores MCP configuration in:
11+
# - Project config: .cursor/mcp.json in the project directory
12+
# - Global config: ~/.cursor/mcp.json in the home directory
13+
14+
# Global config path for Cursor
15+
HOME_DIR = os.path.expanduser("~")
16+
CURSOR_CONFIG_PATH = os.path.join(HOME_DIR, ".cursor", "mcp.json")
17+
18+
logger = logging.getLogger(__name__)
19+
20+
# Get the project config path for Cursor
21+
def get_project_config_path(project_dir: str) -> str:
22+
"""
23+
Get the project-specific MCP configuration path for Cursor
24+
25+
Args:
26+
project_dir (str): Project directory path
27+
28+
Returns:
29+
str: Path to the project-specific MCP configuration file
30+
"""
31+
return os.path.join(project_dir, ".cursor", "mcp.json")
32+
33+
34+
class CursorManager:
35+
"""Manages Cursor client configuration for MCP"""
36+
37+
def __init__(self):
38+
self.config_path = CURSOR_CONFIG_PATH
39+
40+
def is_cursor_installed(self) -> bool:
41+
"""Check if Cursor is installed"""
42+
return os.path.isdir(os.path.dirname(self.config_path))
43+
44+
def read_config(self) -> Optional[Dict[str, Any]]:
45+
"""Read the Cursor MCP configuration"""
46+
if not os.path.exists(self.config_path):
47+
return None
48+
49+
try:
50+
with open(self.config_path, 'r') as f:
51+
return json.load(f)
52+
except json.JSONDecodeError:
53+
logger.error(f"Error parsing Cursor config file: {self.config_path}")
54+
return None
55+
except Exception as e:
56+
logger.error(f"Error reading Cursor config file: {str(e)}")
57+
return None
58+
59+
def write_config(self, config: Dict[str, Any]) -> bool:
60+
"""Write the Cursor MCP configuration"""
61+
try:
62+
# Create directory if it doesn't exist
63+
os.makedirs(os.path.dirname(self.config_path), exist_ok=True)
64+
65+
with open(self.config_path, 'w') as f:
66+
json.dump(config, f, indent=2)
67+
return True
68+
except Exception as e:
69+
logger.error(f"Error writing Cursor config file: {str(e)}")
70+
return False
71+
72+
def create_default_config(self) -> Dict[str, Any]:
73+
"""Create a default Cursor MCP configuration"""
74+
return {
75+
"mcpServers": {}
76+
}
77+
78+
def sync_mcp_servers(self, servers: List[Dict[str, Any]]) -> bool:
79+
"""Sync MCP servers to Cursor configuration"""
80+
config = self.read_config() or self.create_default_config()
81+
82+
# Update mcpServers section
83+
for server in servers:
84+
name = server.get("name")
85+
if name:
86+
config.setdefault("mcpServers", {})[name] = {
87+
"command": server.get("command", ""),
88+
"args": server.get("args", []),
89+
}
90+
91+
# Add environment variables if present
92+
if "env" in server and server["env"]:
93+
config["mcpServers"][name]["env"] = server["env"]
94+
95+
# Write updated config
96+
return self.write_config(config)
97+
98+
def get_servers(self) -> Dict[str, Any]:
99+
"""Get MCP servers from Cursor configuration
100+
101+
Returns:
102+
Dict[str, Any]: Dictionary mapping server names to their configurations
103+
"""
104+
config = self.read_config()
105+
if not config:
106+
return {}
107+
108+
return config.get("mcpServers", {})

src/mcp/commands/edit.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,14 @@
1212

1313
from mcp.clients.claude_desktop import ClaudeDesktopManager
1414
from mcp.clients.windsurf import WindsurfManager
15+
from mcp.clients.cursor import CursorManager
1516
from mcp.utils.config import ConfigManager
1617

1718
console = Console()
1819
config_manager = ConfigManager()
1920
claude_manager = ClaudeDesktopManager()
2021
windsurf_manager = WindsurfManager()
22+
cursor_manager = CursorManager()
2123

2224
@click.command()
2325
@click.option("--edit", is_flag=True, help="Open the active client's config in default editor")
@@ -47,6 +49,11 @@ def edit(edit, create):
4749
client_name = "Windsurf"
4850
install_url = "https://codeium.com/windsurf/download"
4951
is_installed_method = client_manager.is_windsurf_installed
52+
elif active_client == "cursor":
53+
client_manager = cursor_manager
54+
client_name = "Cursor"
55+
install_url = "https://cursor.sh/download"
56+
is_installed_method = client_manager.is_cursor_installed
5057
else:
5158
console.print(f"[bold red]Error:[/] Unsupported active client: {active_client}")
5259
console.print("Please switch to a supported client using 'mcp client <client-name>'")

src/mcp/commands/list_servers.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,14 @@
99

1010
from mcp.clients.claude_desktop import ClaudeDesktopManager
1111
from mcp.clients.windsurf import WindsurfManager
12+
from mcp.clients.cursor import CursorManager
1213
from mcp.utils.config import ConfigManager
1314

1415
console = Console()
1516
config_manager = ConfigManager()
1617
claude_manager = ClaudeDesktopManager()
1718
windsurf_manager = WindsurfManager()
19+
cursor_manager = CursorManager()
1820

1921
@click.command(name="list")
2022
@click.option("--available", is_flag=True, help="List all available MCP servers")
@@ -62,6 +64,9 @@ def list(available, outdated):
6264
elif active_client == "windsurf":
6365
client_manager = windsurf_manager
6466
client_name = "Windsurf"
67+
elif active_client == "cursor":
68+
client_manager = cursor_manager
69+
client_name = "Cursor"
6570
else:
6671
console.print(f"[bold red]Error:[/] Unsupported active client: {active_client}")
6772
console.print("Please switch to a supported client using 'mcp client <client-name>'")
@@ -125,6 +130,9 @@ def list(available, outdated):
125130
elif active_client == "windsurf":
126131
client_manager = windsurf_manager
127132
client_name = "Windsurf"
133+
elif active_client == "cursor":
134+
client_manager = cursor_manager
135+
client_name = "Cursor"
128136
else:
129137
console.print(f"[bold red]Error:[/] Unsupported active client: {active_client}")
130138
console.print("Please switch to a supported client using 'mcp client <client-name>'")

src/mcp/utils/client_detector.py

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77

88
from mcp.clients.claude_desktop import CLAUDE_CONFIG_PATH
99
from mcp.clients.windsurf import WINDSURF_CONFIG_PATH
10+
from mcp.clients.cursor import CURSOR_CONFIG_PATH
1011

1112
def detect_installed_clients() -> Dict[str, bool]:
1213
"""
@@ -18,10 +19,12 @@ def detect_installed_clients() -> Dict[str, bool]:
1819
claude_installed = os.path.isdir(os.path.dirname(CLAUDE_CONFIG_PATH))
1920
windsurf_installed = os.path.isdir(os.path.dirname(WINDSURF_CONFIG_PATH))
2021

22+
cursor_installed = os.path.isdir(os.path.dirname(CURSOR_CONFIG_PATH))
23+
2124
return {
2225
"claude-desktop": claude_installed,
2326
"windsurf": windsurf_installed,
24-
"cursor": False # Not implemented yet
27+
"cursor": cursor_installed
2528
}
2629

2730
def get_client_config_paths() -> Dict[str, str]:
@@ -33,7 +36,8 @@ def get_client_config_paths() -> Dict[str, str]:
3336
"""
3437
return {
3538
"claude-desktop": CLAUDE_CONFIG_PATH,
36-
"windsurf": WINDSURF_CONFIG_PATH
39+
"windsurf": WINDSURF_CONFIG_PATH,
40+
"cursor": CURSOR_CONFIG_PATH
3741
}
3842

3943
def get_client_display_info() -> Dict[str, Dict[str, str]]:
@@ -57,7 +61,7 @@ def get_client_display_info() -> Dict[str, Dict[str, str]]:
5761
"cursor": {
5862
"name": "Cursor",
5963
"download_url": "https://cursor.sh/download",
60-
"config_file": "" # Not implemented yet
64+
"config_file": CURSOR_CONFIG_PATH
6165
}
6266
}
6367

src/mcp/utils/config.py

Lines changed: 79 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -59,14 +59,17 @@ def _default_config(self) -> Dict[str, Any]:
5959
"clients": {
6060
"claude-desktop": {
6161
"enabled_servers": [],
62+
"disabled_servers": {},
6263
"installed": installed_clients.get("claude-desktop", False)
6364
},
6465
"cursor": {
6566
"enabled_servers": [],
67+
"disabled_servers": {},
6668
"installed": installed_clients.get("cursor", False)
6769
},
6870
"windsurf": {
6971
"enabled_servers": [],
72+
"disabled_servers": {},
7073
"installed": installed_clients.get("windsurf", False)
7174
}
7275
}
@@ -111,7 +114,11 @@ def unregister_server(self, server_name: str) -> None:
111114
self._save_config()
112115

113116
def enable_server_for_client(self, server_name: str, client_name: str) -> bool:
114-
"""Enable a server for a specific client"""
117+
"""Enable a server for a specific client
118+
119+
If the server was previously disabled for this client, its configuration
120+
will be restored from the disabled_servers section.
121+
"""
115122
if server_name not in self._config.get("servers", {}):
116123
logger.error(f"Server not installed: {server_name}")
117124
return False
@@ -120,22 +127,90 @@ def enable_server_for_client(self, server_name: str, client_name: str) -> bool:
120127
logger.error(f"Unknown client: {client_name}")
121128
return False
122129

123-
enabled_servers = self._config["clients"][client_name].setdefault("enabled_servers", [])
130+
client_config = self._config["clients"][client_name]
131+
enabled_servers = client_config.setdefault("enabled_servers", [])
132+
disabled_servers = client_config.setdefault("disabled_servers", {})
133+
134+
# Check if we have the server in disabled servers and can restore its config
135+
if server_name in disabled_servers:
136+
# Find the appropriate client manager to update config
137+
if client_name == "claude-desktop":
138+
from mcp.clients.claude_desktop import ClaudeDesktopManager
139+
client_manager = ClaudeDesktopManager()
140+
elif client_name == "windsurf":
141+
from mcp.clients.windsurf import WindsurfManager
142+
client_manager = WindsurfManager()
143+
elif client_name == "cursor":
144+
from mcp.clients.cursor import CursorManager
145+
client_manager = CursorManager()
146+
else:
147+
# Unknown client type, but we'll still track in our config
148+
pass
149+
150+
# If we have a client manager, update its config
151+
if 'client_manager' in locals():
152+
server_config = disabled_servers[server_name]
153+
server_list = [server_config]
154+
client_manager.sync_mcp_servers(server_list)
155+
156+
# Remove from disabled list
157+
del disabled_servers[server_name]
158+
124159
if server_name not in enabled_servers:
125160
enabled_servers.append(server_name)
126161
self._save_config()
127162

128163
return True
129164

130165
def disable_server_for_client(self, server_name: str, client_name: str) -> bool:
131-
"""Disable a server for a specific client"""
166+
"""Disable a server for a specific client
167+
168+
This saves the server's configuration to the disabled_servers section
169+
so it can be restored later if re-enabled.
170+
"""
132171
if client_name not in self._config.get("clients", {}):
133172
logger.error(f"Unknown client: {client_name}")
134173
return False
135174

136-
enabled_servers = self._config["clients"][client_name].get("enabled_servers", [])
175+
client_config = self._config["clients"][client_name]
176+
enabled_servers = client_config.get("enabled_servers", [])
177+
disabled_servers = client_config.setdefault("disabled_servers", {})
178+
179+
# Only proceed if the server is currently enabled
137180
if server_name in enabled_servers:
181+
# Save the server configuration to disabled_servers
182+
server_info = self._config.get("servers", {}).get(server_name, {})
183+
if server_info:
184+
disabled_servers[server_name] = server_info.copy()
185+
186+
# Remove from enabled list
138187
enabled_servers.remove(server_name)
188+
189+
# Find the appropriate client manager to update config
190+
if client_name == "claude-desktop":
191+
from mcp.clients.claude_desktop import ClaudeDesktopManager
192+
client_manager = ClaudeDesktopManager()
193+
elif client_name == "windsurf":
194+
from mcp.clients.windsurf import WindsurfManager
195+
client_manager = WindsurfManager()
196+
elif client_name == "cursor":
197+
from mcp.clients.cursor import CursorManager
198+
client_manager = CursorManager()
199+
else:
200+
# Unknown client type, but we'll still track in our config
201+
pass
202+
203+
# If we have a client manager, update its config to remove the server
204+
if 'client_manager' in locals():
205+
# Get current client config
206+
client_config = client_manager.read_config() or {}
207+
208+
# For cursor, the structure is slightly different
209+
if client_name == "cursor" and "mcpServers" in client_config:
210+
if server_name in client_config["mcpServers"]:
211+
del client_config["mcpServers"][server_name]
212+
client_manager.write_config(client_config)
213+
139214
self._save_config()
140215

141216
return True

0 commit comments

Comments
 (0)