fds-mcp 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. fds_mcp-0.1.0/.github/workflows/ci.yml +88 -0
  2. fds_mcp-0.1.0/.github/workflows/release.yml +64 -0
  3. fds_mcp-0.1.0/.gitignore +59 -0
  4. fds_mcp-0.1.0/CHANGELOG.md +78 -0
  5. fds_mcp-0.1.0/CONTRIBUTING.md +59 -0
  6. fds_mcp-0.1.0/Dockerfile +34 -0
  7. fds_mcp-0.1.0/LICENSE +21 -0
  8. fds_mcp-0.1.0/PKG-INFO +589 -0
  9. fds_mcp-0.1.0/README.de.md +589 -0
  10. fds_mcp-0.1.0/README.md +559 -0
  11. fds_mcp-0.1.0/docs/FINDINGS.de.md +343 -0
  12. fds_mcp-0.1.0/docs/FINDINGS.md +341 -0
  13. fds_mcp-0.1.0/docs/oauth-setup.de.md +174 -0
  14. fds_mcp-0.1.0/docs/oauth-setup.md +167 -0
  15. fds_mcp-0.1.0/docs/screenshots/01-applications-list.png +0 -0
  16. fds_mcp-0.1.0/docs/screenshots/02-register-application.png +0 -0
  17. fds_mcp-0.1.0/docs/screenshots/03-oauth-consent.png +0 -0
  18. fds_mcp-0.1.0/examples/request-draft.yaml +61 -0
  19. fds_mcp-0.1.0/pyproject.toml +64 -0
  20. fds_mcp-0.1.0/src/fds_mcp/__init__.py +8 -0
  21. fds_mcp-0.1.0/src/fds_mcp/__main__.py +6 -0
  22. fds_mcp-0.1.0/src/fds_mcp/auth.py +451 -0
  23. fds_mcp-0.1.0/src/fds_mcp/browser.py +242 -0
  24. fds_mcp-0.1.0/src/fds_mcp/cli.py +199 -0
  25. fds_mcp-0.1.0/src/fds_mcp/client.py +396 -0
  26. fds_mcp-0.1.0/src/fds_mcp/config.py +191 -0
  27. fds_mcp-0.1.0/src/fds_mcp/drafts.py +301 -0
  28. fds_mcp-0.1.0/src/fds_mcp/errors.py +25 -0
  29. fds_mcp-0.1.0/src/fds_mcp/rules.py +727 -0
  30. fds_mcp-0.1.0/src/fds_mcp/server.py +1421 -0
  31. fds_mcp-0.1.0/src/fds_mcp/throttle.py +135 -0
  32. fds_mcp-0.1.0/tests/conftest.py +129 -0
  33. fds_mcp-0.1.0/tests/test_api_contract.py +259 -0
  34. fds_mcp-0.1.0/tests/test_auth.py +161 -0
  35. fds_mcp-0.1.0/tests/test_browser_send.py +558 -0
  36. fds_mcp-0.1.0/tests/test_client.py +176 -0
  37. fds_mcp-0.1.0/tests/test_drafts.py +154 -0
  38. fds_mcp-0.1.0/tests/test_instance_config.py +117 -0
  39. fds_mcp-0.1.0/tests/test_law_narrowing.py +134 -0
  40. fds_mcp-0.1.0/tests/test_reply.py +299 -0
  41. fds_mcp-0.1.0/tests/test_rules.py +396 -0
  42. fds_mcp-0.1.0/tests/test_security_gates.py +393 -0
  43. fds_mcp-0.1.0/tests/test_server_tools.py +399 -0
