Commit Graph
2227 Commits
Author SHA1 Message Date
Chris CoutinhoandClaude Opus 4.7 8cc84ac08b docs: add SECURITY.md and GitHub issue templates
Add a security policy directing private vulnerability reports to
security@astrolabecloud.com instead of public issues, and update the
README's Security section to point at it.

Add structured issue forms under .github/ISSUE_TEMPLATE/ covering bugs,
feature requests, questions, and documentation, plus a config.yml that
disables blank issues and routes security reports and open-ended
questions to the appropriate channels. The bug template captures
fields most commonly missing from past reports (server/Nextcloud/app
versions, deployment mode, transport, MCP client).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-29 23:05:29 +02:00
github-actions[bot] a9c5759869 bump: version 0.72.7 → 0.73.0 2026-04-29 20:27:36 +00:00
Chris CoutinhoandGitHub b4a9639002 Merge pull request #739 from cbcoutinho/ci/sticky-claude-code-review
ci(claude-review): use sticky tracking comment instead of stacking
2026-04-29 22:27:13 +02:00
Chris CoutinhoandGitHub 517ee154ee Merge pull request #737 from cbcoutinho/feat/deck-card-comments
feat(deck): add card comment tools
2026-04-29 22:26:55 +02:00
Chris CoutinhoandClaude Opus 4.7 efa33a1331 ci(claude-review): use sticky tracking comment instead of stacking
Bumps the claude-code-action pin to v1.0.97 and turns on
`track_progress` + `use_sticky_comment` so reviews update a single
tracking comment per PR instead of appending a fresh comment on every
push. Mirrors the pattern in astrolabe-cloud-website.

