fastapi-mcp-neo 0.13.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.
- fastapi_mcp_neo-0.13.0/.github/workflows/ci-skip.yml +25 -0
- fastapi_mcp_neo-0.13.0/.github/workflows/ci.yml +107 -0
- fastapi_mcp_neo-0.13.0/.github/workflows/publish.yml +59 -0
- fastapi_mcp_neo-0.13.0/.gitignore +20 -0
- fastapi_mcp_neo-0.13.0/AGENTS.md +109 -0
- fastapi_mcp_neo-0.13.0/CHANGELOG.md +771 -0
- fastapi_mcp_neo-0.13.0/PKG-INFO +749 -0
- fastapi_mcp_neo-0.13.0/README.md +731 -0
- fastapi_mcp_neo-0.13.0/SECURITY.md +29 -0
- fastapi_mcp_neo-0.13.0/comparison/README.md +203 -0
- fastapi_mcp_neo-0.13.0/comparison/bench/env_ours/pyproject.toml +14 -0
- fastapi_mcp_neo-0.13.0/comparison/bench/env_theirs/pyproject.toml +10 -0
- fastapi_mcp_neo-0.13.0/comparison/bench/merge_results.py +108 -0
- fastapi_mcp_neo-0.13.0/comparison/bench/results.json +300 -0
- fastapi_mcp_neo-0.13.0/comparison/bench/results_ours.json +173 -0
- fastapi_mcp_neo-0.13.0/comparison/bench/results_theirs.json +86 -0
- fastapi_mcp_neo-0.13.0/comparison/bench/run_ours.py +218 -0
- fastapi_mcp_neo-0.13.0/comparison/bench/run_theirs.py +151 -0
- fastapi_mcp_neo-0.13.0/comparison/bench/shared_app.py +130 -0
- fastapi_mcp_neo-0.13.0/examples/notes_oauth/.env.example +12 -0
- fastapi_mcp_neo-0.13.0/examples/notes_oauth/README.md +166 -0
- fastapi_mcp_neo-0.13.0/examples/notes_oauth/app/__init__.py +0 -0
- fastapi_mcp_neo-0.13.0/examples/notes_oauth/app/__main__.py +37 -0
- fastapi_mcp_neo-0.13.0/examples/notes_oauth/app/auth_routes.py +171 -0
- fastapi_mcp_neo-0.13.0/examples/notes_oauth/app/config.py +27 -0
- fastapi_mcp_neo-0.13.0/examples/notes_oauth/app/credentials.py +53 -0
- fastapi_mcp_neo-0.13.0/examples/notes_oauth/app/main.py +63 -0
- fastapi_mcp_neo-0.13.0/examples/notes_oauth/app/mcp_oauth.py +85 -0
- fastapi_mcp_neo-0.13.0/examples/notes_oauth/app/models.py +54 -0
- fastapi_mcp_neo-0.13.0/examples/notes_oauth/app/notes_routes.py +142 -0
- fastapi_mcp_neo-0.13.0/examples/notes_oauth/app/observability.py +48 -0
- fastapi_mcp_neo-0.13.0/examples/notes_oauth/app/session.py +50 -0
- fastapi_mcp_neo-0.13.0/examples/notes_oauth/app/store.py +23 -0
- fastapi_mcp_neo-0.13.0/examples/notes_oauth/pyproject.toml +35 -0
- fastapi_mcp_neo-0.13.0/examples/notes_oauth/scripts/smoke.py +105 -0
- fastapi_mcp_neo-0.13.0/examples/otel_smoke.md +122 -0
- fastapi_mcp_neo-0.13.0/examples/otel_smoke.py +124 -0
- fastapi_mcp_neo-0.13.0/examples/shop/__init__.py +0 -0
- fastapi_mcp_neo-0.13.0/examples/shop/__main__.py +27 -0
- fastapi_mcp_neo-0.13.0/examples/shop/app.py +587 -0
- fastapi_mcp_neo-0.13.0/examples/shop/jwt_passthrough.py +168 -0
- fastapi_mcp_neo-0.13.0/examples/shop/mcp_walkthrough.py +104 -0
- fastapi_mcp_neo-0.13.0/examples/shop/server.py +31 -0
- fastapi_mcp_neo-0.13.0/pyproject.toml +64 -0
- fastapi_mcp_neo-0.13.0/src/fastapi_mcp_neo/__init__.py +40 -0
- fastapi_mcp_neo-0.13.0/src/fastapi_mcp_neo/depth_guard.py +105 -0
- fastapi_mcp_neo-0.13.0/src/fastapi_mcp_neo/domains.py +146 -0
- fastapi_mcp_neo-0.13.0/src/fastapi_mcp_neo/graphiql.py +82 -0
- fastapi_mcp_neo-0.13.0/src/fastapi_mcp_neo/handler.py +337 -0
- fastapi_mcp_neo-0.13.0/src/fastapi_mcp_neo/http_api.py +89 -0
- fastapi_mcp_neo-0.13.0/src/fastapi_mcp_neo/invoker.py +370 -0
- fastapi_mcp_neo-0.13.0/src/fastapi_mcp_neo/mcp/__init__.py +0 -0
- fastapi_mcp_neo-0.13.0/src/fastapi_mcp_neo/mcp/errors.py +38 -0
- fastapi_mcp_neo-0.13.0/src/fastapi_mcp_neo/mcp/progressive_tools.py +350 -0
- fastapi_mcp_neo-0.13.0/src/fastapi_mcp_neo/mcp/server.py +355 -0
- fastapi_mcp_neo-0.13.0/src/fastapi_mcp_neo/mcp/tools.py +185 -0
- fastapi_mcp_neo-0.13.0/src/fastapi_mcp_neo/naming.py +35 -0
- fastapi_mcp_neo-0.13.0/src/fastapi_mcp_neo/recursive_expand.py +260 -0
- fastapi_mcp_neo-0.13.0/src/fastapi_mcp_neo/scalars.py +110 -0
- fastapi_mcp_neo-0.13.0/src/fastapi_mcp_neo/scanner.py +831 -0
- fastapi_mcp_neo-0.13.0/src/fastapi_mcp_neo/schema_builder.py +353 -0
- fastapi_mcp_neo-0.13.0/src/fastapi_mcp_neo/search.py +104 -0
- fastapi_mcp_neo-0.13.0/src/fastapi_mcp_neo/type_builder.py +1041 -0
- fastapi_mcp_neo-0.13.0/tests/__init__.py +0 -0
- fastapi_mcp_neo-0.13.0/tests/conftest.py +32 -0
- fastapi_mcp_neo-0.13.0/tests/e2e/__init__.py +0 -0
- fastapi_mcp_neo-0.13.0/tests/e2e/test_graphiql.py +182 -0
- fastapi_mcp_neo-0.13.0/tests/e2e/test_mcp_auth.py +92 -0
- fastapi_mcp_neo-0.13.0/tests/e2e/test_mcp_headers.py +104 -0
- fastapi_mcp_neo-0.13.0/tests/e2e/test_mcp_mount.py +82 -0
- fastapi_mcp_neo-0.13.0/tests/e2e/test_mcp_progressive.py +266 -0
- fastapi_mcp_neo-0.13.0/tests/e2e/test_mcp_server.py +190 -0
- fastapi_mcp_neo-0.13.0/tests/e2e/test_mcp_simple_tools.py +137 -0
- fastapi_mcp_neo-0.13.0/tests/e2e/test_oauth_flow.py +235 -0
- fastapi_mcp_neo-0.13.0/tests/e2e/test_otel_propagation.py +124 -0
- fastapi_mcp_neo-0.13.0/tests/guards/__init__.py +0 -0
- fastapi_mcp_neo-0.13.0/tests/guards/test_example_public_api.py +39 -0
- fastapi_mcp_neo-0.13.0/tests/guards/test_version.py +22 -0
- fastapi_mcp_neo-0.13.0/tests/integration/__init__.py +0 -0
- fastapi_mcp_neo-0.13.0/tests/integration/test_alias_contract.py +202 -0
- fastapi_mcp_neo-0.13.0/tests/integration/test_dataclass.py +321 -0
- fastapi_mcp_neo-0.13.0/tests/integration/test_depth_guard.py +107 -0
- fastapi_mcp_neo-0.13.0/tests/integration/test_document_cache.py +180 -0
- fastapi_mcp_neo-0.13.0/tests/integration/test_handler_execution.py +310 -0
- fastapi_mcp_neo-0.13.0/tests/integration/test_handler_runtime.py +204 -0
- fastapi_mcp_neo-0.13.0/tests/integration/test_headers_passthrough.py +165 -0
- fastapi_mcp_neo-0.13.0/tests/integration/test_json_passthrough.py +104 -0
- fastapi_mcp_neo-0.13.0/tests/integration/test_mutation_semantics.py +70 -0
- fastapi_mcp_neo-0.13.0/tests/integration/test_param_matrix.py +362 -0
- fastapi_mcp_neo-0.13.0/tests/integration/test_recursive_expand.py +739 -0
- fastapi_mcp_neo-0.13.0/tests/integration/test_router_edges.py +344 -0
- fastapi_mcp_neo-0.13.0/tests/integration/test_scanner_contract.py +57 -0
- fastapi_mcp_neo-0.13.0/tests/integration/test_schema_builder.py +26 -0
- fastapi_mcp_neo-0.13.0/tests/integration/test_schema_sdl.py +284 -0
- fastapi_mcp_neo-0.13.0/tests/integration/test_type_mapping_regressions.py +616 -0
- fastapi_mcp_neo-0.13.0/tests/integration/test_typevar_routes.py +145 -0
- fastapi_mcp_neo-0.13.0/tests/support/__init__.py +0 -0
- fastapi_mcp_neo-0.13.0/tests/support/apps.py +224 -0
- fastapi_mcp_neo-0.13.0/tests/support/mcp.py +49 -0
- fastapi_mcp_neo-0.13.0/tests/support/models.py +68 -0
- fastapi_mcp_neo-0.13.0/tests/unit/__init__.py +0 -0
- fastapi_mcp_neo-0.13.0/tests/unit/test_depth_guard.py +68 -0
- fastapi_mcp_neo-0.13.0/tests/unit/test_domains.py +113 -0
- fastapi_mcp_neo-0.13.0/tests/unit/test_invoker.py +306 -0
- fastapi_mcp_neo-0.13.0/tests/unit/test_naming.py +38 -0
- fastapi_mcp_neo-0.13.0/tests/unit/test_recursive_expand_walk.py +52 -0
- fastapi_mcp_neo-0.13.0/tests/unit/test_scanner.py +604 -0
- fastapi_mcp_neo-0.13.0/tests/unit/test_search.py +103 -0
- fastapi_mcp_neo-0.13.0/tests/unit/test_type_builder.py +357 -0
- fastapi_mcp_neo-0.13.0/uv.lock +2682 -0
- fastapi_mcp_neo-0.13.0/why.md +81 -0
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# Companion to ci.yml. The real CI is path-filtered, so a PR that changes
|
|
2
|
+
# only non-code files (README, CHANGELOG, Comparison, etc.) skips ci.yml
|
|
3
|
+
# entirely — leaving the required "CI All Green" check stuck in "Expected"
|
|
4
|
+
# forever and blocking the PR. This workflow runs on the INVERSE path filter
|
|
5
|
+
# and reports the same "CI All Green" check as passing, so such PRs can
|
|
6
|
+
# merge. On a mixed PR both workflows run; both report green, and a real
|
|
7
|
+
# failure still fails the same-named check, so nothing is masked.
|
|
8
|
+
name: Test
|
|
9
|
+
|
|
10
|
+
on:
|
|
11
|
+
pull_request:
|
|
12
|
+
paths-ignore:
|
|
13
|
+
- 'src/fastapi_mcp_neo/**'
|
|
14
|
+
- 'tests/**'
|
|
15
|
+
- 'examples/**'
|
|
16
|
+
- 'pyproject.toml'
|
|
17
|
+
- 'uv.lock'
|
|
18
|
+
|
|
19
|
+
jobs:
|
|
20
|
+
ci-all-green:
|
|
21
|
+
name: CI All Green
|
|
22
|
+
runs-on: ubuntu-latest
|
|
23
|
+
steps:
|
|
24
|
+
- name: No code paths changed
|
|
25
|
+
run: echo "No code paths changed — real CI skipped; reporting green."
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
name: Test
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
pull_request:
|
|
5
|
+
paths:
|
|
6
|
+
- 'src/fastapi_mcp_neo/**'
|
|
7
|
+
- 'tests/**'
|
|
8
|
+
- 'examples/**'
|
|
9
|
+
- 'pyproject.toml'
|
|
10
|
+
- 'uv.lock'
|
|
11
|
+
push:
|
|
12
|
+
branches: [master]
|
|
13
|
+
paths:
|
|
14
|
+
- 'src/fastapi_mcp_neo/**'
|
|
15
|
+
- 'tests/**'
|
|
16
|
+
- 'examples/**'
|
|
17
|
+
- 'pyproject.toml'
|
|
18
|
+
- 'uv.lock'
|
|
19
|
+
|
|
20
|
+
jobs:
|
|
21
|
+
test:
|
|
22
|
+
runs-on: ubuntu-latest
|
|
23
|
+
strategy:
|
|
24
|
+
# One version failing must not cancel the others: a canceled job shows
|
|
25
|
+
# as failed even when its tests passed (observed: 3.13 finished green,
|
|
26
|
+
# then got canceled by 3.10's failure with fail-fast's default true).
|
|
27
|
+
fail-fast: false
|
|
28
|
+
matrix:
|
|
29
|
+
python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"]
|
|
30
|
+
|
|
31
|
+
steps:
|
|
32
|
+
- uses: actions/checkout@v5
|
|
33
|
+
|
|
34
|
+
- name: Set up Python ${{ matrix.python-version }}
|
|
35
|
+
uses: actions/setup-python@v6
|
|
36
|
+
with:
|
|
37
|
+
python-version: ${{ matrix.python-version }}
|
|
38
|
+
|
|
39
|
+
- name: Set up uv
|
|
40
|
+
uses: astral-sh/setup-uv@v7
|
|
41
|
+
with:
|
|
42
|
+
enable-cache: true
|
|
43
|
+
|
|
44
|
+
- name: Install dependencies
|
|
45
|
+
run: uv sync --all-extras
|
|
46
|
+
|
|
47
|
+
- name: Run linter
|
|
48
|
+
run: uv run ruff check src tests
|
|
49
|
+
|
|
50
|
+
- name: Run type checker
|
|
51
|
+
run: uv run mypy src
|
|
52
|
+
|
|
53
|
+
- name: Test with pytest (coverage gate 95%)
|
|
54
|
+
run: |
|
|
55
|
+
uv run pytest tests/ -v --cov=fastapi_mcp_neo \
|
|
56
|
+
--cov-report=term-missing --cov-fail-under=95
|
|
57
|
+
|
|
58
|
+
# Guards the demo (and examples' syntax) against framework API drift. The
|
|
59
|
+
# demo is the framework's first real consumer; if a public API is renamed or
|
|
60
|
+
# removed, this job fails on the PR instead of shipping a demo that only
|
|
61
|
+
# breaks at run time. Critical: uv sync installs the WORKING-COPY framework
|
|
62
|
+
# (editable), so the demo is verified against the code in this PR — not the
|
|
63
|
+
# PyPI release. examples/notes_oauth is a standalone project with its own
|
|
64
|
+
# dependency set, so it is guarded here by compileall (syntax) and by
|
|
65
|
+
# tests/test_example_public_api.py in the test job (public-API-only imports).
|
|
66
|
+
demo-guard:
|
|
67
|
+
runs-on: ubuntu-latest
|
|
68
|
+
steps:
|
|
69
|
+
- uses: actions/checkout@v5
|
|
70
|
+
|
|
71
|
+
- name: Set up Python
|
|
72
|
+
uses: actions/setup-python@v6
|
|
73
|
+
with:
|
|
74
|
+
python-version: "3.13"
|
|
75
|
+
|
|
76
|
+
- name: Set up uv
|
|
77
|
+
uses: astral-sh/setup-uv@v7
|
|
78
|
+
with:
|
|
79
|
+
enable-cache: true
|
|
80
|
+
|
|
81
|
+
- name: Install working-copy framework + extras
|
|
82
|
+
run: uv sync --all-extras
|
|
83
|
+
|
|
84
|
+
- name: Demo app builds against working-copy framework
|
|
85
|
+
run: |
|
|
86
|
+
uv run python -c "import examples.shop.app; print(f'demo builds: {len(examples.shop.app.app.routes)} routes')"
|
|
87
|
+
|
|
88
|
+
- name: Compile all example sources
|
|
89
|
+
run: uv run python -m compileall -q examples
|
|
90
|
+
|
|
91
|
+
# Single required-status gate. Branch protection should require this ONE
|
|
92
|
+
# check ("CI All Green") instead of each job (the matrix makes per-job names
|
|
93
|
+
# awkward). It aggregates the real jobs and fails if any did not succeed.
|
|
94
|
+
# A mirror job of the same name lives in ci-skip.yml: when a PR touches no
|
|
95
|
+
# code paths this workflow is skipped entirely, so the mirror reports the
|
|
96
|
+
# same check as green and docs-only PRs are never blocked on a check that
|
|
97
|
+
# path filtering skipped.
|
|
98
|
+
ci-all-green:
|
|
99
|
+
name: CI All Green
|
|
100
|
+
if: always()
|
|
101
|
+
needs: [test, demo-guard]
|
|
102
|
+
runs-on: ubuntu-latest
|
|
103
|
+
steps:
|
|
104
|
+
- name: Verify required jobs succeeded
|
|
105
|
+
run: |
|
|
106
|
+
echo "test=${{ needs.test.result }} demo-guard=${{ needs.demo-guard.result }}"
|
|
107
|
+
[ "${{ needs.test.result }}" = "success" ] && [ "${{ needs.demo-guard.result }}" = "success" ]
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
name: Publish to PyPI via uv
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
workflow_dispatch:
|
|
5
|
+
push:
|
|
6
|
+
tags:
|
|
7
|
+
- "v*"
|
|
8
|
+
|
|
9
|
+
jobs:
|
|
10
|
+
publish:
|
|
11
|
+
runs-on: ubuntu-latest
|
|
12
|
+
|
|
13
|
+
permissions:
|
|
14
|
+
contents: write # gh release create needs it; PyPI steps need only read
|
|
15
|
+
|
|
16
|
+
steps:
|
|
17
|
+
- name: Checkout repository
|
|
18
|
+
uses: actions/checkout@v5
|
|
19
|
+
|
|
20
|
+
- name: Set up uv
|
|
21
|
+
uses: astral-sh/setup-uv@v7
|
|
22
|
+
|
|
23
|
+
- name: Set up Python
|
|
24
|
+
uses: actions/setup-python@v6
|
|
25
|
+
with:
|
|
26
|
+
python-version: "3.13"
|
|
27
|
+
|
|
28
|
+
- name: Build the package
|
|
29
|
+
run: uv build
|
|
30
|
+
|
|
31
|
+
- name: Publish to PyPI
|
|
32
|
+
run: uv publish --token ${{ secrets.PYPI_PUBLISHER }}
|
|
33
|
+
|
|
34
|
+
- name: Create the GitHub release from the CHANGELOG section
|
|
35
|
+
# Same tag, notes extracted from CHANGELOG.md — the release entry
|
|
36
|
+
# must never be forgotten again (0.9.0 shipped to PyPI with no
|
|
37
|
+
# Releases-page entry and looked like a failed release).
|
|
38
|
+
# Idempotent: a re-run skips creation when the entry already exists.
|
|
39
|
+
env:
|
|
40
|
+
GH_TOKEN: ${{ github.token }}
|
|
41
|
+
run: |
|
|
42
|
+
if gh release view "$GITHUB_REF_NAME" >/dev/null 2>&1; then
|
|
43
|
+
echo "release $GITHUB_REF_NAME already exists — skipping"
|
|
44
|
+
exit 0
|
|
45
|
+
fi
|
|
46
|
+
VERSION="${GITHUB_REF_NAME#v}"
|
|
47
|
+
awk -v header="## $VERSION " '
|
|
48
|
+
index($0, header) == 1 { found = 1; next }
|
|
49
|
+
found && /^## / { exit }
|
|
50
|
+
found { print }
|
|
51
|
+
' CHANGELOG.md > release-notes.md
|
|
52
|
+
if [ ! -s release-notes.md ]; then
|
|
53
|
+
echo "no CHANGELOG section for $VERSION — falling back to tag annotation"
|
|
54
|
+
git tag -l --format='%(contents)' "$GITHUB_REF_NAME" > release-notes.md
|
|
55
|
+
fi
|
|
56
|
+
gh release create "$GITHUB_REF_NAME" \
|
|
57
|
+
--title "$GITHUB_REF_NAME" \
|
|
58
|
+
--notes-file release-notes.md \
|
|
59
|
+
--latest
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
__pycache__/
|
|
2
|
+
*.pyc
|
|
3
|
+
.venv/
|
|
4
|
+
.mypy_cache/
|
|
5
|
+
.ruff_cache/
|
|
6
|
+
.pytest_cache/
|
|
7
|
+
dist/
|
|
8
|
+
.DS_Store
|
|
9
|
+
.claude/
|
|
10
|
+
|
|
11
|
+
# consumer example: local env + resolved lock stay untracked
|
|
12
|
+
examples/*/.env
|
|
13
|
+
examples/*/uv.lock
|
|
14
|
+
examples/*/.venv/
|
|
15
|
+
Comparison/bench/*/uv.lock
|
|
16
|
+
.coverage
|
|
17
|
+
|
|
18
|
+
# Claude Code project MCP servers (e.g. notes-demo on localhost:8020) —
|
|
19
|
+
# environment-local dev convenience, not project config
|
|
20
|
+
.mcp.json
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
# Agent.md — release process and attention points
|
|
2
|
+
|
|
3
|
+
Operating instructions for agents (and humans) releasing this repo.
|
|
4
|
+
|
|
5
|
+
## Release flow
|
|
6
|
+
|
|
7
|
+
Prerequisites: on `master`, in sync with origin, working tree clean, CI green
|
|
8
|
+
(`gh run list`). The Unreleased section of `CHANGELOG.md` should carry the
|
|
9
|
+
changes you are shipping.
|
|
10
|
+
|
|
11
|
+
1. **Decide the version.** This repo is 0.x: breaking changes bump the minor
|
|
12
|
+
(0.1 → 0.2 → …), additive changes may too, fixes alone patch. Look at the
|
|
13
|
+
Unreleased entries: anything marked *(breaking)* forces a minor bump.
|
|
14
|
+
2. **Bump the version in TWO places** — this is the classic miss:
|
|
15
|
+
|
|
16
|
+
| File | What to change |
|
|
17
|
+
|---|---|
|
|
18
|
+
| `pyproject.toml` | `version = "X.Y.Z"` (+ `uv lock` to refresh the self-entry) |
|
|
19
|
+
| `CHANGELOG.md` | `## Unreleased` → `## X.Y.Z (YYYY-MM-DD)` |
|
|
20
|
+
|
|
21
|
+
`__version__` needs NO hand-edit: since 0.6.0 it is **derived from
|
|
22
|
+
installed package metadata** (`importlib.metadata`), so pyproject is the
|
|
23
|
+
single source of truth. (0.5.0 shipped with a hardcoded `__version__`
|
|
24
|
+
still at 0.4.0 — the published package reported the wrong runtime
|
|
25
|
+
version; deriving removed that release step entirely.)
|
|
26
|
+
`tests/guards/test_version.py` fails the suite if `__version__` and pyproject
|
|
27
|
+
ever disagree again.
|
|
28
|
+
|
|
29
|
+
Verify before committing:
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
grep '^version' pyproject.toml
|
|
33
|
+
uv run python -c "import fastapi_mcp_neo; print(fastapi_mcp_neo.__version__)"
|
|
34
|
+
# both must print the same X.Y.Z
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
3. **Commit**: `chore(release): X.Y.Z`, one-line summary of the wave.
|
|
38
|
+
4. **Tag and push**:
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
git tag vX.Y.Z
|
|
42
|
+
git push origin master vX.Y.Z
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
5. **Publish is automatic.** The `v*` tag triggers `.github/workflows/publish.yml`:
|
|
46
|
+
`uv build` → `uv publish` (token from the `PYPI_PUBLISHER` secret). No
|
|
47
|
+
GitHub Release is created — the CHANGELOG is the release notes. Watch it:
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
gh run watch $(gh run list --workflow publish.yml --limit 1 --json databaseId -q '.[0].databaseId')
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
6. **Verify before announcing**:
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
curl -s https://pypi.org/pypi/fastapi-mcp-neo/json | python3 -c \
|
|
57
|
+
"import sys,json; print(json.load(sys.stdin)['info']['version'])"
|
|
58
|
+
pip download fastapi-mcp-neo==X.Y.Z --no-deps -d /tmp/check && rm -rf /tmp/check
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Stronger check (used for 0.4.0): run the example off the published wheel —
|
|
62
|
+
from `examples/notes_oauth`, bypassing the editable source:
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
OTEL_OTLP_ENDPOINT=http://localhost:4317 uv run --no-project \
|
|
66
|
+
--with 'fastapi-mcp-neo[mcp]==X.Y.Z' \
|
|
67
|
+
--with fastapi --with uvicorn --with python-dotenv --with asgi-lifespan \
|
|
68
|
+
--with opentelemetry-sdk --with opentelemetry-exporter-otlp \
|
|
69
|
+
python -c "import fastapi_mcp_neo; assert 'site-packages' in fastapi_mcp_neo.__file__; from app.__main__ import main; main()"
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
7. **After release**: new work goes under a fresh `## Unreleased` heading in
|
|
73
|
+
the CHANGELOG (Added / Fixed / Changed subsections).
|
|
74
|
+
|
|
75
|
+
## Attention points
|
|
76
|
+
|
|
77
|
+
- **Release only from `master`.** Feature branches merge (fast-forward when
|
|
78
|
+
linear) before any version bump.
|
|
79
|
+
- **Test suite layers.** `tests/unit/` pins one module without handler
|
|
80
|
+
execution, `tests/integration/` goes through `RouterGraphQLHandler`,
|
|
81
|
+
`tests/e2e/` drives ASGI / fastmcp clients, `tests/guards/` holds meta
|
|
82
|
+
pins. Shared sample models, app factories, and protocol helpers live in
|
|
83
|
+
`tests/support/` — import via `from tests.support import apps, models`
|
|
84
|
+
(every directory keeps an `__init__.py`). New tests go where their
|
|
85
|
+
SUBJECT lives, not where the ticket came from; wave/ticket IDs belong in
|
|
86
|
+
docstrings, never in test names.
|
|
87
|
+
- **The tag is the trigger.** Pushing the tag publishes to PyPI — a broken
|
|
88
|
+
release cannot be unpublished, only superseded by the next version. Never
|
|
89
|
+
move or delete a published tag.
|
|
90
|
+
- **PyPI token**: `PYPI_PUBLISHER` in repo secrets. First release of a new
|
|
91
|
+
project name needs an account-wide token; afterwards rotate to a
|
|
92
|
+
project-scoped one. Also claim the project on PyPI after first publish.
|
|
93
|
+
- **`__version__` consistency** is enforced by `tests/guards/test_version.py`
|
|
94
|
+
(derived from package metadata; pyproject is the single source) — the
|
|
95
|
+
wheel's runtime string must match the tag.
|
|
96
|
+
- **CHANGELOG discipline**: entries land in Unreleased as they ship, not in
|
|
97
|
+
a batch at release time; each entry says *what changed and why*, breaking
|
|
98
|
+
changes are marked *(breaking)*.
|
|
99
|
+
- **Commit hygiene**: conventional commits (`feat:`, `fix:`, `docs:`,
|
|
100
|
+
`chore(release):`); CHANGELOG hunks split into the commit they describe
|
|
101
|
+
when a release groups several commits.
|
|
102
|
+
- **Coverage gate**: CI runs pytest with `--cov-fail-under=95` (baseline
|
|
103
|
+
96.7% after the layered-suite refactor); local `uv run pytest` stays
|
|
104
|
+
ungated for fast partial runs.
|
|
105
|
+
- **Locks**: the repo-root `uv.lock` is committed; `examples/*` and
|
|
106
|
+
`comparison/bench/*` locks are ignored (they carry local path sources).
|
|
107
|
+
- **Known trap**: port 8020 — check `lsof -nP -iTCP:8020 -sTCP:LISTEN`
|
|
108
|
+
before "the server won't start" debugging; yesterday's instance may still
|
|
109
|
+
be holding it (`python -c`-launched processes dodge `pkill -f "python -m app"`).
|