Round-2 fixes after the bot review on PR #798 plus two user follow-ups (self-signed Postgres support; asyncpg should be a PyPI extra). Folded into the same PR rather than a follow-up since the work is still unmerged. Security -------- - Mask database credentials in all 5 log call sites (storage.py × 4, migrations.py × 1) via a new `mask_db_password()` helper in config.py. Uses SQLAlchemy's `make_url(...).render_as_string(hide_password=True)` with a regex fallback so the masking path never raises. - New `tests/unit/test_storage_logging.py` asserts a sentinel password never appears in `caplog` during `RefreshTokenStorage.initialize()`. Distribution ------------ - `asyncpg` moved to `[project.optional-dependencies] postgres` so a vanilla `pip install nextcloud-mcp-server` no longer pulls in the ~5 MB C extension. The Docker image runs `uv sync --extra postgres`, so containerized deployments are unchanged. - When `DATABASE_URL=postgresql+asyncpg://...` is set on a venv missing the extra, `RefreshTokenStorage.initialize()` raises a friendly RuntimeError pointing at `[postgres]` rather than the generic ModuleNotFoundError. TLS for the Postgres backend ---------------------------- - New `DATABASE_VERIFY_SSL` + `DATABASE_CA_BUNDLE` env vars mirror the existing `NEXTCLOUD_VERIFY_SSL` / `NEXTCLOUD_CA_BUNDLE` pattern (validators in Settings.__post_init__, `get_database_ssl()` helper alongside `get_nextcloud_ssl_verify()`). `DATABASE_VERIFY_SSL=false` wins over `DATABASE_CA_BUNDLE` for incident-response convenience. - Default is **None** rather than True — keeps PR #798's behavior intact for cluster-internal Postgres that runs without TLS. Operators opt into verify-full or supply a private CA. ADR-026 records the reasoning vs the Nextcloud HTTPS default. - Engine factory in `storage.py` passes `ssl` via `connect_args` only when `get_database_ssl()` returns non-None; otherwise asyncpg's default (`prefer`) applies. - Storage logs which TLS mode is active at INFO (no secret material). Configurable connection pool ---------------------------- - `DATABASE_POOL_SIZE` (default 10) and `DATABASE_MAX_OVERFLOW` (default 20) replace the hardcoded engine values. With many replicas this can blow past managed-Postgres `max_connections=100`; tune down for large fleets. - gte-1 / gte-0 validators in __post_init__ reject 0/negative pool sizes at startup with the offending value in the error. Consistency polish ------------------ - Migration 006: convert raw `op.execute("ALTER TABLE ... ADD COLUMN")` to `op.batch_alter_table(...).add_column(sa.Column("nonce", sa.Text))` for stylistic consistency with the rewritten 001-005. Downgrade now drops the column instead of being a no-op. - `registered_webhooks.created_at` standardized from `sa.Float` to `sa.BigInteger` (all other `*_at` columns); `store_webhook()` casts `time.time()` → `int`. - `is_sqlite_url()` made case-insensitive. Testing ------- - New `tests/integration/test_storage_postgres.py::test_cleanup_expired_roundtrip` exercises `cleanup_expired_tokens`, `cleanup_expired_sessions`, and `cleanup_expired_browser_sessions` — relies on DELETE rowcount, historically dialect-tricky. - `tests/unit/test_ssl_config.py` extended with `TestDatabaseSSLSettings` + `TestGetDatabaseSSL` classes (9 new tests) mirroring the existing Nextcloud SSL tests one-for-one. Docs ---- - `docs/configuration.md` Centralized-Storage section grew the four new env vars + a homelab example with a private CA. - `docs/ADR-026` grew Distribution, TLS, and `alembic/env.py` async-pattern subsections explaining the non-obvious design choices. Helm chart counterpart in cbcoutinho/helm-charts PR #34 (separate commit on `feat/nextcloud-mcp-server-database-url`). Verification ------------ - `uv run pytest tests/unit/` — 1025 passed. - `TEST_DATABASE_URL=... uv run pytest tests/integration/test_storage_postgres.py -m postgres` — 6 passed (including new cleanup test). - `uv run ruff check && uv run ruff format --check && uv run ty check -- nextcloud_mcp_server` — clean. Tracked on Astrolabe Cloud POC board, card #99. --- _This PR was generated with the help of AI, and reviewed by a Human_ Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
143 lines
4.9 KiB
TOML
143 lines
4.9 KiB
TOML
[project]
|
|
name = "nextcloud-mcp-server"
|
|
version = "0.86.4"
|
|
description = "Model Context Protocol (MCP) server for Nextcloud integration - enables AI assistants to interact with Nextcloud data"
|
|
authors = [
|
|
{name = "Chris Coutinho", email = "chris@coutinho.io"}
|
|
]
|
|
readme = "README.md"
|
|
license = {text = "AGPL-3.0-only"}
|
|
requires-python = ">=3.11"
|
|
keywords = ["nextcloud", "mcp", "model-context-protocol", "llm", "ai", "claude", "webdav", "caldav", "carddav"]
|
|
dependencies = [
|
|
"mcp[cli] (>=1.27,<1.28)",
|
|
"httpx (>=0.28.1,<0.29.0)",
|
|
"pillow (>=10.3.0,<12.0.0)", # Compatible with fastembed
|
|
"icalendar (>=7.1.0,<7.2.0)",
|
|
"pythonvcard4>=0.2.0",
|
|
"pydantic>=2.11.4",
|
|
"click>=8.1.8",
|
|
"caldav>=3.0.1,<4.0",
|
|
"recurring-ical-events>=3.8.0,<4.0", # Client-side recurrence expansion (preserves TZID/floating semantics)
|
|
"pyjwt[crypto]>=2.8.0",
|
|
"aiosqlite>=0.20.0", # Async SQLite for refresh token storage
|
|
"alembic>=1.14.0", # Database migrations
|
|
"authlib>=1.6.5",
|
|
"qdrant-client>=1.17.0",
|
|
"fastembed>=0.7.3", # BM25 sparse vector embeddings for hybrid search
|
|
"anthropic>=0.42.0", # For RAG evaluation with Anthropic LLMs
|
|
"boto3>=1.35.0", # For Amazon Bedrock provider (optional)
|
|
# Observability dependencies
|
|
"prometheus-client>=0.21.0", # Prometheus metrics
|
|
"opentelemetry-api>=1.28.2", # OpenTelemetry API
|
|
"opentelemetry-sdk>=1.28.2", # OpenTelemetry SDK
|
|
"starlette<1.0", # Pin until we address starlette 1.0 breaking changes (#648)
|
|
"opentelemetry-instrumentation-asgi>=0.49b2", # Auto-instrument ASGI/Starlette
|
|
"opentelemetry-instrumentation-httpx>=0.49b2", # Auto-instrument httpx client
|
|
"opentelemetry-instrumentation-logging>=0.49b2", # Logging integration
|
|
"opentelemetry-exporter-otlp-proto-grpc>=1.28.2", # OTLP gRPC exporter
|
|
"python-json-logger>=3.2.0", # Structured JSON logging
|
|
"jinja2>=3.1.6",
|
|
"langchain-text-splitters>=1.0.0",
|
|
"markdownify>=0.14.1", # HTML to Markdown conversion for News items
|
|
"pymupdf>=1.26.6",
|
|
"pymupdf4llm>=0.2.2",
|
|
"openai>=2.8.1",
|
|
"dynaconf>=3.2.13,<4.0",
|
|
"mistralai>=2.4.5",
|
|
"sqlalchemy[asyncio]>=2.0",
|
|
]
|
|
classifiers = [
|
|
"Development Status :: 4 - Beta",
|
|
"Intended Audience :: Developers",
|
|
"License :: OSI Approved :: GNU Affero General Public License v3",
|
|
"Programming Language :: Python :: 3",
|
|
"Programming Language :: Python :: 3.11",
|
|
"Programming Language :: Python :: 3.12",
|
|
"Topic :: Scientific/Engineering :: Artificial Intelligence",
|
|
"Topic :: Communications",
|
|
"Topic :: Internet :: WWW/HTTP",
|
|
]
|
|
|
|
[project.urls]
|
|
Homepage = "https://github.com/cbcoutinho/nextcloud-mcp-server"
|
|
Documentation = "https://github.com/cbcoutinho/nextcloud-mcp-server#readme"
|
|
Repository = "https://github.com/cbcoutinho/nextcloud-mcp-server"
|
|
"Bug Tracker" = "https://github.com/cbcoutinho/nextcloud-mcp-server/issues"
|
|
Changelog = "https://github.com/cbcoutinho/nextcloud-mcp-server/blob/master/CHANGELOG.md"
|
|
|
|
[tool.pytest.ini_options]
|
|
anyio_mode = "auto"
|
|
addopts = "-p no:asyncio" # Disable pytest-asyncio plugin, use only anyio
|
|
log_cli = 1
|
|
log_cli_level = "ERROR"
|
|
log_level = "ERROR"
|
|
markers = [
|
|
"unit: Fast unit tests with mocked dependencies",
|
|
"integration: Integration tests requiring Docker containers",
|
|
"smoke: Critical path smoke tests for quick validation",
|
|
"keycloak: OAuth tests that utilize keycloak external identity provider",
|
|
"login_flow: Login Flow v2 integration tests (ADR-022)",
|
|
"multi_user_basic: Multi-user BasicAuth pass-through tests (ADR-020)",
|
|
"postgres: Tests requiring the docker-compose postgres-test service (ADR-026)",
|
|
]
|
|
testpaths = [
|
|
"tests",
|
|
]
|
|
# Timeout settings to prevent tests from hanging indefinitely
|
|
timeout = 180 # 3 minutes default timeout per test (includes fixture setup)
|
|
timeout_func_only = false # Timeout includes fixture setup/teardown
|
|
|
|
[tool.commitizen]
|
|
name = "cz_conventional_commits"
|
|
tag_format = "v$version"
|
|
version_scheme = "pep440"
|
|
version_provider = "uv"
|
|
update_changelog_on_bump = true
|
|
major_version_zero = true
|
|
|
|
[tool.ruff.lint]
|
|
extend-select = ["I", "PLC0415", "G004"]
|
|
|
|
[tool.ruff.lint.per-file-ignores]
|
|
"tests/**" = ["PLC0415"]
|
|
|
|
[build-system]
|
|
requires = ["uv_build>=0.11.5,<0.12.0"]
|
|
build-backend = "uv_build"
|
|
|
|
[tool.uv.build-backend]
|
|
module-name = "nextcloud_mcp_server"
|
|
module-root = ""
|
|
|
|
[dependency-groups]
|
|
dev = [
|
|
"commitizen>=4.8.2",
|
|
"datasets>=3.3.0", # For BeIR nfcorpus dataset loading
|
|
"ipython>=9.2.0",
|
|
"playwright>=1.49.1",
|
|
"pytest>=8.3.5",
|
|
"pytest-cov>=6.1.1",
|
|
"pytest-mock>=3.15.1",
|
|
"pytest-playwright-asyncio>=0.7.1",
|
|
"pytest-timeout>=2.3.1",
|
|
"ruff>=0.11.13",
|
|
"reportlab>=4.0.0",
|
|
"ty>=0.0.1a25",
|
|
"pytest-otel>=2.0.1",
|
|
]
|
|
|
|
[project.scripts]
|
|
nextcloud-mcp-server = "nextcloud_mcp_server.cli:cli"
|
|
|
|
[project.optional-dependencies]
|
|
postgres = [
|
|
"asyncpg>=0.29",
|
|
]
|
|
|
|
[[tool.uv.index]]
|
|
name = "testpypi"
|
|
url = "https://test.pypi.org/simple/"
|
|
publish-url = "https://test.pypi.org/legacy/"
|
|
explicit = true
|