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.
- fds_mcp-0.1.0/.github/workflows/ci.yml +88 -0
- fds_mcp-0.1.0/.github/workflows/release.yml +64 -0
- fds_mcp-0.1.0/.gitignore +59 -0
- fds_mcp-0.1.0/CHANGELOG.md +78 -0
- fds_mcp-0.1.0/CONTRIBUTING.md +59 -0
- fds_mcp-0.1.0/Dockerfile +34 -0
- fds_mcp-0.1.0/LICENSE +21 -0
- fds_mcp-0.1.0/PKG-INFO +589 -0
- fds_mcp-0.1.0/README.de.md +589 -0
- fds_mcp-0.1.0/README.md +559 -0
- fds_mcp-0.1.0/docs/FINDINGS.de.md +343 -0
- fds_mcp-0.1.0/docs/FINDINGS.md +341 -0
- fds_mcp-0.1.0/docs/oauth-setup.de.md +174 -0
- fds_mcp-0.1.0/docs/oauth-setup.md +167 -0
- fds_mcp-0.1.0/docs/screenshots/01-applications-list.png +0 -0
- fds_mcp-0.1.0/docs/screenshots/02-register-application.png +0 -0
- fds_mcp-0.1.0/docs/screenshots/03-oauth-consent.png +0 -0
- fds_mcp-0.1.0/examples/request-draft.yaml +61 -0
- fds_mcp-0.1.0/pyproject.toml +64 -0
- fds_mcp-0.1.0/src/fds_mcp/__init__.py +8 -0
- fds_mcp-0.1.0/src/fds_mcp/__main__.py +6 -0
- fds_mcp-0.1.0/src/fds_mcp/auth.py +451 -0
- fds_mcp-0.1.0/src/fds_mcp/browser.py +242 -0
- fds_mcp-0.1.0/src/fds_mcp/cli.py +199 -0
- fds_mcp-0.1.0/src/fds_mcp/client.py +396 -0
- fds_mcp-0.1.0/src/fds_mcp/config.py +191 -0
- fds_mcp-0.1.0/src/fds_mcp/drafts.py +301 -0
- fds_mcp-0.1.0/src/fds_mcp/errors.py +25 -0
- fds_mcp-0.1.0/src/fds_mcp/rules.py +727 -0
- fds_mcp-0.1.0/src/fds_mcp/server.py +1421 -0
- fds_mcp-0.1.0/src/fds_mcp/throttle.py +135 -0
- fds_mcp-0.1.0/tests/conftest.py +129 -0
- fds_mcp-0.1.0/tests/test_api_contract.py +259 -0
- fds_mcp-0.1.0/tests/test_auth.py +161 -0
- fds_mcp-0.1.0/tests/test_browser_send.py +558 -0
- fds_mcp-0.1.0/tests/test_client.py +176 -0
- fds_mcp-0.1.0/tests/test_drafts.py +154 -0
- fds_mcp-0.1.0/tests/test_instance_config.py +117 -0
- fds_mcp-0.1.0/tests/test_law_narrowing.py +134 -0
- fds_mcp-0.1.0/tests/test_reply.py +299 -0
- fds_mcp-0.1.0/tests/test_rules.py +396 -0
- fds_mcp-0.1.0/tests/test_security_gates.py +393 -0
- 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
|
fds_mcp-0.1.0/.gitignore
ADDED
|
@@ -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.
|
fds_mcp-0.1.0/Dockerfile
ADDED
|
@@ -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.
|