diff --git a/.github/workflows/tests-unit.yml b/.github/workflows/tests-unit.yml index 77135fa..0df3d69 100644 --- a/.github/workflows/tests-unit.yml +++ b/.github/workflows/tests-unit.yml @@ -12,7 +12,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: ["3.12", "3.13"] + python-version: ["3.12", "3.13", "3.14"] steps: - uses: actions/checkout@v4 diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 57191b2..5cf7eee 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -21,7 +21,7 @@ repos: files: src/ - repo: https://github.com/tox-dev/pyproject-fmt - rev: v2.6.0 + rev: v2.20.0 hooks: - id: pyproject-fmt diff --git a/README.md b/README.md index a1d6b62..031b30b 100644 --- a/README.md +++ b/README.md @@ -5,6 +5,8 @@ [![Integration Tests](https://github.com/cloud-py-api/nextcloud-mcp-server/actions/workflows/tests-integration.yml/badge.svg)](https://github.com/cloud-py-api/nextcloud-mcp-server/actions/workflows/tests-integration.yml) [![codecov](https://codecov.io/gh/cloud-py-api/nextcloud-mcp-server/graph/badge.svg)](https://codecov.io/gh/cloud-py-api/nextcloud-mcp-server) +![PythonVersion](https://img.shields.io/badge/python-3.12%20%7C%203.13%20%7C%203.14-blue) + > **Experimental** — This repository is fully maintained by AI (Claude). It serves as an experiment in autonomous AI-driven open-source development. An [MCP (Model Context Protocol)](https://modelcontextprotocol.io/) server that exposes Nextcloud APIs as tools for AI assistants. Connect any MCP-compatible client (Claude Desktop, Claude Code, etc.) to your Nextcloud instance and let AI manage files, read notifications, interact with Talk, and more. diff --git a/codecov.yml b/codecov.yml new file mode 100644 index 0000000..d851788 --- /dev/null +++ b/codecov.yml @@ -0,0 +1,8 @@ +coverage: + status: + project: + default: + after_n_builds: 5 + patch: + default: + after_n_builds: 5 diff --git a/pyproject.toml b/pyproject.toml index 30e63c0..33b925b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,5 @@ [build-system] build-backend = "setuptools.build_meta" - requires = [ "setuptools>=68" ] [project] @@ -20,12 +19,12 @@ classifiers = [ "Programming Language :: Python :: 3 :: Only", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", + "Programming Language :: Python :: 3.14", ] dependencies = [ "mcp[cli]>=1.20", "niquests>=3", ] - optional-dependencies.dev = [ "black", "isort", @@ -38,8 +37,8 @@ optional-dependencies.dev = [ ] scripts.nextcloud-mcp = "nextcloud_mcp.__main__:main" -[tool.setuptools.packages.find] -where = [ "src" ] +[tool.setuptools] +packages.find.where = [ "src" ] [tool.black] line-length = 120 @@ -78,13 +77,6 @@ lint.extend-per-file-ignores."tests/**/*.py" = [ "E402", "S", "UP" ] [tool.isort] profile = "black" -[tool.pytest.ini_options] -testpaths = [ "tests" ] -asyncio_mode = "auto" -markers = [ - "integration: marks tests that require a running Nextcloud instance", -] - [tool.pyright] pythonVersion = "3.12" typeCheckingMode = "strict" @@ -92,3 +84,10 @@ venvPath = "." venv = "venv" reportUnusedFunction = false reportPrivateUsage = false + +[tool.pytest] +ini_options.testpaths = [ "tests" ] +ini_options.asyncio_mode = "auto" +ini_options.markers = [ + "integration: marks tests that require a running Nextcloud instance", +] diff --git a/tests/integration/test_server.py b/tests/integration/test_server.py index 3d3ea03..807b66f 100644 --- a/tests/integration/test_server.py +++ b/tests/integration/test_server.py @@ -71,3 +71,11 @@ async def test_create_server_with_different_permissions(self) -> None: ) mcp = create_server(config) assert len(mcp._tool_manager.list_tools()) == len(EXPECTED_TOOLS) + + @pytest.mark.asyncio + async def test_create_server_from_env(self, monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.setenv("NEXTCLOUD_URL", "http://nextcloud.ncmcp") + monkeypatch.setenv("NEXTCLOUD_USER", "admin") + monkeypatch.setenv("NEXTCLOUD_PASSWORD", "admin") + mcp = create_server() + assert len(mcp._tool_manager.list_tools()) == len(EXPECTED_TOOLS) diff --git a/tests/integration/test_talk_polls.py b/tests/integration/test_talk_polls.py index b24f757..f501069 100644 --- a/tests/integration/test_talk_polls.py +++ b/tests/integration/test_talk_polls.py @@ -362,6 +362,21 @@ async def test_close_poll_preserves_question_and_options(self, nc_mcp: McpTestHe finally: await _delete_room(nc_mcp, str(room["token"])) + @pytest.mark.asyncio + async def test_close_public_poll_includes_voter_details(self, nc_mcp: McpTestHelper) -> None: + room = await _create_room(nc_mcp, "test-close-details") + try: + created = await _create_test_poll(nc_mcp, str(room["token"]), result_mode=0) + await nc_mcp.call("vote_poll", token=str(room["token"]), poll_id=int(created["id"]), option_ids=[0]) + result = await nc_mcp.call("close_poll", token=str(room["token"]), poll_id=int(created["id"])) + poll = json.loads(result) + assert "details" in poll + assert isinstance(poll["details"], list) + assert len(poll["details"]) >= 1 + assert poll["details"][0]["actorId"] == "admin" + finally: + await _delete_room(nc_mcp, str(room["token"])) + class TestPollPermissions: @pytest.mark.asyncio diff --git a/tests/test_state.py b/tests/test_state.py new file mode 100644 index 0000000..39b16c4 --- /dev/null +++ b/tests/test_state.py @@ -0,0 +1,26 @@ +"""Tests for global state management.""" + +import pytest + +import nextcloud_mcp.state as state_module +from nextcloud_mcp.state import get_client, get_config + + +class TestStateNotInitialized: + def test_get_client_before_init_raises(self) -> None: + original = state_module._client + state_module._client = None + try: + with pytest.raises(RuntimeError, match="Server not initialized"): + get_client() + finally: + state_module._client = original + + def test_get_config_before_init_raises(self) -> None: + original = state_module._config + state_module._config = None + try: + with pytest.raises(RuntimeError, match="Server not initialized"): + get_config() + finally: + state_module._config = original