Skip to content

refactor: move mcp tools and scripts into app/entry - #85

Merged
dimslaev merged 17 commits into
refactor/backend-featuresfrom
refactor/backend-entry
Oct 19, 2026
Merged

dimslaev merged 17 commits into
refactor/backend-featuresfrom
refactor/backend-entry

Conversation

@dimslaev

Copy link
Copy Markdown
Owner

Step 4 of the backend architecture plan. Stacked on #84.

⚠️ After merge: reinstall the systemd units before 03:30 that night

The pipeline, report and backup units now start -m app.entry.cli.collect, -m app.entry.cli.report and app/entry/cli/backup_db.py. pipeline/ and backend/scripts/ are gone, so the old units fail with ModuleNotFoundError or a missing file. Right after the deploy, on the box:

sudo cp deploy/*.service deploy/*.timer /etc/systemd/system/ && sudo systemctl daemon-reload

Do it before 03:30 (backup) and 04:00 (collect).

What moved

  • app/router.py is folded into app/main.py. Router order is unchanged.
  • app/mcp/ became app/entry/mcp/: server.py (FastMCP instance, instructions, tool list), auth.py (SharedSecret, WRITE_SCOPE, require_write), and one file per tool. Each tool checks the scope, calls one feature verb and shapes the result. sql_query.py keeps the read-only DSN, the plan check and the blocked tables.
  • app/entry/cli/ holds collect.py (was pipeline/run.py), report.py (was pipeline/report.py), backup_db.py, prestart.sh, wait_for_db.py, create_superuser.py, seed_tags.py + tags.json, seed_articles.py, seed_stories.py, backfill_publishers.py and find_missing_tags.py. Run them with python -m app.entry.cli.<name> from backend/.
  • test.sh and tests-start.sh moved to root scripts/. pipeline/ and backend/scripts/ are deleted.
  • Entry imports only a feature's service and model. To make that possible:
    • catalog: the ArticlePublic assembly and origin_condition moved from service.py to a new rows.py, so service.py can re-export search and the facet functions without a cycle.
    • audience: service.py re-exports the sign-in and welcome mail functions.
    • newsletter: service.py re-exports draft.
    • curation: service.py re-exports check_link, and LinkError moved to model.py.
    • collect: service.py re-exports AGGREGATOR_TYPES and PublisherResolver.
  • Config: deploy units' ExecStart, the prestart path in deploy-production.yml and test-backend.yml, the tests-start path in test-backend.yml, the mypy/ty paths in pre-commit (now backend/app only), and the T201 ignore (now app/entry/cli/**). The CLAUDE.md MCP path and the paths in docs/backend.md and docs/style.md are updated too.
  • Tests: app/mcp/tests moved to app/entry/mcp/tests. test_tools.py was split into test_sql_query.py and test_web_tools.py. tests/scripts/test_wait_for_db.py moved to app/entry/cli/tests/. The tests/ subfolders are left for step 5.

Behaviour changes

  • The cli entrypoints now count toward coverage (source = ["app"]). The total is 80%; CI requires 65%.
  • app.core.email no longer calls logging.basicConfig on import (see bug fixes). The backend's INFO line "send email result" no longer prints unless a handler is configured.
  • Sentry is off when ENVIRONMENT=development (see bug fixes).

Bug fixes

  • The backup never caught an empty dump. dump_db checked the size of the gzip file, which is never 0 bytes because of the gzip header. It now checks the uncompressed byte count.
  • The backup's compose path. It was parents[2]. After the move that would have pointed at backend/, so it is now parents[4]. A test pins it to the repo's compose.yml.
  • Sentry ran in development. main.py compared ENVIRONMENT with "local", which is not one of its values, so the check was always true. It now compares with "development". Strict mypy flagged it.
  • Importing app.core.email set the root logger to INFO. The collect and report runs import it through collect.service. httpx then logged a line for every request of the night into the journal.
  • The alembic floor. The config lives in [tool.alembic], which older alembic versions don't read. The floor went from >=1.12.1 to >=1.18.0. The lock already had 1.18.5.
  • docs/backend.md said tests-start.sh passes its args to pytest. test.sh uses its args as the coverage report's title, and CI relies on that. The doc now says so.

Tests added

  • MCP gate end to end over HTTP: the read token calling a write tool is refused; the write token gets past the gate; with MCP_WRITE_TOKEN unset, its old value gets a 401 and the read token still can't write. These sit alongside the existing tests for no token, a wrong token and an unset MCP_TOKEN.
  • sql_query's read-only and blocked-table guards were already covered. Those tests moved with the file.
  • cli: every deploy unit's ExecStart and every python -m in prestart.sh/tests-start.sh resolves to a real module or file. collect and report exit with their verb's code (0 and 1). The backup fails on a failed or empty pg_dump. The seeds refuse production. The collect run leaves the root logger at WARNING.

Types

cd backend && uv run mypy --strict app/entry app/features app/core reports 0 errors, tests included. That meant removing the type ignores in find_missing_tags.py (now col() and an NDArray[float32] alias) and annotating the test functions.

Review findings

The Claude Code Review workflow can't read diffs right now, so I ran the code-review skill at high effort. It compared against master, so some findings belong to #82–#84:

  • Fixed here: the stale docs/style.md scripts paragraph, the root logger set at import, and the alembic floor.
  • Not changed: "each tool file repeats require_write + Session + error mapping". One file per tool is what the plan asks for, and a shared context manager would save one line per tool but hide what each one does.
  • Left for other steps: send_mail reading os.environ beside settings (ADR 7 question). The collect run building Settings() at import (the plan accepts this under ADR 7). The Jev non-number answer counting as 0, which refactor: split backend domains into app/features #84 lists as intended. Stale paths in the glossary and the deploy/README.md env table (step 8).

How I verified

  • ruff check/format, mypy (root and strict from backend/), ty, typos (one pre-existing hit in .claude/skills/curate/regression.md, not touched here), the banned-names hook, and the other pre-commit hooks over all files.
  • Backend tests on a fresh Postgres 16 + pgvector db, run with the moved prestart.sh and ../scripts/tests-start.sh: 658 passed, 1 failed. The failure is /search, which needs the Hugging Face model download this sandbox blocks. It passes in CI.
  • bash scripts/generate-client.sh leaves frontend/src/client unchanged. The OpenAPI JSON is byte-identical, path order included.
  • A tools/list dump (names, descriptions, input schemas, in order) and the server instructions diff empty against the base.

Generated by Claude Code

* test: colocate backend tests beside their modules

* test: cover the curation tools, sign-in refusals, publisher lookups and the known-URL filter

* fix: fail the daily report when its mail does not send

* test: pin that the report reaches the send, and clean up the filter's publisher

* ci: enforce backend import layers (#87)

* fix: run mypy and ty in pre-commit with the backend config

* ci: enforce backend import layers

* fix: name the exact claude-code-action tag in its pin comments

* ci: deploy only what the box runs (#88)

* docs: point the deploy README's env table at app/features

* ci: deploy only what the box runs

* test: check the deploy filter ships every path the units and the job use

* docs: record the layered backend (#89)

* docs: record the layered backend

* fix: address the backend migration review (#91)

* fix: configure logging for the api process

* fix: let a publisher's link claim a platform subdomain

* fix: fail a link loudly on a non-numeric jev answer

* docs: point stale docstrings at where the code moved

---------

Co-authored-by: Claude <noreply@anthropic.com>

---------

Co-authored-by: Claude <noreply@anthropic.com>

---------

Co-authored-by: Claude <noreply@anthropic.com>

---------

Co-authored-by: Claude <noreply@anthropic.com>

---------

Co-authored-by: Claude <noreply@anthropic.com>
@dimslaev
dimslaev merged commit 11f46b1 into refactor/backend-features Oct 19, 2026
5 checks passed
@dimslaev
dimslaev deleted the refactor/backend-entry branch October 19, 2026 22:39
dimslaev added a commit that referenced this pull request Oct 19, 2026
* refactor: split backend domains into app/features

* refactor: type app/features for mypy strict

* fix: keep links on a shared platform's subdomains with the aggregator

* fix: drop a newsletter product whose picked link is past the search results

* test: cover approve and reject against the schema, and publisher resolution edges

* chore: point the end-of-file exclude at the moved email templates

* refactor: tidy comments and test helpers from the features split

* refactor: move mcp tools and scripts into app/entry (#85)

* refactor: expose what entry needs through each feature's service

* refactor: split the mcp tools into app/entry/mcp and fold the router into main

* refactor: move scripts and the timer entrypoints into app/entry/cli

* test: cover the mcp token gate end to end over http

* test: cover the cli entrypoints' targets and exit codes

* fix: fail the backup when pg_dump writes nothing

* refactor: make app/entry pass strict mypy

* fix: keep sentry off in development

* chore: name the seed module in restore-db's hint without backticks

* docs: say what tests-start.sh does with its argument

* test: return the tool result's error flag and text from the gate tests' helper

* fix: stop importing the email module from configuring the root logger

* fix: require the alembic that reads its config from pyproject

* test: check the collect run leaves the root logger alone

* docs: say where command-line entrypoints live

* refactor: name the newsletter service alike in every mcp tool

* test: colocate backend tests beside their modules (#86)

* test: colocate backend tests beside their modules

* test: cover the curation tools, sign-in refusals, publisher lookups and the known-URL filter

* fix: fail the daily report when its mail does not send

* test: pin that the report reaches the send, and clean up the filter's publisher

* ci: enforce backend import layers (#87)

* fix: run mypy and ty in pre-commit with the backend config

* ci: enforce backend import layers

* fix: name the exact claude-code-action tag in its pin comments

* ci: deploy only what the box runs (#88)

* docs: point the deploy README's env table at app/features

* ci: deploy only what the box runs

* test: check the deploy filter ships every path the units and the job use

* docs: record the layered backend (#89)

* docs: record the layered backend

* fix: address the backend migration review (#91)

* fix: configure logging for the api process

* fix: let a publisher's link claim a platform subdomain

* fix: fail a link loudly on a non-numeric jev answer

* docs: point stale docstrings at where the code moved

---------

Co-authored-by: Claude <noreply@anthropic.com>

---------

Co-authored-by: Claude <noreply@anthropic.com>

---------

Co-authored-by: Claude <noreply@anthropic.com>

---------

Co-authored-by: Claude <noreply@anthropic.com>

---------

Co-authored-by: Claude <noreply@anthropic.com>

---------

Co-authored-by: Claude <noreply@anthropic.com>

---------

Co-authored-by: Claude <noreply@anthropic.com>
dimslaev added a commit that referenced this pull request Oct 19, 2026
* fix: percent-encode database credentials in the dsn

* fix: declare the indexes and column types the migrations built

* refactor: move tables into app/core/db

* test: cover the schema against migrations, the engine and ledger writes

* refactor: split backend domains into app/features (#84)

* refactor: split backend domains into app/features

* refactor: type app/features for mypy strict

* fix: keep links on a shared platform's subdomains with the aggregator

* fix: drop a newsletter product whose picked link is past the search results

* test: cover approve and reject against the schema, and publisher resolution edges

* chore: point the end-of-file exclude at the moved email templates

* refactor: tidy comments and test helpers from the features split

* refactor: move mcp tools and scripts into app/entry (#85)

* refactor: expose what entry needs through each feature's service

* refactor: split the mcp tools into app/entry/mcp and fold the router into main

* refactor: move scripts and the timer entrypoints into app/entry/cli

* test: cover the mcp token gate end to end over http

* test: cover the cli entrypoints' targets and exit codes

* fix: fail the backup when pg_dump writes nothing

* refactor: make app/entry pass strict mypy

* fix: keep sentry off in development

* chore: name the seed module in restore-db's hint without backticks

* docs: say what tests-start.sh does with its argument

* test: return the tool result's error flag and text from the gate tests' helper

* fix: stop importing the email module from configuring the root logger

* fix: require the alembic that reads its config from pyproject

* test: check the collect run leaves the root logger alone

* docs: say where command-line entrypoints live

* refactor: name the newsletter service alike in every mcp tool

* test: colocate backend tests beside their modules (#86)

* test: colocate backend tests beside their modules

* test: cover the curation tools, sign-in refusals, publisher lookups and the known-URL filter

* fix: fail the daily report when its mail does not send

* test: pin that the report reaches the send, and clean up the filter's publisher

* ci: enforce backend import layers (#87)

* fix: run mypy and ty in pre-commit with the backend config

* ci: enforce backend import layers

* fix: name the exact claude-code-action tag in its pin comments

* ci: deploy only what the box runs (#88)

* docs: point the deploy README's env table at app/features

* ci: deploy only what the box runs

* test: check the deploy filter ships every path the units and the job use

* docs: record the layered backend (#89)

* docs: record the layered backend

* fix: address the backend migration review (#91)

* fix: configure logging for the api process

* fix: let a publisher's link claim a platform subdomain

* fix: fail a link loudly on a non-numeric jev answer

* docs: point stale docstrings at where the code moved

---------

Co-authored-by: Claude <noreply@anthropic.com>

---------

Co-authored-by: Claude <noreply@anthropic.com>

---------

Co-authored-by: Claude <noreply@anthropic.com>

---------

Co-authored-by: Claude <noreply@anthropic.com>

---------

Co-authored-by: Claude <noreply@anthropic.com>

---------

Co-authored-by: Claude <noreply@anthropic.com>

---------

Co-authored-by: Claude <noreply@anthropic.com>

---------

Co-authored-by: Claude <noreply@anthropic.com>
dimslaev added a commit that referenced this pull request Oct 19, 2026
* refactor: move shared backend mechanism into app/core

* refactor: reword the check_link module docstring

* fix: read a feed date with no timezone as utc

* test: cover token rejection, the daily mail send and github lookups

* test: spell unparsable the way typos expects

* refactor: move tables into app/core/db (#83)

* fix: percent-encode database credentials in the dsn

* fix: declare the indexes and column types the migrations built

* refactor: move tables into app/core/db

* test: cover the schema against migrations, the engine and ledger writes

* refactor: split backend domains into app/features (#84)

* refactor: split backend domains into app/features

* refactor: type app/features for mypy strict

* fix: keep links on a shared platform's subdomains with the aggregator

* fix: drop a newsletter product whose picked link is past the search results

* test: cover approve and reject against the schema, and publisher resolution edges

* chore: point the end-of-file exclude at the moved email templates

* refactor: tidy comments and test helpers from the features split

* refactor: move mcp tools and scripts into app/entry (#85)

* refactor: expose what entry needs through each feature's service

* refactor: split the mcp tools into app/entry/mcp and fold the router into main

* refactor: move scripts and the timer entrypoints into app/entry/cli

* test: cover the mcp token gate end to end over http

* test: cover the cli entrypoints' targets and exit codes

* fix: fail the backup when pg_dump writes nothing

* refactor: make app/entry pass strict mypy

* fix: keep sentry off in development

* chore: name the seed module in restore-db's hint without backticks

* docs: say what tests-start.sh does with its argument

* test: return the tool result's error flag and text from the gate tests' helper

* fix: stop importing the email module from configuring the root logger

* fix: require the alembic that reads its config from pyproject

* test: check the collect run leaves the root logger alone

* docs: say where command-line entrypoints live

* refactor: name the newsletter service alike in every mcp tool

* test: colocate backend tests beside their modules (#86)

* test: colocate backend tests beside their modules

* test: cover the curation tools, sign-in refusals, publisher lookups and the known-URL filter

* fix: fail the daily report when its mail does not send

* test: pin that the report reaches the send, and clean up the filter's publisher

* ci: enforce backend import layers (#87)

* fix: run mypy and ty in pre-commit with the backend config

* ci: enforce backend import layers

* fix: name the exact claude-code-action tag in its pin comments

* ci: deploy only what the box runs (#88)

* docs: point the deploy README's env table at app/features

* ci: deploy only what the box runs

* test: check the deploy filter ships every path the units and the job use

* docs: record the layered backend (#89)

* docs: record the layered backend

* fix: address the backend migration review (#91)

* fix: configure logging for the api process

* fix: let a publisher's link claim a platform subdomain

* fix: fail a link loudly on a non-numeric jev answer

* docs: point stale docstrings at where the code moved

---------

Co-authored-by: Claude <noreply@anthropic.com>

---------

Co-authored-by: Claude <noreply@anthropic.com>

---------

Co-authored-by: Claude <noreply@anthropic.com>

---------

Co-authored-by: Claude <noreply@anthropic.com>

---------

Co-authored-by: Claude <noreply@anthropic.com>

---------

Co-authored-by: Claude <noreply@anthropic.com>

---------

Co-authored-by: Claude <noreply@anthropic.com>

---------

Co-authored-by: Claude <noreply@anthropic.com>

---------

Co-authored-by: Claude <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants