Skip to content
Open
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
4 changes: 2 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.2.2

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Pin checkout to the real v4.2.2 commit

The SHA 11d5960a326750d5838078e36cf38b85af677262 is not a valid commit in actions/checkout (the v4.2.2 commit is 11bd71901bbe5b1630ceea73d27597364c9af683), so GitHub cannot resolve this action and the job stops before checkout. The same invalid pin is introduced in both ci.yml jobs and in pages.yml, publish.yml, and cowork-auto-pr.yml, disabling the repository's CI, deployment, publishing, and automatic-PR workflows until the reference is corrected.

AGENTS.md reference: AGENTS.md:L28-L31

Useful? React with 👍 / 👎.

with:
persist-credentials: false

Expand All @@ -34,7 +34,7 @@ jobs:
CLICK_TO_MCP_NO_LICENSE: "1"

steps:
- uses: actions/checkout@v4
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.2.2
with:
persist-credentials: false

Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/cowork-auto-pr.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ jobs:
# without this step every run failed with "not a git repository" and no
# PR was ever opened (fleet-wide defect: 11/11 seeded copies lacked it).
- name: Check out the pushed branch
uses: actions/checkout@v4
uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.2.2
with:
ref: ${{ github.ref_name }}
fetch-depth: 0
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/pages.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.2.2
with:
persist-credentials: false
- name: Setup Pages
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ jobs:
id-token: write

steps:
- uses: actions/checkout@v4
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.2.2
with:
persist-credentials: false

Expand Down
37 changes: 32 additions & 5 deletions click_to_mcp/discover.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,13 @@
from __future__ import annotations

import importlib
import logging
from dataclasses import dataclass
from importlib.metadata import distribution, entry_points
from typing import Any

logger = logging.getLogger(__name__)


@dataclass
class DiscoveredCLI:
Expand Down Expand Up @@ -47,7 +50,8 @@ def _get_package_metadata(pkg_name: str) -> str:
try:
dist = distribution(pkg_name)
return dist.metadata.get("Summary", "") or ""
except Exception:
except Exception as exc:
logger.debug("Failed to get metadata for package %r: %s", pkg_name, exc)
return ""


Expand Down Expand Up @@ -88,7 +92,13 @@ def scan_entry_points() -> list[DiscoveredCLI]:
is_typer=(cli_type == "typer"),
)
)
except (Exception, SystemExit):
except (Exception, SystemExit) as exc:
logger.debug(
"Skipping entry point %r (%s): %s",
entry_point.name,
getattr(entry_point, "module", "?"),
exc,
)
continue

return discovered
Expand All @@ -113,8 +123,21 @@ def load_cli(cli_name: str) -> Any | None:
if entry_point.name == cli_name:
try:
return entry_point.load()
except Exception:
except Exception as exc:
logger.debug("Failed to load entry point %r: %s", cli_name, exc)
return None

# Fallback: built-in demo CLI bundled with this package.
# When running from a source checkout without `pip install -e .`,
# the entry point may not be registered, but we can still import directly.
if cli_name == "click-to-mcp-demo":
try:
from .demo import cli as demo_cli
return demo_cli
except Exception as exc:
logger.debug("Failed to import built-in demo CLI: %s", exc)
return None

return None


Expand All @@ -138,7 +161,10 @@ def import_cli(module_path: str, attr_name: str) -> Any | None:
if obj is not None:
return obj
return module
except Exception:
except Exception as exc:
logger.debug(
"Failed to import CLI from %s.%s: %s", module_path, attr_name, exc
)
return None


Expand Down Expand Up @@ -168,7 +194,8 @@ def find_our_clis() -> dict[str, Any]:
obj = ep.load()
if _probe_cli_type(obj) != "unknown":
result[ep.name] = obj
except Exception:
except Exception as exc:
logger.debug("Failed to load our CLI %r: %s", ep.name, exc)
continue

return result
73 changes: 73 additions & 0 deletions tests/test_discover_logging.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
"""Tests for observability in discover.py — silent exception handlers should log."""

from __future__ import annotations

import logging
from unittest.mock import MagicMock, patch

import pytest

from click_to_mcp.discover import (
find_our_clis,
import_cli,
load_cli,
scan_entry_points,
)


class TestDiscoverLogging:
"""Verify that silent exception paths emit diagnostic log messages."""

def test_scan_entry_points_logs_load_failure(self, caplog: pytest.LogCaptureFixture) -> None:
"""scan_entry_points should log when an entry point fails to load."""
fake_ep = MagicMock()
fake_ep.name = "broken-cli"
fake_ep.module = "nonexistent.module"
fake_ep.attr = "cli"
fake_ep.dist = None
fake_ep.load.side_effect = ImportError("no module named 'nonexistent'")

with patch("click_to_mcp.discover.entry_points") as mock_eps:
mock_eps.return_value.select.return_value = [fake_ep]
with caplog.at_level(logging.DEBUG, logger="click_to_mcp.discover"):
result = scan_entry_points()

assert result == []
assert any("broken-cli" in r.message for r in caplog.records)

def test_load_cli_logs_entry_point_failure(self, caplog: pytest.LogCaptureFixture) -> None:
"""load_cli should log when an entry point load raises."""
fake_ep = MagicMock()
fake_ep.name = "my-broken-tool"
fake_ep.load.side_effect = RuntimeError("entry point crashed")

with patch("click_to_mcp.discover.entry_points") as mock_eps:
mock_eps.return_value.select.return_value = [fake_ep]
with caplog.at_level(logging.DEBUG, logger="click_to_mcp.discover"):
result = load_cli("my-broken-tool")

assert result is None
assert any("my-broken-tool" in r.message for r in caplog.records)

def test_import_cli_logs_import_failure(self, caplog: pytest.LogCaptureFixture) -> None:
"""import_cli should log when module import fails."""
with caplog.at_level(logging.DEBUG, logger="click_to_mcp.discover"):
result = import_cli("totally.fake.module.xyz", "app")

assert result is None
assert any("totally.fake.module.xyz" in r.message for r in caplog.records)

def test_find_our_clis_logs_load_failure(self, caplog: pytest.LogCaptureFixture) -> None:
"""find_our_clis should log when a known-module entry point fails to load."""
fake_ep = MagicMock()
fake_ep.name = "json2sql"
fake_ep.module = "json2sql.cli"
fake_ep.load.side_effect = AttributeError("missing attr")

with patch("click_to_mcp.discover.entry_points") as mock_eps:
mock_eps.return_value.select.return_value = [fake_ep]
with caplog.at_level(logging.DEBUG, logger="click_to_mcp.discover"):
result = find_our_clis()

assert result == {}
assert any("json2sql" in r.message for r in caplog.records)
Loading