@@ -0,0 +1,88 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+ workflow_dispatch:
8
+
9
+ # Read-only, repository-wide. Deliberately NOT pull_request_target: this workflow
10
+ # checks out and executes the contents of the pull request (pip install -e runs the
11
+ # build backend), and pull_request_target would do that with a writable token and
12
+ # access to repository secrets.
13
+ permissions:
14
+ contents: read
15
+
16
+ jobs:
17
+ test:
18
+ name: pytest (Python ${{ matrix.python-version }})
19
+ runs-on: ubuntu-latest
20
+ strategy:
21
+ fail-fast: false
22
+ matrix:
23
+ python-version: ["3.10", "3.11", "3.12"]
24
+
25
+ steps:
26
+ - uses: actions/checkout@v5
27
+ with:
28
+ persist-credentials: false
29
+
30
+ - uses: actions/setup-python@v5
31
+ with:
32
+ python-version: ${{ matrix.python-version }}
33
+ cache: pip
34
+
35
+ - name: Install
36
+ run: |
37
+ python -m pip install --upgrade pip
38
+ pip install -e ".[dev]"
39
+
40
+ - name: Lint
41
+ run: python -m ruff check src tests
42
+
43
+ - name: Run the offline test suite
44
+ run: python -m pytest -m "not live" -v
45
+
46
+ secrets:
47
+ name: secret scan
48
+ runs-on: ubuntu-latest
49
+ steps:
50
+ - uses: actions/checkout@v5
51
+ with:
52
+ fetch-depth: 0 # gitleaks scans every commit, not just the tip
53
+ persist-credentials: false
54
+
55
+ - name: Install gitleaks
56
+ env:
57
+ GITLEAKS_VERSION: 8.28.0
58
+ GITLEAKS_SHA256: a65b5253807a68ac0cafa4414031fd740aeb55f54fb7e55f386acb52e6a840eb
59
+ run: |
60
+ set -euo pipefail
61
+ curl -sSfL -o gitleaks.tar.gz \
62
+ "https://github.com/gitleaks/gitleaks/releases/download/v${GITLEAKS_VERSION}/gitleaks_${GITLEAKS_VERSION}_linux_x64.tar.gz"
63
+ echo "${GITLEAKS_SHA256} gitleaks.tar.gz" | sha256sum -c -
64
+ tar -xzf gitleaks.tar.gz gitleaks
65
+ ./gitleaks version
66
+
67
+ - name: Scan the whole git history
68
+ run: ./gitleaks git --redact --verbose --exit-code 1 .
69
+
70
+ - name: Scan the working tree
71
+ run: ./gitleaks dir --redact --verbose --exit-code 1 .
72
+
73
+ - name: Check that no secret-bearing file is tracked
74
+ run: |
75
+ if git ls-files | grep -E '(^|/)(tokens\.json|config\.json|\.env)$|\.(pem|key|p12|pfx)$'; then
76
+ echo "::error::A secret-bearing file is tracked in git."
77
+ exit 1
78
+ fi
79
+
80
+ - name: Check for private network addresses and internal hostnames
81
+ run: |
82
+ set -euo pipefail
83
+ pattern='(^|[^0-9.])(10\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}|192\.168\.[0-9]{1,3}\.[0-9]{1,3}|172\.(1[6-9]|2[0-9]|3[01])\.[0-9]{1,3}\.[0-9]{1,3}|100\.(6[4-9]|[7-9][0-9]|1[01][0-9]|12[0-7])\.[0-9]{1,3}\.[0-9]{1,3})|\.intranet\.|\.internal\b|\.ts\.net\b'
84
+ # The workflow file is excluded because it contains the pattern itself.
85
+ if git grep -nEI "$pattern" -- . ':!*.png' ':!*.jpg' ':!.github/workflows/ci.yml'; then
86
+ echo "::error::An internal address or hostname reached the repository."
87
+ exit 1
88
+ fi
@@ -0,0 +1,64 @@
1
+ # Publishes to PyPI when a v* tag is pushed.
2
+ #
3
+ # No API token anywhere: PyPI's Trusted Publishing exchanges the workflow's OIDC
4
+ # identity for a short-lived upload credential. That is why `id-token: write` and the
5
+ # `pypi` environment below have to match the publisher configured on PyPI exactly.
6
+ name: Release
7
+
8
+ on:
9
+ push:
10
+ tags: ["v*"]
11
+
12
+ permissions:
13
+ contents: read
14
+
15
+ jobs:
16
+ build:
17
+ runs-on: ubuntu-latest
18
+ steps:
19
+ - uses: actions/checkout@v5
20
+
21
+ - uses: actions/setup-python@v5
22
+ with:
23
+ python-version: "3.12"
24
+
25
+ - name: Refuse to release a tag that disagrees with the package version
26
+ run: |
27
+ set -euo pipefail
28
+ tag="${GITHUB_REF_NAME#v}"
29
+ pkg=$(python -c "import tomllib,pathlib; \
30
+ print(tomllib.loads(pathlib.Path('pyproject.toml').read_text())['project']['version'])")
31
+ echo "tag=$tag pyproject=$pkg"
32
+ [ "$tag" = "$pkg" ] || { echo "::error::tag $tag != version $pkg"; exit 1; }
33
+
34
+ - name: Install and test before anything is published
35
+ run: |
36
+ python -m pip install --upgrade pip
37
+ pip install -e ".[dev]" build
38
+ python -m pytest -m "not live" -q
39
+
40
+ - run: python -m build
41
+
42
+ - name: Check the artifacts render on PyPI
43
+ run: |
44
+ pip install twine
45
+ python -m twine check dist/*
46
+
47
+ - uses: actions/upload-artifact@v4
48
+ with:
49
+ name: dist
50
+ path: dist/
51
+
52
+ publish:
53
+ needs: build
54
+ runs-on: ubuntu-latest
55
+ environment: pypi
56
+ permissions:
57
+ id-token: write # the whole point: no stored credential
58
+ steps:
59
+ - uses: actions/download-artifact@v4
60
+ with:
61
+ name: dist
62
+ path: dist/
63
+
64
+ - uses: pypa/gh-action-pypi-publish@release/v1
@@ -0,0 +1,59 @@
1
+ # --- secrets: never commit ---
2
+ tokens.json
3
+ config.json
4
+ .env
5
+ .env.*
6
+ *.pem
7
+ *.key
8
+ *.p12
9
+ *.pfx
10
+ secrets/
11
+
12
+ # --- Python ---
13
+ __pycache__/
14
+ *.py[cod]
15
+ *$py.class
16
+ *.so
17
+ .Python
18
+ build/
19
+ develop-eggs/
20
+ dist/
21
+ downloads/
22
+ eggs/
23
+ .eggs/
24
+ lib64/
25
+ parts/
26
+ sdist/
27
+ var/
28
+ wheels/
29
+ share/python-wheels/
30
+ *.egg-info/
31
+ .installed.cfg
32
+ *.egg
33
+ MANIFEST
34
+
35
+ # --- virtualenvs ---
36
+ .venv/
37
+ venv/
38
+ ENV/
39
+ env/
40
+
41
+ # --- test / tooling ---
42
+ .pytest_cache/
43
+ .mypy_cache/
44
+ .ruff_cache/
45
+ .coverage
46
+ .coverage.*
47
+ htmlcov/
48
+ coverage.xml
49
+ .tox/
50
+ .nox/
51
+
52
+ # --- editors / OS ---
53
+ .idea/
54
+ .vscode/
55
+ .DS_Store
56
+
57
+ # --- local working data ---
58
+ drafts/
59
+ *.body.txt
@@ -0,0 +1,78 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project are documented here. The format follows
4
+ [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to
5
+ [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
+
7
+ ## [Unreleased]
8
+
9
+ ### Added
10
+
11
+ - **`build_reply_draft`** (yellow): prepares a follow-up message to an authority — looks
12
+ the request up, validates the text, and returns the finished message, the subject in
13
+ froide's own format (`AW: <title> [#<id>]`) and the URL of the form. It sends nothing,
14
+ and there is no argument that makes it.
15
+ - **Rule `R19-reply-needs-salutation`**, the inverse of `R10`. froide does not frame a
16
+ follow-up, so salutation and closing formula have to stand in the text, exactly once
17
+ each. The reply rule set also applies `R04` (the form is prefilled with U+2026), `R06`
18
+ and the 230-character subject cap.
19
+ - **Rules `R18-full-text-self-contained` and `L06-required-elements`**, which check the
20
+ letter the authority *receives* rather than the body that was written: legal basis (the
21
+ only `ERROR`), cost pre-notification, cost cap, deadline, forwarding when not
22
+ responsible, electronic reply.
23
+ - **`send_reply_via_browser`** (red, opt-in): sends an approved reply draft by driving a
24
+ real browser. Not registered unless `FDS_MCP_BROWSER_SEND=1`; needs the optional extra
25
+ `fds-mcp[browser]`. Five gates plus in-form checks, and an API confirmation afterwards —
26
+ without it the outcome is reported as `unconfirmed`, never as success. The README states
27
+ plainly what switching it on costs.
28
+ - **`tests/test_api_contract.py`**: live learning tests that fire when FragDenStaat changes
29
+ the limits this server is built around — the `kind: "email"` refusal with its
30
+ `kind: "post"` calibration, the web view's indifference to bearer tokens, and the set of
31
+ resources the API router exposes.
32
+ - **German documentation** alongside the English original: `README.de.md` and
33
+ `docs/oauth-setup.de.md`, linked in both directions. US English is the source.
34
+ - Environment variables `FDS_MCP_BROWSER_SEND` and `FDS_MCP_BROWSER_PROFILE`.
35
+
36
+ ## [0.1.0] — 2026-09-05
37
+
38
+ First release.
39
+
40
+ ### Added
41
+
42
+ - **MCP stdio server** (`fds-mcp serve`) exposing 14 tools in three safety tiers.
43
+ - **Green tools** (no authentication, no side effects): `search_authorities`,
44
+ `get_authority`, `get_law`, `check_jurisdiction`.
45
+ - `get_authority` recomputes the law that the REST API would apply, because the API
46
+ does not expose `default_law`.
47
+ - `check_jurisdiction` walks the GeoRegion `part_of` chain and returns the evidence
48
+ trail alongside the covering authorities.
49
+ - **Yellow tools** (OAuth token, read only): `list_my_requests`, `get_request`,
50
+ `get_messages`, `list_attachments`, `download_attachment`, `check_deadlines`.
51
+ - **Red tools** (`dry_run=True` by default): `create_request_draft` (local YAML only, no
52
+ network), `validate_draft`, `build_submit_url` (two-step above the measured 4096-byte
53
+ URL limit), `submit_request`.
54
+ - **Five submission gates** enforced in code: approved status, no open ERROR finding,
55
+ desired law equal to the API default, a human-set confirmation token, and local
56
+ rate-limit bookkeeping against 5/5min, 6/6h, 10/24h and 20/7d.
57
+ - **Read-only HTTP client**: every non-GET method is refused unless `allow_write=True`
58
+ is set explicitly. Pagination de-duplicates by id and raises rather than truncating
59
+ silently.
60
+ - **OAuth 2.0 Authorization Code + PKCE** (`fds-mcp login`) with an HTTPS loopback
61
+ listener on `https://localhost:8765/callback` and a `fragdenstaat://callback` fallback
62
+ for manual paste. Tokens are stored at `~/.config/fds-mcp/tokens.json` with mode 0600
63
+ and refreshed automatically.
64
+ - **Rule set** `R01`–`R17` offline and `L01`–`L05` live, reproducing froide's web-form
65
+ validation, which is considerably stricter than the REST API's.
66
+ - CLI subcommands `serve`, `configure`, `login`, `whoami`, `status`, `logout`,
67
+ `validate`.
68
+ - Test suite with `pytest-socket`; network access is blocked except for tests marked
69
+ `live`, which perform read-only GETs.
70
+
71
+ ### Known limitations
72
+
73
+ Not offered, because the API does not support it: replying to an authority by e-mail,
74
+ choosing the legal basis via `POST /api/v1/request/`, server-side drafts, and setting
75
+ status, resolution, tags or the law after the fact. Documenting postal mail (tus upload
76
+ plus `kind: post`) is possible through the API but is not implemented yet.
77
+
78
+ [0.1.0]: https://github.com/notDIRK/fds-mcp/releases/tag/v0.1.0
@@ -0,0 +1,59 @@
1
+ # Contributing
2
+
3
+ Thanks for taking a look.
4
+
5
+ ## Ground rules
6
+
7
+ 1. **Never invent an endpoint or a field.** Every claim about the FragDenStaat API must
8
+ be verifiable — against the live site, against the OpenAPI schema at
9
+ `https://fragdenstaat.de/api/v1/schema/`, or against a specific place in
10
+ [okfde/froide](https://github.com/okfde/froide) /
11
+ [okfde/fragdenstaat_de](https://github.com/okfde/fragdenstaat_de). Cite it in a
12
+ comment or a docstring.
13
+ 2. **Never send a write to fragdenstaat.de while developing or testing.** No POST, PUT,
14
+ PATCH or DELETE, not even once, not even "just to see". Tests that touch the network
15
+ are marked `live` and perform read-only GETs.
16
+ 3. **Do not weaken the safety gates.** The seven rules in the README's *Safety model* are
17
+ the point of this project. If you change one, the test that proves it has to change
18
+ with it, and the reasoning belongs in the pull request.
19
+ 4. **No secrets in the repository.** Not in code, not in examples, not in test fixtures,
20
+ not in commit messages. `.gitignore` covers `tokens.json`, `config.json`, `.env`,
21
+ `*.pem` and `*.key`; do not work around it.
22
+
23
+ ## Setup
24
+
25
+ ```bash
26
+ python -m venv .venv && source .venv/bin/activate
27
+ pip install -e ".[dev]"
28
+ python -m pytest -m "not live"
29
+ ```
30
+
31
+ `python -m pytest -m "not live"` must be green before you open a pull request. Run
32
+ `python -m pytest -m live` too if your change touches the API layer.
33
+
34
+ ## Adding a rule
35
+
36
+ Rules live in `src/fds_mcp/rules.py`. Offline rules (`R…`) get `@offline_rule("Rnn-name")`
37
+ and take the draft dict; live rules (`L…`) get `@live_rule("Lnn-name")` and additionally
38
+ take a client. Use the next free number, do not renumber existing rules, and add a test
39
+ that makes the rule fire on a deliberately broken draft.
40
+
41
+ Pick the severity honestly:
42
+
43
+ - `ERROR` — froide would reject this, or it would produce a wrong or harmful request.
44
+ - `WARN` — likely a mistake, but a human might have meant it.
45
+ - `INFO` — worth knowing.
46
+
47
+ Only `ERROR` blocks a submission.
48
+
49
+ ## Style
50
+
51
+ - Python 3.10+, type hints on public functions.
52
+ - Comments explain *why*, especially where froide's behaviour is surprising.
53
+ - Keep the docstring of every MCP tool accurate: it is the only thing the model reads.
54
+
55
+ ## Reporting a security issue
56
+
57
+ Open a normal issue if it concerns the safety gates. If you have found something that
58
+ could cause a request to be sent without a human's consent, please say so plainly in the
59
+ title so it gets read first.
@@ -0,0 +1,34 @@
1
+ # Runs the MCP server over stdio in a container.
2
+ #
3
+ # It exists mostly so directories such as Glama can start the server and read its tool
4
+ # list. That works with no credentials at all: the four account-free tools are enough to
5
+ # answer an introspection request.
6
+ #
7
+ # docker build -t fds-mcp .
8
+ # docker run --rm -i fds-mcp
9
+ #
10
+ # To use the token-bound tools, mount the config directory read-only:
11
+ #
12
+ # docker run --rm -i -v "$HOME/.config/fds-mcp:/config:ro" -e FDS_MCP_HOME=/config fds-mcp
13
+ #
14
+ # Nothing is written inside the container; drafts belong on the host.
15
+
16
+ FROM python:3.12-slim
17
+
18
+ # No .pyc, unbuffered stdio -- the transport is stdout, so buffering would stall it.
19
+ ENV PYTHONDONTWRITEBYTECODE=1 \
20
+ PYTHONUNBUFFERED=1
21
+
22
+ WORKDIR /app
23
+
24
+ COPY pyproject.toml README.md LICENSE ./
25
+ COPY src ./src
26
+
27
+ RUN pip install --no-cache-dir .
28
+
29
+ # Never as root: this process talks to a public API and writes local files.
30
+ RUN useradd --create-home --uid 10001 fds
31
+ USER fds
32
+
33
+ # stdio transport: no port, no healthcheck, the client owns the lifecycle.
34
+ ENTRYPOINT ["fds-mcp", "serve"]
fds_mcp-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Dirk Wolbeck
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.