The prompt now directs Claude to deliver the review by editing the
tracking comment via `mcp__github_comment__update_claude_comment`, and
`Bash(gh pr comment:*)` is dropped from the allowed-tools list since
that path is no longer used. Permissions widen from read to write on
pull-requests + issues so the action can edit its own comment.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-29 22:22:32 +02:00
Chris CoutinhoandClaude Opus 4.7 2129bd6fac fix(deck): address review feedback on card comment tools
- Wrap raw DeckComment returns in CardCommentResponse(BaseResponse) for
  create/update so the success/timestamp envelope matches other deck tools
  (#737 review issue 2).
- Rename ListCardCommentsResponse.total → count and clarify in the
  description that it's the page size, not a server-side total — the Deck
  list endpoint does not expose one (#737 review issue 3).
- Validate the documented 1000-character limit on create/update with an
  inline length check + ValueError, matching the pattern in
  api/management.py (#737 review issue 4).
- Use modern int | None union syntax for the new parent_id parameter
  (#737 review issue 1); rest of the file is left in the existing
  Optional[...] style.

Also add an MCP-level test that the >1000 char message is rejected, and
update the existing comment tests to unwrap the new comment field.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-29 16:57:46 +02:00
Chris CoutinhoandClaude Opus 4.7 13abaf3db7 test(deck): add integration tests for card comment tools
Cover full CRUD lifecycle (create → list → update → delete → verify gone)
and the reply path where parent_id populates replyTo on the new comment.

Tests run against the live mcp container via the existing nc_mcp_client
fixture and reuse the temporary_board_with_card fixture for setup/cleanup.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-29 15:35:10 +02:00
Chris CoutinhoandClaude Opus 4.7 454f6912bc feat(deck): add card comment tools
Expose four new MCP tools backed by existing DeckClient comment methods:

- deck_get_card_comments — list with limit/offset pagination
- deck_create_card_comment — top-level or threaded (via parent_id)
- deck_update_card_comment — author-only on the server
- deck_delete_card_comment — author-only, destructive, idempotent

Adds ListCardCommentsResponse and CardCommentOperationResponse models, and
extends the client unit tests to cover replies, deletion, pagination, and
the request shape for updates.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-29 13:39:24 +02:00
github-actions[bot] a5f56eaf18 bump: version 0.72.6 → 0.72.7 2026-04-27 21:59:50 +00:00
Chris CoutinhoandGitHub 478a5ae39d Merge pull request #733 from cbcoutinho/fix/index-php-prefix-732
fix(client): route /apps/* through /index.php (fixes #732)
2026-04-27 23:59:18 +02:00
Chris CoutinhoandGitHub 47d966c0e4 Merge pull request #736 from cbcoutinho/fix/notes-v5-write-drift-730
fix(notes): defensively unwrap list-shaped Notes responses (refs #730)
2026-04-27 23:59:03 +02:00
Chris CoutinhoandGitHub 02b23fb301 Merge pull request #692 from cbcoutinho/renovate/nextcloud-32-32.0.8
chore(deps): update nextcloud-32:32.0.8 docker digest to 334f45c
2026-04-27 09:40:57 +02:00
renovate-bot-cbcoutinho[bot]andGitHub a3934535bf chore(deps): update nextcloud-32:32.0.8 docker digest to 334f45c 2026-04-27 04:21:29 +00:00
github-actions[bot] 91956e9d65 bump: version 0.72.5 → 0.72.6 2026-04-26 22:45:44 +00:00
Chris CoutinhoandGitHub 134777c459 Merge pull request #735 from cbcoutinho/fix/pydantic-model-drift-728-704
fix(models): coerce Contact.birthday + relax Table.owner_display_name (fixes #728, #704)
2026-04-27 00:45:24 +02:00
Chris CoutinhoandClaude Opus 4.7 fc62be08e2 fix(notes): defensively unwrap list-shaped Notes responses (refs #730)
Notes app v5.0.0 has scenarios where the API returns a JSON list where the
MCP server expects a single note object — notably the notes_api#fail
catch-all returning [] for unmatched routes. Without a guard, callers hit
a cryptic Pydantic "argument after ** must be a mapping, not list" from
Note(**payload).

Add a small _expect_note_object helper at the client layer:
- dict → pass through (the healthy case)
- single-element list → unwrap and warn (Notes v5.0.0 quirk)
- empty list, multi-element list, non-dict → raise a diagnostic ValueError
  that names the operation and points at the likely root cause (URL prefix,
  unmatched route, wrong API version)

Wire it into get_note / create_note / update so any list-shaped response
fails clearly instead of cryptically.

Six unit tests pin every branch of the helper.

Note: The 405s the issue reports for update_note / append_content match
Notes v5.0.0's documented routes (PUT /api/v1/notes/{id}) per upstream
appinfo/routes.php. They are most likely a downstream effect of #732
(missing /index.php URL prefix on installs without Pretty URLs) — the fix
in PR #733 should resolve those once it lands.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 18:02:19 +02:00
Chris CoutinhoandClaude Opus 4.7 06f895f5b9 fix(models): coerce Contact.birthday + relax Table.owner_display_name
Two upstream Pydantic ValidationErrors that took down whole list responses.

#704: Contact.birthday is declared str, but vobject parses BDAY as a
datetime.date — any contact with a populated BDAY broke nc_contacts_list_contacts
entirely. Add a field_validator(mode="before") that coerces date / datetime
to ISO strings. Strings and None pass through unchanged. Defense in depth:
existing call sites already coerce, but the model is now correct on its own
so any future code path that constructs Contact from raw vobject output
stays safe.

#728: Tables app v2.0.1 stopped emitting owner_display_name on the top-level
table payload (still present inside views via get_schema), so list_tables
failed for every user with a Pydantic ValidationError. Make the field
Optional[str] = None — captures the value when present, won't blow up when
missing.

Six new direct-construction unit tests in tests/unit/test_response_models.py
pin both fixes (date / datetime / str / None for birthday; with / without
owner_display_name for Table) so the regressions can't recur silently.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 17:14:08 +02:00
Chris CoutinhoandClaude Opus 4.7 3fb3680a83 fix(client): route /apps/* through /index.php for non-pretty-URL installs
Bare /apps/<app>/... URLs return 404 on Nextcloud installs without Pretty
URLs (URL rewriting), which is opt-in and not the default — see #732. The
/index.php/apps/... form is the universal entry point and works regardless
of web-server config, matching how /remote.php/dav and /ocs/v2.php already
have dedicated entry points.

Add a small _resolve_url helper on BaseNextcloudClient that rewrites
/apps/... → /index.php/apps/... at the top of _make_request, so every
current call site (notes, deck, cookbook, news) and any future ones are
covered transparently with no per-client churn.

Other path prefixes (/remote.php, /ocs, absolute URLs, already-prefixed
/index.php/apps) pass through unchanged. New unit tests in
tests/unit/client/test_base.py pin all six cases.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 16:33:37 +02:00
Chris CoutinhoandGitHub 4e669781ee Merge pull request #723 from cbcoutinho/docs/add-contributor-cla
docs: add contributor license agreement
2026-04-26 02:05:50 +02:00
Chris CoutinhoandClaude Opus 4.7 21a4a90144 docs(cla): name the contracting legal entity
Adds "Astrolabe Cloud" as the named maintainer in the CLA Background
so the contracting party is identifiable, addressing reviewer feedback
on PR #723.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 22:18:38 +02:00
Chris CoutinhoandClaude Opus 4.7 d4a48d4f65 docs: add contributor license agreement
Adds a CLA so future contributions can be relicensed if the project
later offers commercial terms alongside AGPL-3.0. Adapted from the
Apache 2.0 ICLA with Dutch-law modifications: moral rights waiver
under Auteurswet art. 25, GDPR data-processing notice referencing
cla-assistant.io, and Amsterdam jurisdiction.

Signing is administered via the hosted cla-assistant.io service
(configured outside this repo); the Gist referenced there is kept in
sync with CLA.md.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 22:03:27 +02:00
github-actions[bot] c8a231053e bump: version 0.72.4 → 0.72.5 2026-04-23 05:30:09 +00:00
Chris CoutinhoandGitHub 282dce0953 Merge pull request #715 from cbcoutinho/fix/chunk-context-app-password-credentials
fix(api): use stored app password for chunk-context and pdf-preview
2026-04-23 07:29:50 +02:00
Chris CoutinhoandGitHub 3e23585951 Merge pull request #718 from cbcoutinho/fix/login-flow-oauth-stagger
test: stagger parallel OAuth fetches for login-flow users
2026-04-23 07:29:01 +02:00
Chris CoutinhoandClaude Opus 4.7 4a2e3fc169 test: stagger parallel OAuth fetches for login-flow users
Mirrors the per-user delay pattern used in tests/conftest.py:all_oauth_tokens
(commit 963a504). Without it, all four Playwright browser contexts hit
Nextcloud's OIDC authorize endpoint simultaneously and the last users in
iteration order (charlie/diana) frequently time out on the consent screen
in CI, producing `TimeoutError: Timeout waiting for OAuth callback`.

Uses a 0.5s stagger locally and 10s in GITHUB_ACTIONS, matching the
existing fixture so behaviour stays consistent across the two parallel
fixtures.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-23 07:05:17 +02:00
Chris CoutinhoandClaude Opus 4.7 9cf4e16672 test: poll Astrolabe search until the target note is indexed
The previous run on nc32 failed at the search-result assertion because
`wait_for_vector_sync` returned on the first indexed-count bump (deck
seed cards) before this specific note hit Qdrant. Replace the single
search call with a poll that retries every 2s until the unique term
returns our note, or times out after 60s with a loud diagnostic. The
previously-observed flake would now wait past the deck-card indexing
window rather than racing it.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-22 21:55:16 +02:00
Chris CoutinhoandClaude Opus 4.7 b3bd14f183 test: tighten regression guard and hoist httpx import
Per review:
- Hoist `import httpx` out of the two test function bodies and into
  the module imports at the top of
  test_astrolabe_chunk_context.py.
- Simplify the regression guard in
  test_management_chunk_context_endpoint.py to use
  `mock.assert_awaited_once_with(...)` instead of manually unpacking
  call_args. This is stricter — it fails loudly on signature change —
  and matches the canonical pattern for asserting mock calls.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-22 20:26:41 +02:00
Chris CoutinhoandClaude Opus 4.7 06d871ce22 test: address chunk-context review comments
- Rename test_chunk_context_endpoint_handles_missing_app_password to
  test_chunk_context_endpoint_rejects_invalid_bearer so it reflects
  what is actually exercised: an invalid bearer is rejected upfront at
  validate_token_and_get_user, not at the NotProvisionedError branch.
  The NotProvisionedError path is covered by the corresponding unit
  test in test_management_chunk_context_endpoint.py.
- Hoist `import base64` to module level per PEP 8.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-22 20:17:34 +02:00
Chris CoutinhoandClaude Opus 4.7 bea5c3f5ee test: include Nextcloud CSRF token in chunk-context integration test
Astrolabe's ApiController endpoints (search, chunk-context) require a
CSRF `requesttoken` header — axios picks it up from OC.requestToken
automatically in the SPA, but page.request.get() does not.

The first CI run failed on the search step with 412 CSRF check failed
before reaching the chunk-context assertion that was supposed to
surface the handler bug. Load the Astrolabe page, read OC.requestToken,
and pass it on both calls.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-22 20:15:05 +02:00
Chris CoutinhoandClaude Opus 4.7 8a0d06107e fix(api): use stored app password for chunk-context and pdf-preview
The /api/v1/chunk-context and /api/v1/pdf-preview handlers in
api/visualization.py forwarded the incoming OAuth bearer directly to
Nextcloud via NextcloudClient.from_token. In multi-user BasicAuth mode
Nextcloud has no validator for those bearers on Notes/WebDAV, so it
treats the request as anonymous and returns 401 — surfaced to the user
as a 500 from /apps/astrolabe/api/chunk-context. Search worked because
it only hits Qdrant.

Architecturally, OAuth is only for Astrolabe→MCP server; MCP server→
Nextcloud always uses the per-user app password stored during provision
(background sync already does this via vector.oauth_sync).

- Resolve the Nextcloud client through get_user_client_basic_auth in
  both get_chunk_context and get_pdf_preview, surfacing
  NotProvisionedError as a clean 401 instead of opaque 500.
- Apply the same fix to the session-cookie variant in
  auth/viz_routes.chunk_context_endpoint for the internal viz UI.

Tests:
- New unit file test_management_chunk_context_endpoint.py, including a
  regression guard that asserts get_user_client_basic_auth is awaited
  (so reverting to from_token fails without needing a live Nextcloud).
- Updated test_management_pdf_preview_endpoint.py to mock the new auth
  path (drops extract_bearer_token / NextcloudClient.from_token patches).
- New integration test test_astrolabe_chunk_context.py drives the full
  chain (browser → Astrolabe → MCP → Nextcloud) in multi-user BasicAuth
  mode, plus bare-bones 401 checks on the MCP endpoint.

Full unit suite: 546 passed.

Companion PR on astrolabe (cbcoutinho/astrolabe#66) sends the Nextcloud
UID as loginName in the app-password POST body so the stored record is
complete. Submodule bump to that branch will follow once CI reproduces
the failure on the old submodule.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-22 19:50:13 +02:00
github-actions[bot] d766f3c014 bump: version 0.72.3 → 0.72.4 2026-04-16 22:38:24 +00:00
Chris CoutinhoandGitHub 5b6942282c Merge pull request #711 from cbcoutinho/fix/mcp-client-session-cancel-scope
fix(tests): convert create_mcp_client_session to asynccontextmanager
2026-04-17 00:38:04 +02:00
Chris CoutinhoandClaude Opus 4.6 ea7e86f7f4 chore: update astrolabe submodule to v0.13.9
Picks up astrolabe/astrolabe#61 which fixes app password provisioning
failure caused by loginName mismatch in ITokenProvider::generateToken().
This was the root cause of vector sync never indexing in multi-user
BasicAuth mode, which caused the plotly visualization test to fail.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-17 00:25:47 +02:00
Chris CoutinhoandClaude Opus 4.6 3935f45be8 fix(tests): convert create_mcp_client_session to asynccontextmanager
The multi-user-basic integration job was consistently failing with
`CancelledError: Cancelled via cancel scope ... by <async_generator_athrow>`
followed by a cascade of `anyio.ClosedResourceError` in every subsequent
test. Root cause: `create_mcp_client_session` was declared as an async
generator driven by `async for session in ...:`, so Python's generator
finalizer (`aclose`) ran under pytest-asyncio's cleanup task instead of
the task that owned the nested `streamablehttp_client` cancel scope.
anyio then raised when the inner task group saw its scope being exited
from a foreign task, leaving the memory object streams half-closed and
poisoning the rest of the session.

Switching to `@asynccontextmanager` + `async with ... as session:` makes
`__aenter__`/`__aexit__` run in the frame that owns the context manager,
satisfying anyio's structured concurrency requirements.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 12:58:47 +02:00
github-actions[bot] 9b6b554155 bump: version 0.72.2 → 0.72.3 2026-04-15 10:22:31 +00:00
Chris CoutinhoandGitHub e0ed533a54 Merge pull request #709 from dylanlangston/master
Fix: Nutrition.calories field rejects integer values from Nextcloud Cookbook API
2026-04-15 12:22:06 +02:00
DylanandGitHub 06a8262e0e Merge pull request #1 from dylanlangston/copilot/fix-issue-708
Fix Nutrition model to accept numeric values from Cookbook API
2026-04-15 01:05:16 -04:00
480504f8b5 raise ValueError for bool inputs instead of returning as-is
Agent-Logs-Url: https://github.com/dylanlangston/nextcloud-mcp-server/sessions/0360ab28-8913-450e-8c61-697e71ba9742

Co-authored-by: dylanlangston <16236219+dylanlangston@users.noreply.github.com>
2026-04-14 23:04:18 +00:00
6ae30acc6f address review: exclude bool from coercion, parameterize tests over all fields
Agent-Logs-Url: https://github.com/dylanlangston/nextcloud-mcp-server/sessions/0360ab28-8913-450e-8c61-697e71ba9742

Co-authored-by: dylanlangston <16236219+dylanlangston@users.noreply.github.com>
2026-04-14 23:02:56 +00:00
cb88d2b062 fix: coerce numeric nutrition values to strings in Cookbook model (fixes #708)
Agent-Logs-Url: https://github.com/dylanlangston/nextcloud-mcp-server/sessions/d7163bb6-ad5d-4406-8d11-145c5317ec19

Co-authored-by: dylanlangston <16236219+dylanlangston@users.noreply.github.com>
2026-04-14 22:49:34 +00:00
github-actions[bot] fea84dd646 bump: version 0.72.1 → 0.72.2 2026-04-14 19:52:21 +00:00
Chris CoutinhoandGitHub 80822e00e7 Merge pull request #707 from cbcoutinho/fix/uvx-pypi-deployment
fix: enable uvx/PyPI deployments without Docker assumptions
2026-04-14 21:51:58 +02:00
Chris CoutinhoandClaude Opus 4.6 512de1f6b0 test: address PR #707 reviewer feedback on config path helpers
- _resolve_settings_files() now raises FileNotFoundError when
  NEXTCLOUD_MCP_SETTINGS_FILE points to a missing file, instead of
  silently falling back to defaults (footgun on typos).
- .secrets.toml is now looked for alongside the explicit settings file
  when NEXTCLOUD_MCP_SETTINGS_FILE is set, matching user expectation for
  /etc-style deployments. Unset behaviour (cwd lookup) is unchanged.
- get_token_db_path() drops the redundant os.environ.get() short-circuit;
  TOKEN_STORAGE_DB is already bound through dynaconf because the key is
  declared in _DEFAULTS.
- is_ephemeral_token_db() docstring documents the "must call
  get_token_db_path() first" precondition.
- alembic.ini comment clarifies the ./tokens.db placeholder is cwd-relative
  by design and points readers at the -x database_url escape hatch.
- New tests/unit/test_config_paths.py (12 tests) covering the ephemeral
  tempfile lifecycle, the TOKEN_STORAGE_DB override path, and all six
  _resolve_settings_files() cases including the two new behaviours.

Full unit suite now at 476 passed (464 + 12 new). Ruff + ty clean.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 21:14:42 +02:00
Chris CoutinhoandClaude Opus 4.6 146b622ebf fix: enable uvx/PyPI deployments without Docker assumptions
Two bugs made `uvx --from . nextcloud-mcp-server run` (and any pip install)
unusable outside Docker:

1. Dynaconf was configured with ignore_unknown_envvars=True and relied on
   settings.toml to declare the key schema. With no settings.toml in a wheel
   install, every env var (NEXTCLOUD_HOST, MCP_DEPLOYMENT_MODE, ...) was
   silently dropped. Moved the schema into a Python _DEFAULTS dict passed
   directly to Dynaconf, kept settings.toml as an optional external override
   (renamed to settings.toml.example, gitignored), and pointed docker-compose
   at the example file.

2. Token SQLite DB defaulted to /app/data/tokens.db in multiple places
   (auth/storage.py, migrations.py, alembic/env.py, cli.py db subcommands),
   which blew up at uvicorn startup with FileNotFoundError on non-Docker
   hosts. Replaced with a new config.get_token_db_path() helper that
   resolves TOKEN_STORAGE_DB if explicitly set, otherwise allocates a
   per-process tempfile cleaned up at interpreter exit via atexit — mirroring
   the "ephemeral by default" pattern used for QDRANT_LOCATION=:memory:.

Containers are unaffected: docker-compose services now explicitly set
TOKEN_STORAGE_DB=/app/data/tokens.db (the fourth service that was missing
this pin has been brought in line with the other three).

Verified end-to-end in an isolated /tmp venv: env-var-only startup, Alembic
migrations run against the tempfile, Application startup complete, /health/live
returns 200, tempfile deleted on SIGTERM. Unit tests (464) + ruff + ty pass.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 20:11:23 +02:00
Chris Coutinho fd1846de03 ci: Bump astrolabe 2026-04-11 20:16:32 +02:00
Chris CoutinhoandGitHub edb6c8d820 Merge pull request #643 from cbcoutinho/renovate/uv_build-0.x
chore(deps): update dependency uv_build to >=0.11.5,<0.12.0
2026-04-09 18:12:03 +02:00
renovate-bot-cbcoutinho[bot]andGitHub e72a751fdb chore(deps): update dependency uv_build to >=0.11.5,<0.12.0 2026-04-08 22:17:06 +00:00
Chris CoutinhoandGitHub 26e632c1e2 Merge pull request #644 from cbcoutinho/renovate/ghcr.io-astral-sh-uv-0.x
chore(deps): update ghcr.io/astral-sh/uv docker tag to v0.11.4
2026-04-08 15:12:59 +02:00
Chris CoutinhoandGitHub 6b6a5eb639 Merge pull request #698 from cbcoutinho/renovate/anthropics-claude-code-action-1.x
chore(deps): update anthropics/claude-code-action action to v1.0.90
2026-04-08 14:06:51 +02:00
renovate-bot-cbcoutinho[bot]andGitHub 4fbdd471ad chore(deps): update anthropics/claude-code-action action to v1.0.90 2026-04-08 10:19:11 +00:00