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
2 changes: 1 addition & 1 deletion .github/workflows/tests-unit.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
Comment thread
coderabbitai[bot] marked this conversation as resolved.
steps:
- uses: actions/checkout@v4

Expand Down
2 changes: 1 addition & 1 deletion .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
8 changes: 8 additions & 0 deletions codecov.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
coverage:
status:
project:
default:
after_n_builds: 5
patch:
default:
after_n_builds: 5
21 changes: 10 additions & 11 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
[build-system]
build-backend = "setuptools.build_meta"

requires = [ "setuptools>=68" ]

[project]
Expand All @@ -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",
Expand All @@ -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
Expand Down Expand Up @@ -78,17 +77,17 @@ 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"
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",
]
8 changes: 8 additions & 0 deletions tests/integration/test_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
15 changes: 15 additions & 0 deletions tests/integration/test_talk_polls.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
26 changes: 26 additions & 0 deletions tests/test_state.py
Original file line number Diff line number Diff line change
@@ -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
Loading