Files
mcp-nextcloud/pyproject.toml
T
Chris CoutinhoandClaude Opus 4.7 f2b7bf132f fix(storage): address PR #798 review feedback (credentials, asyncpg extra, TLS, pool)
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>
2026-05-16 18:53:45 +02:00

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