fastapi-gql-mcp 0.4.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_gql_mcp-0.4.0/.github/workflows/ci-skip.yml +26 -0
- fastapi_gql_mcp-0.4.0/.github/workflows/ci.yml +107 -0
- fastapi_gql_mcp-0.4.0/.github/workflows/gh-pages.yml +42 -0
- fastapi_gql_mcp-0.4.0/.github/workflows/publish.yml +41 -0
- fastapi_gql_mcp-0.4.0/.gitignore +15 -0
- fastapi_gql_mcp-0.4.0/CHANGELOG.md +262 -0
- fastapi_gql_mcp-0.4.0/Comparison/README.md +104 -0
- fastapi_gql_mcp-0.4.0/Comparison/bench/env_ours/pyproject.toml +14 -0
- fastapi_gql_mcp-0.4.0/Comparison/bench/env_theirs/pyproject.toml +16 -0
- fastapi_gql_mcp-0.4.0/Comparison/bench/merge_results.py +81 -0
- fastapi_gql_mcp-0.4.0/Comparison/bench/results.json +252 -0
- fastapi_gql_mcp-0.4.0/Comparison/bench/results_ours.json +142 -0
- fastapi_gql_mcp-0.4.0/Comparison/bench/results_theirs.json +72 -0
- fastapi_gql_mcp-0.4.0/Comparison/bench/run_ours.py +168 -0
- fastapi_gql_mcp-0.4.0/Comparison/bench/run_theirs.py +146 -0
- fastapi_gql_mcp-0.4.0/Comparison/bench/shared_app.py +130 -0
- fastapi_gql_mcp-0.4.0/Comparison/index.html +460 -0
- fastapi_gql_mcp-0.4.0/PKG-INFO +327 -0
- fastapi_gql_mcp-0.4.0/README.md +310 -0
- fastapi_gql_mcp-0.4.0/demo/__init__.py +0 -0
- fastapi_gql_mcp-0.4.0/demo/__main__.py +27 -0
- fastapi_gql_mcp-0.4.0/demo/app.py +313 -0
- fastapi_gql_mcp-0.4.0/demo/jwt_passthrough.py +168 -0
- fastapi_gql_mcp-0.4.0/demo/mcp_walkthrough.py +104 -0
- fastapi_gql_mcp-0.4.0/demo/server.py +31 -0
- fastapi_gql_mcp-0.4.0/examples/notes_oauth/.env.example +12 -0
- fastapi_gql_mcp-0.4.0/examples/notes_oauth/README.md +166 -0
- fastapi_gql_mcp-0.4.0/examples/notes_oauth/app/__init__.py +0 -0
- fastapi_gql_mcp-0.4.0/examples/notes_oauth/app/__main__.py +37 -0
- fastapi_gql_mcp-0.4.0/examples/notes_oauth/app/auth_routes.py +171 -0
- fastapi_gql_mcp-0.4.0/examples/notes_oauth/app/config.py +27 -0
- fastapi_gql_mcp-0.4.0/examples/notes_oauth/app/credentials.py +53 -0
- fastapi_gql_mcp-0.4.0/examples/notes_oauth/app/main.py +63 -0
- fastapi_gql_mcp-0.4.0/examples/notes_oauth/app/mcp_oauth.py +85 -0
- fastapi_gql_mcp-0.4.0/examples/notes_oauth/app/models.py +45 -0
- fastapi_gql_mcp-0.4.0/examples/notes_oauth/app/notes_routes.py +90 -0
- fastapi_gql_mcp-0.4.0/examples/notes_oauth/app/observability.py +48 -0
- fastapi_gql_mcp-0.4.0/examples/notes_oauth/app/session.py +50 -0
- fastapi_gql_mcp-0.4.0/examples/notes_oauth/app/store.py +23 -0
- fastapi_gql_mcp-0.4.0/examples/notes_oauth/pyproject.toml +34 -0
- fastapi_gql_mcp-0.4.0/examples/notes_oauth/scripts/smoke.py +105 -0
- fastapi_gql_mcp-0.4.0/examples/otel_smoke.md +117 -0
- fastapi_gql_mcp-0.4.0/examples/otel_smoke.py +124 -0
- fastapi_gql_mcp-0.4.0/pyproject.toml +60 -0
- fastapi_gql_mcp-0.4.0/src/fastapi_gql_mcp/__init__.py +21 -0
- fastapi_gql_mcp-0.4.0/src/fastapi_gql_mcp/depth_guard.py +94 -0
- fastapi_gql_mcp-0.4.0/src/fastapi_gql_mcp/domains.py +146 -0
- fastapi_gql_mcp-0.4.0/src/fastapi_gql_mcp/graphiql.py +82 -0
- fastapi_gql_mcp-0.4.0/src/fastapi_gql_mcp/handler.py +183 -0
- fastapi_gql_mcp-0.4.0/src/fastapi_gql_mcp/http_api.py +89 -0
- fastapi_gql_mcp-0.4.0/src/fastapi_gql_mcp/invoker.py +336 -0
- fastapi_gql_mcp-0.4.0/src/fastapi_gql_mcp/mcp/__init__.py +0 -0
- fastapi_gql_mcp-0.4.0/src/fastapi_gql_mcp/mcp/errors.py +38 -0
- fastapi_gql_mcp-0.4.0/src/fastapi_gql_mcp/mcp/progressive_tools.py +274 -0
- fastapi_gql_mcp-0.4.0/src/fastapi_gql_mcp/mcp/server.py +274 -0
- fastapi_gql_mcp-0.4.0/src/fastapi_gql_mcp/mcp/tools.py +178 -0
- fastapi_gql_mcp-0.4.0/src/fastapi_gql_mcp/naming.py +35 -0
- fastapi_gql_mcp-0.4.0/src/fastapi_gql_mcp/scalars.py +110 -0
- fastapi_gql_mcp-0.4.0/src/fastapi_gql_mcp/scanner.py +395 -0
- fastapi_gql_mcp-0.4.0/src/fastapi_gql_mcp/schema_builder.py +290 -0
- fastapi_gql_mcp-0.4.0/src/fastapi_gql_mcp/type_builder.py +487 -0
- fastapi_gql_mcp-0.4.0/tests/__init__.py +0 -0
- fastapi_gql_mcp-0.4.0/tests/test_depth_guard.py +155 -0
- fastapi_gql_mcp-0.4.0/tests/test_domains.py +113 -0
- fastapi_gql_mcp-0.4.0/tests/test_example_public_api.py +39 -0
- fastapi_gql_mcp-0.4.0/tests/test_graphiql.py +182 -0
- fastapi_gql_mcp-0.4.0/tests/test_handler_schema.py +371 -0
- fastapi_gql_mcp-0.4.0/tests/test_invoker.py +343 -0
- fastapi_gql_mcp-0.4.0/tests/test_json_passthrough.py +102 -0
- fastapi_gql_mcp-0.4.0/tests/test_mcp_progressive.py +162 -0
- fastapi_gql_mcp-0.4.0/tests/test_mcp_simple.py +429 -0
- fastapi_gql_mcp-0.4.0/tests/test_mutation_semantics.py +70 -0
- fastapi_gql_mcp-0.4.0/tests/test_naming.py +38 -0
- fastapi_gql_mcp-0.4.0/tests/test_oauth_flow.py +234 -0
- fastapi_gql_mcp-0.4.0/tests/test_otel_propagation.py +124 -0
- fastapi_gql_mcp-0.4.0/tests/test_passthrough_headers.py +275 -0
- fastapi_gql_mcp-0.4.0/tests/test_scanner.py +354 -0
- fastapi_gql_mcp-0.4.0/tests/test_type_builder.py +257 -0
- fastapi_gql_mcp-0.4.0/uv.lock +2529 -0
|
@@ -0,0 +1,26 @@
|
|
|
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_gql_mcp/**'
|
|
14
|
+
- 'tests/**'
|
|
15
|
+
- 'demo/**'
|
|
16
|
+
- 'examples/**'
|
|
17
|
+
- 'pyproject.toml'
|
|
18
|
+
- 'uv.lock'
|
|
19
|
+
|
|
20
|
+
jobs:
|
|
21
|
+
ci-all-green:
|
|
22
|
+
name: CI All Green
|
|
23
|
+
runs-on: ubuntu-latest
|
|
24
|
+
steps:
|
|
25
|
+
- name: No code paths changed
|
|
26
|
+
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_gql_mcp/**'
|
|
7
|
+
- 'tests/**'
|
|
8
|
+
- 'demo/**'
|
|
9
|
+
- 'examples/**'
|
|
10
|
+
- 'pyproject.toml'
|
|
11
|
+
- 'uv.lock'
|
|
12
|
+
push:
|
|
13
|
+
branches: [master]
|
|
14
|
+
paths:
|
|
15
|
+
- 'src/fastapi_gql_mcp/**'
|
|
16
|
+
- 'tests/**'
|
|
17
|
+
- 'demo/**'
|
|
18
|
+
- 'examples/**'
|
|
19
|
+
- 'pyproject.toml'
|
|
20
|
+
- 'uv.lock'
|
|
21
|
+
|
|
22
|
+
jobs:
|
|
23
|
+
test:
|
|
24
|
+
runs-on: ubuntu-latest
|
|
25
|
+
strategy:
|
|
26
|
+
# One version failing must not cancel the others: a canceled job shows
|
|
27
|
+
# as failed even when its tests passed (observed: 3.13 finished green,
|
|
28
|
+
# then got canceled by 3.10's failure with fail-fast's default true).
|
|
29
|
+
fail-fast: false
|
|
30
|
+
matrix:
|
|
31
|
+
python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"]
|
|
32
|
+
|
|
33
|
+
steps:
|
|
34
|
+
- uses: actions/checkout@v5
|
|
35
|
+
|
|
36
|
+
- name: Set up Python ${{ matrix.python-version }}
|
|
37
|
+
uses: actions/setup-python@v6
|
|
38
|
+
with:
|
|
39
|
+
python-version: ${{ matrix.python-version }}
|
|
40
|
+
|
|
41
|
+
- name: Set up uv
|
|
42
|
+
uses: astral-sh/setup-uv@v7
|
|
43
|
+
with:
|
|
44
|
+
enable-cache: true
|
|
45
|
+
|
|
46
|
+
- name: Install dependencies
|
|
47
|
+
run: uv sync --all-extras
|
|
48
|
+
|
|
49
|
+
- name: Run linter
|
|
50
|
+
run: uv run ruff check src tests
|
|
51
|
+
|
|
52
|
+
- name: Run type checker
|
|
53
|
+
run: uv run mypy src
|
|
54
|
+
|
|
55
|
+
- name: Test with pytest
|
|
56
|
+
run: uv run pytest tests/ -v
|
|
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 demo.app; print(f'demo builds: {len(demo.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,42 @@
|
|
|
1
|
+
# Deploys the Comparison page (benchmark visualizations) as a static GitHub
|
|
2
|
+
# Pages site. nexusx deploys mkdocs docs the same way; here the "site" is a
|
|
3
|
+
# single self-contained Comparison/index.html, so the native Pages actions
|
|
4
|
+
# are enough — no build step. Requires Pages enabled in repo settings with
|
|
5
|
+
# source "GitHub Actions".
|
|
6
|
+
name: GH Pages Deploy
|
|
7
|
+
|
|
8
|
+
on:
|
|
9
|
+
push:
|
|
10
|
+
branches: [master]
|
|
11
|
+
paths:
|
|
12
|
+
- 'Comparison/**'
|
|
13
|
+
|
|
14
|
+
permissions:
|
|
15
|
+
contents: read
|
|
16
|
+
pages: write
|
|
17
|
+
id-token: write
|
|
18
|
+
|
|
19
|
+
concurrency:
|
|
20
|
+
group: pages
|
|
21
|
+
cancel-in-progress: true
|
|
22
|
+
|
|
23
|
+
jobs:
|
|
24
|
+
deploy:
|
|
25
|
+
runs-on: ubuntu-latest
|
|
26
|
+
environment:
|
|
27
|
+
name: github-pages
|
|
28
|
+
url: ${{ steps.deployment.outputs.page_url }}
|
|
29
|
+
steps:
|
|
30
|
+
- uses: actions/checkout@v5
|
|
31
|
+
|
|
32
|
+
- name: Configure Pages
|
|
33
|
+
uses: actions/configure-pages@v5
|
|
34
|
+
|
|
35
|
+
- name: Upload Comparison page
|
|
36
|
+
uses: actions/upload-pages-artifact@v3
|
|
37
|
+
with:
|
|
38
|
+
path: Comparison
|
|
39
|
+
|
|
40
|
+
- name: Deploy
|
|
41
|
+
id: deployment
|
|
42
|
+
uses: actions/deploy-pages@v4
|
|
@@ -0,0 +1,41 @@
|
|
|
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
|
|
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 GitHub Release
|
|
35
|
+
uses: softprops/action-gh-release@v2
|
|
36
|
+
with:
|
|
37
|
+
body: See [CHANGELOG](https://github.com/klr-pattern/fastapi-gql-mcp/blob/master/CHANGELOG.md) for details.
|
|
38
|
+
generate_release_notes: true
|
|
39
|
+
files: |
|
|
40
|
+
dist/*.tar.gz
|
|
41
|
+
dist/*.whl
|
|
@@ -0,0 +1,15 @@
|
|
|
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
|
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.4.0 (2026-10-04)
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- **Observability (OpenTelemetry), one waterfall per query**: the bridge
|
|
8
|
+
injects W3C trace context (`traceparent`/`tracestate`/`baggage`) into
|
|
9
|
+
every in-process route call — bridge-generated, so it flows regardless of
|
|
10
|
+
`passthrough_headers`, and is a no-op without an SDK (new core dependency
|
|
11
|
+
`opentelemetry-api`, non-recording by default). A `graphql.execute` span
|
|
12
|
+
covers the GraphQL orchestration layer; route-call timeouts and
|
|
13
|
+
concurrency queue waits surface as span events (`route.timeout`,
|
|
14
|
+
`route.queue`). With fastmcp's tool spans and FastAPI >= 0.142's native
|
|
15
|
+
route spans, one MCP query now lands as
|
|
16
|
+
`tools/call > graphql.execute > GET /route` in a single trace (previously
|
|
17
|
+
the route spans were orphan traces). Regression-locked by
|
|
18
|
+
`tests/test_otel_propagation.py`; walkthrough in
|
|
19
|
+
`examples/otel_smoke.md`.
|
|
20
|
+
|
|
21
|
+
- **`max_depth` (default 10, `None` disables)** on
|
|
22
|
+
`RouterGraphQLHandler` / `RouterMCP`: recursive models make selection-set
|
|
23
|
+
nesting unbounded and an MCP caller is an LLM that can emit runaway
|
|
24
|
+
documents — overly deep ones are now rejected before execution with a
|
|
25
|
+
validation-style error. Fragment spreads resolve inline (spreading a
|
|
26
|
+
deep query across fragments cannot hide it; cyclic spreads are
|
|
27
|
+
rejected), inline fragments are transparent, and the root selection set
|
|
28
|
+
counts as depth 1. `validation_rules=` on the handler passes extra
|
|
29
|
+
graphql-core validation rules through to `graphql()` for anything
|
|
30
|
+
policy-shaped. README gained a "Hardening the bridge" section covering
|
|
31
|
+
these knobs plus the fastmcp rate-limiting / response-limiting
|
|
32
|
+
middleware (per-client by default) for the MCP face.
|
|
33
|
+
|
|
34
|
+
- **`max_concurrency` (default 16, `None` disables)** on
|
|
35
|
+
`RouterGraphQLHandler` / `RouterMCP`: a bound on in-flight route calls
|
|
36
|
+
across all queries. Sibling GraphQL fields resolve concurrently, so one
|
|
37
|
+
wide query fans out into parallel ASGI calls that could hammer the
|
|
38
|
+
wrapped app's upstream (DB, external APIs); the invoker-global semaphore
|
|
39
|
+
bounds that fan-out. The slot is acquired inside the timeout window, so
|
|
40
|
+
queueing time counts against `request_timeout` — a call waiting for a
|
|
41
|
+
slot cannot outlive its own deadline.
|
|
42
|
+
|
|
43
|
+
- **`request_timeout` (default 30s, `None` disables)** on
|
|
44
|
+
`RouterGraphQLHandler` / `RouterMCP`, threaded to the invoker. Enforced
|
|
45
|
+
with `asyncio.wait_for` and surfaced as a field-level `TIMEOUT` error
|
|
46
|
+
(http_status 504) — see Fixed below for why the httpx timeout alone
|
|
47
|
+
could never fire.
|
|
48
|
+
|
|
49
|
+
- **`handler.skips`**: the scanner's skip report (path/method/reason per
|
|
50
|
+
excluded route) is now caller-visible instead of log-only — CI can
|
|
51
|
+
assert `skips == []` (or an expected set) so a route silently falling
|
|
52
|
+
out of the schema fails the build. Completes the scanner's day-one
|
|
53
|
+
"the caller decides whether skips are acceptable" contract.
|
|
54
|
+
|
|
55
|
+
- GraphiQL shows an actionable offline notice (10s) when the esm.sh CDN
|
|
56
|
+
is unreachable, pointing at the raw `POST /graphql` endpoint.
|
|
57
|
+
|
|
58
|
+
### Fixed
|
|
59
|
+
|
|
60
|
+
- **max_depth no longer rejects introspection**: `__`-prefixed meta-fields
|
|
61
|
+
don't count toward the depth guard — GraphiQL/codegen/IDE plugins ship a
|
|
62
|
+
fixed ~15-deep introspection document, which the default `max_depth=10`
|
|
63
|
+
rejected ("Error fetching schema" on a stock GraphiQL page). The guard
|
|
64
|
+
still bounds runaway DATA selections.
|
|
65
|
+
|
|
66
|
+
- `RouteInvoker(timeout=...)` was dead configuration: httpx's
|
|
67
|
+
`ASGITransport` never enforces timeouts (in-process calls bypass
|
|
68
|
+
httpcore) — a route could hang forever regardless of the setting.
|
|
69
|
+
Enforcement now lives in `asyncio.wait_for` inside `invoke()`.
|
|
70
|
+
|
|
71
|
+
- `content-type` / `accept` are refused by `filter_passthrough_headers`
|
|
72
|
+
even when whitelisted: a forwarded `content-type` would retype the JSON
|
|
73
|
+
body request (text/plain + json body → FastAPI 422). Custom headers
|
|
74
|
+
still forward as before.
|
|
75
|
+
|
|
76
|
+
### Changed
|
|
77
|
+
|
|
78
|
+
- Progressive disclosure caches each domain's SDL fragment (the schema is
|
|
79
|
+
immutable after build; agents re-explore the same domain often), and
|
|
80
|
+
`DomainRegistry` answers `children()` from a precomputed index instead
|
|
81
|
+
of a full node scan per `list_domains` call.
|
|
82
|
+
|
|
83
|
+
- `mount_to(auth_at_root=True)` now carries an explicit checklist comment
|
|
84
|
+
of the fastmcp private surface it relies upon (per-route auth guard,
|
|
85
|
+
app-level auth/request-context middleware, well-known routes) — the
|
|
86
|
+
things to re-verify on any fastmcp major bump.
|
|
87
|
+
|
|
88
|
+
- **JSON pass-through for dynamic shapes**: endpoints and fields annotated
|
|
89
|
+
`dict`, `dict[K, V]`, `Any` (and `list`s / model fields thereof) bridge
|
|
90
|
+
as the `JSON` scalar instead of being skipped — an author declaring a
|
|
91
|
+
dynamic shape gets reachability, not invisibility. The line drawn: an
|
|
92
|
+
explicit `dict`/`Any` annotation passes through; a route with no
|
|
93
|
+
annotation and no `response_model` still skips (no contract). Both
|
|
94
|
+
directions: a `JSON` input argument lands as the raw request body.
|
|
95
|
+
|
|
96
|
+
- **`RouterMCP(auth=...)`**: optional fastmcp auth provider (e.g.
|
|
97
|
+
`GitHubProvider`) passed through to `FastMCP` untouched — the MCP
|
|
98
|
+
endpoint then speaks OAuth 2.1 (401 discovery, DCR, PKCE) and clients'
|
|
99
|
+
Bearer tokens travel into route calls via `passthrough_headers` like any
|
|
100
|
+
other caller's. The bridge verifies nothing. When `auth` is set,
|
|
101
|
+
`mount_to` also re-exposes the provider's `/.well-known/*` discovery
|
|
102
|
+
routes at the host app's root — the 401 challenge advertises them there
|
|
103
|
+
(RFC 8414), but the mount alone would shift them under the mount path.
|
|
104
|
+
`mount_to(..., auth_at_root=True)` goes further: MCP endpoint at `path`,
|
|
105
|
+
OAuth/discovery routes at the host root — for reusing an IdP app whose
|
|
106
|
+
registered callback URL lives at the root domain (subdirectory
|
|
107
|
+
redirect_path). Implemented by splicing the wrapped app's routes (host
|
|
108
|
+
routes keep precedence) and copying its middleware stack wholesale — a
|
|
109
|
+
plain `Mount("")` catch-all would silently shadow host routes added
|
|
110
|
+
later, and dropping the app-level middleware (auth verification,
|
|
111
|
+
request-context capture) silently 401s every request.
|
|
112
|
+
|
|
113
|
+
- **Consumer example `examples/notes_oauth`**: a full Notes app behind
|
|
114
|
+
GitHub OAuth — browser session cookie, `POST /auth/token` Bearer, and
|
|
115
|
+
MCP OAuth 2.1 login (Claude Code) as three interchangeable credential
|
|
116
|
+
carriers, with the fastmcp GitHub proxy reusing the app's OAuth callback
|
|
117
|
+
via a redirect subdirectory (`auth_at_root`). Guarded by
|
|
118
|
+
`tests/test_example_public_api.py`: examples import the public API only.
|
|
119
|
+
|
|
120
|
+
### Fixed
|
|
121
|
+
|
|
122
|
+
- Python 3.10 support for recursive Pydantic models: CPython < 3.11 leaves
|
|
123
|
+
``list["Node"]`` with the plain string inside the PEP 585 generic (only
|
|
124
|
+
``typing.Union`` converts str args to ``ForwardRef``), and every evaluator
|
|
125
|
+
only evaluates ``ForwardRef`` — so ``FieldInfo.annotation`` stayed
|
|
126
|
+
unresolved and recursive models were skipped as unsupported types on 3.10
|
|
127
|
+
while working on 3.11+. The type builder now rewraps string args as
|
|
128
|
+
``ForwardRef`` (leaving ``Literal`` values untouched — those strings are
|
|
129
|
+
values, not types) and evaluates them against the model's namespaces,
|
|
130
|
+
seeding the namespace with the model's own name (the enclosing scope binds
|
|
131
|
+
a class name only after the class body runs, so neither module globals
|
|
132
|
+
nor pydantic's parent-namespace snapshot can resolve a self-reference).
|
|
133
|
+
|
|
134
|
+
- The demo app imported ``datetime.UTC`` (Python 3.11+), failing import on
|
|
135
|
+
3.10; it now uses ``timezone.utc``.
|
|
136
|
+
|
|
137
|
+
- A leaf field colliding with a child domain segment inside the same group
|
|
138
|
+
(e.g. a `catalog` endpoint tagged `shop` plus routes tagged
|
|
139
|
+
`shop:catalog`) was silently dropped: the child group field overwrote
|
|
140
|
+
it in the object type, with no warning — and the progressive-disclosure
|
|
141
|
+
index kept advertising the ghost field. Same-named leaves already failed
|
|
142
|
+
fast with `DuplicateFieldError`; leaf-vs-subdomain collisions now fail
|
|
143
|
+
fast the same way, naming both claimants.
|
|
144
|
+
|
|
145
|
+
- `mode="auto"` counted routes via `isinstance(app.routes, APIRoute)`,
|
|
146
|
+
which counts ZERO on FastAPI >= 0.142 (include_router results are
|
|
147
|
+
wrapped in `_IncludedRouter`) — a 30-route app stayed in simple mode,
|
|
148
|
+
handing agents one giant SDL. It also ignored `include`/`exclude`: an
|
|
149
|
+
app narrowed to 3 schema routes still switched to progressive. The
|
|
150
|
+
threshold decision now uses the scanned route count (what actually
|
|
151
|
+
enters the schema).
|
|
152
|
+
|
|
153
|
+
- Single non-model body parameters (e.g. `payload: dict[str, Any]`) were
|
|
154
|
+
wrongly wrapped as `{"payload": ...}` — FastAPI gives a lone body param
|
|
155
|
+
the WHOLE body unless `Body(embed=True)` or multiple body params are
|
|
156
|
+
involved. `_body_embeds` now replicates FastAPI's predicate exactly.
|
|
157
|
+
|
|
158
|
+
### Changed
|
|
159
|
+
|
|
160
|
+
- **`headers_provider` removed; `passthrough_headers` defaults to
|
|
161
|
+
`("authorization",)`** (breaking). Credentials now have a single source —
|
|
162
|
+
the caller: each MCP/GraphQL client connects with its own credentials and
|
|
163
|
+
the bridge forwards them (case-insensitive whitelist; an explicitly empty
|
|
164
|
+
list disables forwarding). The server-side provider (and the
|
|
165
|
+
credential-amplification risk it created when mounted publicly) is gone;
|
|
166
|
+
machines without a user context configure the service credential on the
|
|
167
|
+
MCP client side, or use `handler.execute(..., headers=...)` directly.
|
|
168
|
+
With no HTTP request context (in-memory client) protected routes simply
|
|
169
|
+
answer 401.
|
|
170
|
+
|
|
171
|
+
- **`RouterMCP.run()` is HTTP-only** (streamable HTTP with `host`/`port`
|
|
172
|
+
parameters, default `127.0.0.1:8000`). The wrapped FastAPI app is a
|
|
173
|
+
service whose routes speak HTTP, and per-caller credential passthrough
|
|
174
|
+
needs an HTTP request context that stdio has no notion of — so the stdio
|
|
175
|
+
transport and the `demo.mcp_stdio` entry point are removed (breaking).
|
|
176
|
+
Use `mount_to(app, "/mcp")` to serve MCP on the app's own port.
|
|
177
|
+
|
|
178
|
+
## 0.3.0 (2026-10-03)
|
|
179
|
+
|
|
180
|
+
Schema shape + documentation wave (breaking).
|
|
181
|
+
|
|
182
|
+
### Changed
|
|
183
|
+
|
|
184
|
+
- **Renamed from `routerql` to `fastapi-gql-mcp`** (package
|
|
185
|
+
`fastapi_gql_mcp`): the distribution name now carries both FastAPI and MCP.
|
|
186
|
+
Exception prefixes `RouterQL*` became `GQLMCP*`; `RouterMCP` /
|
|
187
|
+
`RouterGraphQLHandler` keep their Router-based names.
|
|
188
|
+
|
|
189
|
+
### Changed
|
|
190
|
+
|
|
191
|
+
- **Domain-grouped schema (UseCaseService-style hierarchy)**: fields live
|
|
192
|
+
under their tag domain tree — a route tagged `shop:catalog` answers at
|
|
193
|
+
`{ shop { catalog { list_products } } }`. Untagged routes join the domain of
|
|
194
|
+
their first path segment. Domain SDL fragments now show the exact grouped
|
|
195
|
+
address an agent should query.
|
|
196
|
+
- **GraphQL field names now come from the endpoint function name** (e.g.
|
|
197
|
+
`async def get_user` → `get_user`) instead of being reconstructed from the
|
|
198
|
+
URL path + verb. Path/query/body parameters still become the field's
|
|
199
|
+
arguments. Two routes sharing a function name fail fast with
|
|
200
|
+
`DuplicateFieldError` (previously the `_by_{param}` suffix silently
|
|
201
|
+
disambiguated collection/item pairs).
|
|
202
|
+
|
|
203
|
+
### Added
|
|
204
|
+
|
|
205
|
+
- Upgraded the optional `mcp` extra to **fastmcp 4.x** (from 3.1): zero code
|
|
206
|
+
changes needed — `FastMCP` construction, tool registration, `http_app`
|
|
207
|
+
mounting and `run()` all carried over. Verified end-to-end (tools, queries,
|
|
208
|
+
mutations over streamable HTTP) on 4.0.10.
|
|
209
|
+
- Mutation execution semantics pinned by tests: cross-domain writes are
|
|
210
|
+
serial (spec-guaranteed at the mutation root); same-domain writes run in
|
|
211
|
+
parallel. Mutation-only schemas now fail fast with guidance (GraphQL
|
|
212
|
+
requires a Query root).
|
|
213
|
+
- **Argument descriptions**: `Query()/Path()/Body(description=...)` metadata
|
|
214
|
+
maps onto GraphQL argument descriptions, completing the doc chain
|
|
215
|
+
(model docstrings → types, `Field(description)` → fields, endpoint
|
|
216
|
+
docstrings/`summary=` → fields).
|
|
217
|
+
- Runnable demo suite: `python -m demo` (all-in-one server, progressive mode),
|
|
218
|
+
`demo.mcp_stdio`, `demo.mcp_walkthrough`; full documentation coverage in the
|
|
219
|
+
demo app for inspection.
|
|
220
|
+
|
|
221
|
+
## 0.2.0 (2026-10-03)
|
|
222
|
+
|
|
223
|
+
Feature wave over the 0.1 core.
|
|
224
|
+
|
|
225
|
+
### Added
|
|
226
|
+
|
|
227
|
+
- **Progressive disclosure MCP tools** (4 layers over the tag tree):
|
|
228
|
+
`list_domains` → `list_queries(domain)` / `list_mutations(domain)` →
|
|
229
|
+
`get_query_schema(domain)` → `graphql_query`. Domain SDL fragments are
|
|
230
|
+
filtered sub-schemas (only reachable types). Discovery is scoped per domain;
|
|
231
|
+
execution always runs against the full schema. `mode="auto"` switches to
|
|
232
|
+
progressive above `progressive_threshold` (default 25, configurable).
|
|
233
|
+
- **Query Parameter Models**: a lone `Annotated[Model, Query()]` flattens into
|
|
234
|
+
individual alias-aware query arguments. Models mixed with plain query params
|
|
235
|
+
are skipped with a reason.
|
|
236
|
+
- **GraphiQL playground + GraphQL-over-HTTP**: `handler.mount_graphql(app)`
|
|
237
|
+
serves `/graphiql` (CDN build, explorer plugin) wired to `POST /graphql`
|
|
238
|
+
accepting `{query, variables, operationName}`.
|
|
239
|
+
- **`mutation_include`** glob whitelist: with `allow_mutation=True`, write
|
|
240
|
+
routes must match to become mutations.
|
|
241
|
+
- Restructured `demo/` nexusx-style.
|
|
242
|
+
|
|
243
|
+
### Fixed
|
|
244
|
+
|
|
245
|
+
- Type descriptions no longer inherit BaseModel's docstring when a model has
|
|
246
|
+
none of its own.
|
|
247
|
+
|
|
248
|
+
## 0.1.0 (2026-10-03)
|
|
249
|
+
|
|
250
|
+
Initial release.
|
|
251
|
+
|
|
252
|
+
- RouterScanner: routes → field metadata (verb mapping, dependency-tree param
|
|
253
|
+
flattening, SkipRecord taxonomy, tag domains).
|
|
254
|
+
- TypeBuilder: Pydantic → graphql-core (custom DateTime/Date/Time/UUID/Decimal
|
|
255
|
+
scalars, Literal/Enum, cycle-safe registries, alias-first naming).
|
|
256
|
+
- RouteInvoker: in-process ASGI execution with lazy lifespan management and
|
|
257
|
+
`headers_provider` credential passthrough; HTTP ≥ 400 → `GraphQLError`
|
|
258
|
+
with `extensions.code=HTTP_{status}`.
|
|
259
|
+
- SchemaBuilder + RouterGraphQLHandler on graphql-core standard execution;
|
|
260
|
+
route responses nullable per field so sibling results survive failures.
|
|
261
|
+
- RouterMCP: stdio/mount MCP server, `get_schema` + `graphql_query` /
|
|
262
|
+
`graphql_mutation` with operation-type guards and hint-chain envelopes.
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
# fastapi-gql-mcp vs fastapi-mcp 对照分析
|
|
2
|
+
|
|
3
|
+
> 对照对象:**fastapi-gql-mcp 0.3.0**(本仓库,FastAPI → GraphQL → MCP)vs
|
|
4
|
+
> **fastapi-mcp 0.4.0**([tadata-org/fastapi_mcp](https://github.com/tadata-org/fastapi_mcp) ⭐12k,FastAPI → MCP 直暴露)
|
|
5
|
+
>
|
|
6
|
+
> 完整可视化报告:[index.html](./index.html) · 基准数据与复现方法:[bench/](./bench/)
|
|
7
|
+
> 所有数字均为本机实测(2026-10-04),非估算。
|
|
8
|
+
|
|
9
|
+
## 一句话结论
|
|
10
|
+
|
|
11
|
+
两条路线解决同一个问题的不同侧面:**fastapi-mcp 把端点变成工具**(端点 = 工具,最小心智负担),
|
|
12
|
+
**fastapi-gql-mcp 把 API 变成一张可组合的类型化查询图**(schema = 契约,上下文经济性 + 组合能力)。
|
|
13
|
+
小而稳的 API 用前者足够;API 规模增长、需要给 agent 省上下文、需要组合查询/字段投影/写操作门禁/OAuth 登录体验时,后者的架构优势开始兑现。
|
|
14
|
+
|
|
15
|
+
## 架构差异(根源)
|
|
16
|
+
|
|
17
|
+
| | fastapi-mcp | fastapi-gql-mcp |
|
|
18
|
+
|---|---|---|
|
|
19
|
+
| 中间层 | 无(OpenAPI schema → 工具清单) | GraphQL schema(域树、类型、描述链) |
|
|
20
|
+
| 工具模型 | **每个端点一个工具**(N 工具) | **固定 2–6 个工具** + schema 即契约(simple/progressive 两模式) |
|
|
21
|
+
| 工具命名 | FastAPI operationId:`list_notes_api_notes_get` | 端点函数名:`list_notes` |
|
|
22
|
+
| 调用执行 | OpenAPI 参数 → 还原 HTTP 请求(ASGI 进程内) | GraphQL 执行 → 路由调用(ASGI 进程内) |
|
|
23
|
+
| 组合能力 | 无,一次调用 = 一个端点 | 一次 `graphql_query` 组合多域多字段、别名、字段投影 |
|
|
24
|
+
|
|
25
|
+
## 实测关键数字(共享同一个 notes 应用,双环境对称驱动)
|
|
26
|
+
|
|
27
|
+
### 上下文经济性(agent 需要吞下的工具目录 + schema,字节/4 ≈ tokens)
|
|
28
|
+
|
|
29
|
+
| 端点数 | fastapi-mcp 工具目录 | fastapi-gql-mcp 全量(工具+SDL) | fastapi-gql-mcp 渐进式(单域发现) |
|
|
30
|
+
|---|---|---|---|
|
|
31
|
+
| 5 | 685 tok | 796 tok | 1,239 tok |
|
|
32
|
+
| 10 | 1,219 tok | 943 tok | 1,280 tok |
|
|
33
|
+
| 25 | 2,835 tok | 1,181 tok | 1,281 tok |
|
|
34
|
+
| 50 | 5,542 tok | 1,581 tok | **1,281 tok(持平)** |
|
|
35
|
+
| 100 | **10,954 tok** | 2,381 tok | **1,281 tok(持平)** |
|
|
36
|
+
|
|
37
|
+
- fastapi-mcp 随端点数**线性增长**(100 端点 ≈ 11k tokens 只算工具目录)
|
|
38
|
+
- fastapi-gql-mcp simple 模式缓增(SDL 很紧凑),**渐进式模式上下文恒定**(单域按需发现)
|
|
39
|
+
- 诚实反例:**5 端点的小 API,fastapi-mcp 反而更省**(685 vs 796)—— 没有 N+1 组合需求时它的简单就是优势
|
|
40
|
+
|
|
41
|
+
### 往返与响应体积
|
|
42
|
+
|
|
43
|
+
| 任务 | fastapi-mcp | fastapi-gql-mcp |
|
|
44
|
+
|---|---|---|
|
|
45
|
+
| "过滤笔记 + 统计" 组合任务 | **2 次工具调用**(2 个 agent 轮次) | **1 次** `graphql_query` |
|
|
46
|
+
| 同一列表(20 条笔记)响应 | 2,875 B(全量 + indent=2) | 全量 2,341 B / **投影后 610 B(4.7×↓)** |
|
|
47
|
+
|
|
48
|
+
### 延迟(进程内微基准,200 次迭代)
|
|
49
|
+
|
|
50
|
+
| | p50 | p95 |
|
|
51
|
+
|---|---|---|
|
|
52
|
+
| fastapi-mcp 单调用 | **0.91 ms** | 1.00 ms |
|
|
53
|
+
| fastapi-gql-mcp 单查询 | 1.36 ms | 1.93 ms |
|
|
54
|
+
|
|
55
|
+
诚实结论:**单次平凡调用他们更快**(无 GraphQL 执行层)。但真实 agent 成本由**轮次**主导
|
|
56
|
+
(每轮含 LLM 推理,秒级),1ms 级差距远小于"1 轮 vs 2 轮"的差别 —— 组合能力才是省时间的杠杆。
|
|
57
|
+
|
|
58
|
+
## 定性差异(详见 index.html)
|
|
59
|
+
|
|
60
|
+
| 维度 | fastapi-mcp | fastapi-gql-mcp |
|
|
61
|
+
|---|---|---|
|
|
62
|
+
| 错误语义 | HTTP ≥400 → **整个工具调用失败** | **字段级置空** + `extensions.code=HTTP_401/404`,兄弟字段照常返回 |
|
|
63
|
+
| 写操作门禁 | 无概念(全部端点可写) | `allow_mutation` + `mutation_include` 白名单 + 工具级操作类型守卫 |
|
|
64
|
+
| 认证 | OAuth 发现/授权**代理** + 假 DCR;端点防护靠自带 FastAPI `Depends`;不验 token | `auth=` 完整 OAuth 2.1 代理(DCR+PKCE+consent+引用型 token+**端点门禁**);`passthrough_headers` 按调用者透传 |
|
|
65
|
+
| 传输 | SSE / streamable HTTP / stdio(可分离部署) | streamable HTTP(按调用者凭据透传需要 HTTP 上下文,stdio 已移除) |
|
|
66
|
+
| 人的入口 | 无 | GraphiQL + `POST /graphql`(同一 schema 服务人和 agent) |
|
|
67
|
+
| 生态兼容 | `mcp>=1.12` **无上界**,但在 mcp 2.x 上**直接崩溃**(`Server` 签名变更)——本基准被迫双环境 | `fastmcp<5` 锁上界,4.0.10 端到端验证 |
|
|
68
|
+
| 代码规模 | ~2.0k LOC,直接基于 mcp SDK | ~2.6k LOC,基于 graphql-core + fastmcp |
|
|
69
|
+
|
|
70
|
+
## 规范演进:MCP 的 lazy 机制与本对照的关系(2026-10 核实)
|
|
71
|
+
|
|
72
|
+
用户容易把几层东西混为一谈,分层核实如下(依据:本地 mcp SDK 2.3.0 内嵌协议类型 + 生态检索):
|
|
73
|
+
|
|
74
|
+
| 层 | 机制 | 状态 | 省不省 agent 上下文 |
|
|
75
|
+
|---|---|---|---|
|
|
76
|
+
| 核心规范 | `tools/list` 分页(`nextCursor`,2025-06-18 起) | 已发布 | **不省** —— 分页只是传输分块,agent 选工具仍需全量目录 |
|
|
77
|
+
| 核心规范 | `server/discover` + `cacheScope`(2026-07-28 修订) | 已发布 | 不减体积 —— 帮的是 **prompt cache 复用**(public/private 缓存语义) |
|
|
78
|
+
| 规范扩展 | **Tool Search 扩展**(`tools/search`,只留名称/摘要、按需拉取完整定义) | **draft,需客户端+服务端双边支持**,生态采纳进行中 | **省** —— 这是真正的"lazy 工具加载" |
|
|
79
|
+
| 库层 | fastmcp 4.x `SearchTransform`(Regex/BM25):整个目录折叠成 `search_tools` + `call_tool` 两个普通工具 | **今天可用**,任意 MCP 客户端(纯工具实现,无需扩展) | 省 |
|
|
80
|
+
|
|
81
|
+
**对本对照的含义(诚实修正)**:
|
|
82
|
+
|
|
83
|
+
1. fastapi-mcp 的"目录线性增长"是**开箱即用的现状**,不是永久死刑 —— 他们可以自己实现 search 折叠(其裸 mcp SDK 1.x 无现成件),或等 Tool Search 扩展普及且客户端支持
|
|
84
|
+
2. 我们库的**渐进式披露与上述 lazy 模式同型**,且是 plain-tools 实现 —— 不依赖任何扩展,今天在任何客户端上工作;即便工具目录问题未来被扩展彻底解决,**组合查询、字段投影、字段级错误隔离、写门禁仍只有 GraphQL 路线能给** —— 工具目录体积只是本对照的一个维度,不是全部
|
|
85
|
+
3. fastmcp 的 `SearchTransform` 对我们属于"备用弹药":我们的目录本来就恒定在 2–6 个工具,无需折叠;若未来 simple 模式想给超小上下文选项,可叠加
|
|
86
|
+
|
|
87
|
+
## 选型建议
|
|
88
|
+
|
|
89
|
+
- **选 fastapi-mcp**:端点少(<10)且稳定、只给 agent 用、想要零配置最快接入、认证已有 FastAPI 依赖兜底
|
|
90
|
+
- **选 fastapi-gql-mcp**:API 会长大;上下文预算紧;agent 需要一次拿到组合视图;需要字段投影控制响应体积;需要细粒度写门禁;需要 OAuth 登录的完整体验;顺便想让人类也用 GraphQL
|
|
91
|
+
|
|
92
|
+
## 复现
|
|
93
|
+
|
|
94
|
+
前提:把对照库克隆到本仓库旁边(`env_theirs` 以 path 依赖引用它):
|
|
95
|
+
|
|
96
|
+
```bash
|
|
97
|
+
gh repo clone tadata-org/fastapi_mcp ../fastapi-mcp # 对照组源码
|
|
98
|
+
cd Comparison/bench
|
|
99
|
+
uv run --project env_ours python run_ours.py # 本库侧
|
|
100
|
+
uv run --project env_theirs python run_theirs.py # fastapi-mcp 侧
|
|
101
|
+
python3 merge_results.py # 合并 → results.json
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
共享目标应用在 `shared_app.py`(同一份代码接入两个框架 —— 这也是"对接两个框架"的桥接代码本体)。
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "bench-env-ours"
|
|
3
|
+
version = "0.0.0"
|
|
4
|
+
description = "fastapi-gql-mcp side of the comparison benchmark"
|
|
5
|
+
requires-python = ">=3.10"
|
|
6
|
+
dependencies = [
|
|
7
|
+
"fastapi>=0.115",
|
|
8
|
+
"httpx>=0.27",
|
|
9
|
+
"asgi-lifespan>=2.1",
|
|
10
|
+
"fastapi-gql-mcp[mcp]>=0.3",
|
|
11
|
+
]
|
|
12
|
+
|
|
13
|
+
[tool.uv.sources]
|
|
14
|
+
fastapi-gql-mcp = { path = "../../..", editable = true }
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "bench-env-theirs"
|
|
3
|
+
version = "0.0.0"
|
|
4
|
+
description = "fastapi-mcp (tadata) side of the comparison benchmark"
|
|
5
|
+
requires-python = ">=3.10"
|
|
6
|
+
dependencies = [
|
|
7
|
+
"fastapi>=0.115",
|
|
8
|
+
"httpx>=0.27",
|
|
9
|
+
"fastapi-mcp>=0.4",
|
|
10
|
+
# fastapi-mcp 0.4.0 pins mcp>=1.12 with NO upper bound but breaks on
|
|
11
|
+
# mcp 2.x (lowlevel Server signature change) — pinned here so it runs.
|
|
12
|
+
"mcp>=1.12,<2",
|
|
13
|
+
]
|
|
14
|
+
|
|
15
|
+
[tool.uv.sources]
|
|
16
|
+
fastapi-mcp = { path = "../../../../fastapi-mcp", editable = true }
|