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
13 changes: 9 additions & 4 deletions src/fastmcp/cli/install/cursor.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
"""Cursor integration for FastMCP install using Cyclopts."""

import base64
import os
import subprocess
import sys
from pathlib import Path
from typing import Annotated
from urllib.parse import urlparse

import cyclopts
from rich import print
Expand Down Expand Up @@ -51,17 +53,20 @@ def open_deeplink(deeplink: str) -> bool:
Returns:
True if the command succeeded, False otherwise
"""
parsed = urlparse(deeplink)
if parsed.scheme != "cursor":
logger.warning(f"Invalid deeplink scheme: {parsed.scheme}")
return False

try:
if sys.platform == "darwin": # macOS
subprocess.run(["open", deeplink], check=True, capture_output=True)
elif sys.platform == "win32": # Windows
subprocess.run(
["cmd", "/c", "start", deeplink], check=True, capture_output=True
)
os.startfile(deeplink)
else: # Linux and others
subprocess.run(["xdg-open", deeplink], check=True, capture_output=True)
return True
except (subprocess.CalledProcessError, FileNotFoundError):
except (subprocess.CalledProcessError, FileNotFoundError, OSError):
return False


Expand Down
62 changes: 48 additions & 14 deletions tests/cli/test_cursor.py
Original file line number Diff line number Diff line change
Expand Up @@ -135,18 +135,16 @@ def test_open_deeplink_macos(self, mock_run):
["open", "cursor://test"], check=True, capture_output=True
)

@patch("subprocess.run")
def test_open_deeplink_windows(self, mock_run):
def test_open_deeplink_windows(self):
"""Test opening deeplink on Windows."""
with patch("sys.platform", "win32"):
mock_run.return_value = Mock(returncode=0)
with patch(
"fastmcp.cli.install.cursor.os.startfile", create=True
) as mock_startfile:
result = open_deeplink("cursor://test")

result = open_deeplink("cursor://test")

assert result is True
mock_run.assert_called_once_with(
["cmd", "/c", "start", "cursor://test"], check=True, capture_output=True
)
assert result is True
mock_startfile.assert_called_once_with("cursor://test")

@patch("subprocess.run")
def test_open_deeplink_linux(self, mock_run):
Expand All @@ -166,21 +164,57 @@ def test_open_deeplink_failure(self, mock_run):
"""Test handling of deeplink opening failure."""
import subprocess

mock_run.side_effect = subprocess.CalledProcessError(1, ["open"])
with patch("sys.platform", "darwin"):
mock_run.side_effect = subprocess.CalledProcessError(1, ["open"])

result = open_deeplink("cursor://test")
result = open_deeplink("cursor://test")

assert result is False
assert result is False

@patch("subprocess.run")
def test_open_deeplink_command_not_found(self, mock_run):
"""Test handling when open command is not found."""
mock_run.side_effect = FileNotFoundError()
with patch("sys.platform", "darwin"):
mock_run.side_effect = FileNotFoundError()

result = open_deeplink("cursor://test")
result = open_deeplink("cursor://test")

assert result is False

def test_open_deeplink_invalid_scheme(self):
"""Test that non-cursor:// URLs are rejected."""
result = open_deeplink("http://malicious.com")
assert result is False

result = open_deeplink("https://example.com")
assert result is False

result = open_deeplink("file:///etc/passwd")
assert result is False

def test_open_deeplink_valid_cursor_scheme(self):
"""Test that cursor:// URLs are accepted."""
with patch("sys.platform", "darwin"):
with patch("subprocess.run") as mock_run:
mock_run.return_value = Mock(returncode=0)
result = open_deeplink("cursor://anysphere.cursor-deeplink/mcp/install")
assert result is True

def test_open_deeplink_empty_url(self):
"""Test handling of empty URL."""
result = open_deeplink("")
assert result is False

def test_open_deeplink_windows_oserror(self):
"""Test handling of OSError on Windows."""
with patch("sys.platform", "win32"):
with patch(
"fastmcp.cli.install.cursor.os.startfile", create=True
) as mock_startfile:
mock_startfile.side_effect = OSError("File not found")
result = open_deeplink("cursor://test")
assert result is False


class TestInstallCursor:
"""Test cursor installation functionality."""
Expand Down
Loading