From b0c7f544df897803df69e310a1e6f9e5b48ada0a Mon Sep 17 00:00:00 2001 From: Arun Sharma Date: Wed, 23 Sep 2026 09:37:11 -0700 Subject: [PATCH 1/3] Make extension loading best-effort, migrate to ladybug 0.20.x LOAD duckdb can fail (UnicodeDecodeError on Windows, no network for INSTALL, extension dropped for older versions). The json/duckdb extensions are optional, so log a warning and start the server instead of crashing at startup. Also migrates the deprecated real-ladybug dependency to ladybug>=0.20.4 and fixes column-name retrieval (get_column_names) for the new API. Fixes #3 --- pyproject.toml | 4 +- src/mcp_server_ladybug/database.py | 63 +++++++++++++---- tests/test_database_extensions.py | 107 +++++++++++++++++++++++++++++ 3 files changed, 157 insertions(+), 17 deletions(-) create mode 100644 tests/test_database_extensions.py diff --git a/pyproject.toml b/pyproject.toml index cbf385b..fa78343 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,13 +5,13 @@ description = "A MCP server for LadybugDB - an embedded graph database with Cyph readme = "README.md" requires-python = ">=3.10" dependencies = [ - "mcp>=1.9.4", + "mcp>=1.9.4,<2", "tabulate>=0.9.0", "click>=8.1.8", "starlette>=0.46.1", "uvicorn>=0.34.0", "anyio>=4.8.0", - "real-ladybug>=0.14.0", + "ladybug>=0.20.4", ] [project.scripts] diff --git a/src/mcp_server_ladybug/database.py b/src/mcp_server_ladybug/database.py index 5c401cd..a4b7023 100644 --- a/src/mcp_server_ladybug/database.py +++ b/src/mcp_server_ladybug/database.py @@ -14,33 +14,51 @@ def __init__( self._max_rows = max_rows self._max_chars = max_chars self.db_path = db_path + self.db = None self.conn = self._initialize_connection() def _initialize_connection(self): """Initialize connection to the Ladybug database""" try: - import real_ladybug as lb + import ladybug as lb logger.info(f"Connecting to Ladybug database at: {self.db_path}") - db = lb.Database(self.db_path) - conn = lb.Connection(db) + self.db = lb.Database(self.db_path) + conn = lb.Connection(self.db) - logger.info("Loading DuckDB extensions: json") - conn.execute("INSTALL json") - conn.execute("LOAD json") - - logger.info("Loading DuckDB extensions: duckdb") - conn.execute("INSTALL duckdb") - conn.execute("LOAD duckdb") + self._load_extension(conn, "json") + self._load_extension(conn, "duckdb") logger.info("Successfully connected to Ladybug database") return conn except ImportError: raise ImportError( - "real_ladybug package is not available. " - "Please install it with: pip install real-ladybug" + "ladybug package is not available. " + "Please install it with: pip install ladybug" ) + def _load_extension(self, conn, name: str) -> None: + """Best-effort INSTALL/LOAD of an optional extension. + + Extension loading can fail (e.g. UnicodeDecodeError from `LOAD duckdb` + on Windows, no network for INSTALL, extension dropped for old + versions). These extensions are optional, so log a warning and let the + server start instead of crashing. See issue #3. + """ + for stmt in (f"INSTALL {name}", f"LOAD {name}"): + try: + conn.execute(stmt) + except Exception as e: + logger.warning( + "Optional extension '%s' not loaded (%s failed: %s). " + "Continuing without it.", + name, + stmt, + e, + ) + return + logger.info("Extension '%s' loaded", name) + def _execute(self, query: str) -> str: """Execute a Cypher query and format results""" logger.info(f"Executing query: {query[:100]}...") @@ -58,7 +76,12 @@ def _execute(self, query: str) -> str: if hasattr(res, "get_all"): rows = res.get_all() if rows: - if hasattr(res, "columns"): + if hasattr(res, "get_column_names"): + try: + headers = res.get_column_names() + except Exception: + headers = [] + elif hasattr(res, "columns"): headers = res.columns else: headers = [] @@ -109,5 +132,15 @@ def get_schema(self) -> str: def close(self): """Close the database connection""" - if self.conn: - self.conn.close() + if getattr(self, "conn", None): + try: + self.conn.close() + except Exception: + pass + self.conn = None + if getattr(self, "db", None): + try: + self.db.close() + except Exception: + pass + self.db = None diff --git a/tests/test_database_extensions.py b/tests/test_database_extensions.py new file mode 100644 index 0000000..4e34dbb --- /dev/null +++ b/tests/test_database_extensions.py @@ -0,0 +1,107 @@ +"""Regression tests for https://github.com/LadybugDB/mcp-server-ladybug/issues/3. + +`LOAD duckdb` can fail (UnicodeDecodeError on Windows with older +real_ladybug, no network for INSTALL, extension dropped, ...). The json and +duckdb extensions are optional, so DatabaseClient must still start and serve +queries when they fail to load. +""" + +import pathlib + +import ladybug as lb + +from mcp_server_ladybug.database import DatabaseClient + +RealConnection = lb.Connection + + +class _FailingExtensionConnection: + """Wraps the real Connection but fails the given statements.""" + + fail_statements: set = set() + + def __init__(self, db): + self._real = RealConnection(db) + + def execute(self, query): + if query.strip().upper() in self.fail_statements: + # Reproduce issue #3's Windows failure mode. + raise UnicodeDecodeError( + "utf-8", b"\xd5", 163, 164, "invalid continuation byte" + ) + return self._real.execute(query) + + def close(self): + return self._real.close() + + +def _make_failing_connection(*statements): + upper = {s.strip().upper() for s in statements} + + class FailingConnection(_FailingExtensionConnection): + fail_statements = upper + + return FailingConnection + + +def test_client_starts_when_load_duckdb_fails(monkeypatch): + """Exact issue #3 scenario: INSTALL ok, LOAD duckdb raises UnicodeDecodeError.""" + monkeypatch.setattr( + lb, "Connection", _make_failing_connection("LOAD duckdb") + ) + client = DatabaseClient(db_path=":memory:") + try: + out = client.query( + "CREATE NODE TABLE Person (id INT64 PRIMARY KEY, name STRING);" + ) + assert "Person" in out + out = client.query("MATCH (p:Person) RETURN p.id;") + assert "No rows" in out + finally: + client.close() + + +def test_client_starts_when_all_extension_installs_fail(monkeypatch): + """Offline / dropped-extension scenario: every INSTALL/LOAD raises.""" + monkeypatch.setattr( + lb, + "Connection", + _make_failing_connection( + "INSTALL json", "LOAD json", "INSTALL duckdb", "LOAD duckdb" + ), + ) + client = DatabaseClient(db_path=":memory:") + try: + assert "Person" in client.query( + "CREATE NODE TABLE Person (id INT64 PRIMARY KEY, name STRING);" + ) + finally: + client.close() + + +def test_load_extension_helper_swallows_errors(caplog): + """_load_extension logs a warning and returns instead of raising.""" + client = DatabaseClient.__new__(DatabaseClient) + + class Boom: + def execute(self, query): + raise RuntimeError("boom") + + with caplog.at_level("WARNING", logger="mcp_server_ladybug"): + client._load_extension(Boom(), "duckdb") # must not raise + + assert any( + "duckdb" in rec.message and "Continuing without it" in rec.message + for rec in caplog.records + ) + + +def test_no_real_ladybug_references(): + """The deprecated real_ladybug package must not be referenced anymore.""" + src = pathlib.Path(__file__).resolve().parents[1] + offenders = [] + for path in list((src / "src").rglob("*.py")) + [src / "pyproject.toml"]: + text = path.read_text(encoding="utf-8") + if "real_ladybug" in text or "real-ladybug" in text: + offenders.append(str(path)) + assert not offenders, f"real_ladybug references remain: {offenders}" From f4e5d4ada8c7a9327d3cccc27376750bd64d422e Mon Sep 17 00:00:00 2001 From: Arun Sharma Date: Wed, 23 Sep 2026 09:49:50 -0700 Subject: [PATCH 2/3] Add CI matrix for linux, mac, windows Run pytest on ubuntu/macos/windows with Python 3.12 and 3.14 (3.14/Windows covers the issue #3 environment). On Windows, _lbug.pyd links OpenSSL dynamically (libssl-3-x64.dll, libcrypto-3-x64.dll) and the wheel does not vendor those DLLs, so install the OpenSSL Light runtime via choco install openssl.light and put it on PATH. --- .github/workflows/ci.yml | 47 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) create mode 100644 .github/workflows/ci.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..6d17a78 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,47 @@ +name: CI + +on: + push: + branches: [main] + pull_request: + +jobs: + test: + name: test (py${{ matrix.python-version }} / ${{ matrix.os }}) + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, macos-latest, windows-latest] + python-version: ["3.12", "3.14"] + steps: + - uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + + - name: Install uv + uses: astral-sh/setup-uv@v4 + + - name: Install package and test deps + run: uv pip install --system -e . pytest + + - name: Install OpenSSL 3 (Windows) + # _lbug.pyd links OpenSSL dynamically (libssl-3-x64.dll, + # libcrypto-3-x64.dll) and the wheel deliberately does not vendor + # those DLLs, so install the OpenSSL Light runtime. + # See https://github.com/LadybugDB/mcp-server-ladybug/issues/3 + if: runner.os == 'Windows' + shell: pwsh + run: | + choco install openssl.light -y --no-progress + $bin = "C:\Program Files\OpenSSL-Win64\bin" + if (-not (Test-Path "$bin\libssl-3-x64.dll")) { throw "OpenSSL DLLs not found at $bin" } + $env:PATH = "$bin;$env:PATH" + Add-Content -Path $env:GITHUB_PATH -Value $bin + python -c "import ladybug._lbug; print('pybind backend loads OK')" + + - name: Run tests + run: python -m pytest tests/ -v From ab23296b8bacfd7115d8be1b5b74bebdba6ef037 Mon Sep 17 00:00:00 2001 From: Arun Sharma Date: Wed, 23 Sep 2026 09:51:20 -0700 Subject: [PATCH 3/3] Fix OpenSSL install path in Windows CI openssl.light deploys to C:\Program Files\OpenSSL, not OpenSSL-Win64. --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6d17a78..9a044af 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -37,7 +37,7 @@ jobs: shell: pwsh run: | choco install openssl.light -y --no-progress - $bin = "C:\Program Files\OpenSSL-Win64\bin" + $bin = "C:\Program Files\OpenSSL\bin" if (-not (Test-Path "$bin\libssl-3-x64.dll")) { throw "OpenSSL DLLs not found at $bin" } $env:PATH = "$bin;$env:PATH" Add-Content -Path $env:GITHUB_PATH -Value $bin