nene2-python 1.8.25__tar.gz → 1.8.27__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.
- {nene2_python-1.8.25 → nene2_python-1.8.27}/CHANGELOG.md +22 -0
- {nene2_python-1.8.25 → nene2_python-1.8.27}/PKG-INFO +1 -1
- nene2_python-1.8.27/docs/field-trials/2026-05-field-trial-80.md +176 -0
- nene2_python-1.8.27/docs/field-trials/2026-05-field-trial-81.md +198 -0
- {nene2_python-1.8.25 → nene2_python-1.8.27}/pyproject.toml +1 -1
- {nene2_python-1.8.25 → nene2_python-1.8.27}/src/nene2/mcp/server.py +16 -0
- {nene2_python-1.8.25 → nene2_python-1.8.27}/src/nene2/middleware/setup.py +47 -3
- nene2_python-1.8.27/tests/nene2/mcp/test_server.py +51 -0
- {nene2_python-1.8.25 → nene2_python-1.8.27}/tests/nene2/middleware/test_setup_middlewares.py +65 -0
- {nene2_python-1.8.25 → nene2_python-1.8.27}/uv.lock +1 -1
- {nene2_python-1.8.25 → nene2_python-1.8.27}/.env.example +0 -0
- {nene2_python-1.8.25 → nene2_python-1.8.27}/.github/workflows/ci.yml +0 -0
- {nene2_python-1.8.25 → nene2_python-1.8.27}/.github/workflows/docs.yml +0 -0
- {nene2_python-1.8.25 → nene2_python-1.8.27}/.github/workflows/publish.yml +0 -0
- {nene2_python-1.8.25 → nene2_python-1.8.27}/.gitignore +0 -0
- {nene2_python-1.8.25 → nene2_python-1.8.27}/.vitepress/config.mts +0 -0
- {nene2_python-1.8.25 → nene2_python-1.8.27}/.vitepress/theme/custom.css +0 -0
- {nene2_python-1.8.25 → nene2_python-1.8.27}/.vitepress/theme/index.ts +0 -0
- {nene2_python-1.8.25 → nene2_python-1.8.27}/AGENTS.md +0 -0
- {nene2_python-1.8.25 → nene2_python-1.8.27}/CLAUDE.md +0 -0
- {nene2_python-1.8.25 → nene2_python-1.8.27}/Dockerfile +0 -0
- {nene2_python-1.8.25 → nene2_python-1.8.27}/LICENSE +0 -0
- {nene2_python-1.8.25 → nene2_python-1.8.27}/README.md +0 -0
- {nene2_python-1.8.25 → nene2_python-1.8.27}/alembic/README +0 -0
- {nene2_python-1.8.25 → nene2_python-1.8.27}/alembic/env.py +0 -0
- {nene2_python-1.8.25 → nene2_python-1.8.27}/alembic/script.py.mako +0 -0
- {nene2_python-1.8.25 → nene2_python-1.8.27}/alembic/versions/001_create_notes_and_tags_tables.py +0 -0
- {nene2_python-1.8.25 → nene2_python-1.8.27}/alembic.ini +0 -0
- {nene2_python-1.8.25 → nene2_python-1.8.27}/compose.yaml +0 -0
- {nene2_python-1.8.25 → nene2_python-1.8.27}/docs/adr/0001-toolchain.md +0 -0
- {nene2_python-1.8.25 → nene2_python-1.8.27}/docs/adr/0002-clean-architecture.md +0 -0
- {nene2_python-1.8.25 → nene2_python-1.8.27}/docs/adr/0003-security-first.md +0 -0
- {nene2_python-1.8.25 → nene2_python-1.8.27}/docs/adr/0004-ai-first-design.md +0 -0
- {nene2_python-1.8.25 → nene2_python-1.8.27}/docs/adr/0005-logging.md +0 -0
- {nene2_python-1.8.25 → nene2_python-1.8.27}/docs/adr/0006-rate-limiting.md +0 -0
- {nene2_python-1.8.25 → nene2_python-1.8.27}/docs/adr/0009-mcp-design.md +0 -0
- {nene2_python-1.8.25 → nene2_python-1.8.27}/docs/adr/0010-async-use-case.md +0 -0
- {nene2_python-1.8.25 → nene2_python-1.8.27}/docs/adr/0011-mcp-as-core-dependency.md +0 -0
- {nene2_python-1.8.25 → nene2_python-1.8.27}/docs/de/index.md +0 -0
- {nene2_python-1.8.25 → nene2_python-1.8.27}/docs/de/tutorials/getting-started.md +0 -0
- {nene2_python-1.8.25 → nene2_python-1.8.27}/docs/explanation/architecture.md +0 -0
- {nene2_python-1.8.25 → nene2_python-1.8.27}/docs/explanation/design-philosophy.md +0 -0
- {nene2_python-1.8.25 → nene2_python-1.8.27}/docs/field-trials/2026-05-field-trial-1.md +0 -0
- {nene2_python-1.8.25 → nene2_python-1.8.27}/docs/field-trials/2026-05-field-trial-10.md +0 -0
- {nene2_python-1.8.25 → nene2_python-1.8.27}/docs/field-trials/2026-05-field-trial-11.md +0 -0
- {nene2_python-1.8.25 → nene2_python-1.8.27}/docs/field-trials/2026-05-field-trial-12.md +0 -0
- {nene2_python-1.8.25 → nene2_python-1.8.27}/docs/field-trials/2026-05-field-trial-13.md +0 -0
- {nene2_python-1.8.25 → nene2_python-1.8.27}/docs/field-trials/2026-05-field-trial-14.md +0 -0
- {nene2_python-1.8.25 → nene2_python-1.8.27}/docs/field-trials/2026-05-field-trial-15.md +0 -0
- {nene2_python-1.8.25 → nene2_python-1.8.27}/docs/field-trials/2026-05-field-trial-16.md +0 -0
- {nene2_python-1.8.25 → nene2_python-1.8.27}/docs/field-trials/2026-05-field-trial-17.md +0 -0
- {nene2_python-1.8.25 → nene2_python-1.8.27}/docs/field-trials/2026-05-field-trial-18.md +0 -0
- {nene2_python-1.8.25 → nene2_python-1.8.27}/docs/field-trials/2026-05-field-trial-19.md +0 -0
- {nene2_python-1.8.25 → nene2_python-1.8.27}/docs/field-trials/2026-05-field-trial-2.md +0 -0
- {nene2_python-1.8.25 → nene2_python-1.8.27}/docs/field-trials/2026-05-field-trial-20.md +0 -0
- {nene2_python-1.8.25 → nene2_python-1.8.27}/docs/field-trials/2026-05-field-trial-21.md +0 -0
- {nene2_python-1.8.25 → nene2_python-1.8.27}/docs/field-trials/2026-05-field-trial-22.md +0 -0
- {nene2_python-1.8.25 → nene2_python-1.8.27}/docs/field-trials/2026-05-field-trial-23.md +0 -0
- {nene2_python-1.8.25 → nene2_python-1.8.27}/docs/field-trials/2026-05-field-trial-24.md +0 -0
- {nene2_python-1.8.25 → nene2_python-1.8.27}/docs/field-trials/2026-05-field-trial-25.md +0 -0
- {nene2_python-1.8.25 → nene2_python-1.8.27}/docs/field-trials/2026-05-field-trial-26.md +0 -0
- {nene2_python-1.8.25 → nene2_python-1.8.27}/docs/field-trials/2026-05-field-trial-27.md +0 -0
- {nene2_python-1.8.25 → nene2_python-1.8.27}/docs/field-trials/2026-05-field-trial-28.md +0 -0
- {nene2_python-1.8.25 → nene2_python-1.8.27}/docs/field-trials/2026-05-field-trial-29.md +0 -0
- {nene2_python-1.8.25 → nene2_python-1.8.27}/docs/field-trials/2026-05-field-trial-3.md +0 -0
- {nene2_python-1.8.25 → nene2_python-1.8.27}/docs/field-trials/2026-05-field-trial-30.md +0 -0
- {nene2_python-1.8.25 → nene2_python-1.8.27}/docs/field-trials/2026-05-field-trial-31.md +0 -0
- {nene2_python-1.8.25 → nene2_python-1.8.27}/docs/field-trials/2026-05-field-trial-32.md +0 -0
- {nene2_python-1.8.25 → nene2_python-1.8.27}/docs/field-trials/2026-05-field-trial-33.md +0 -0
- {nene2_python-1.8.25 → nene2_python-1.8.27}/docs/field-trials/2026-05-field-trial-34.md +0 -0
- {nene2_python-1.8.25 → nene2_python-1.8.27}/docs/field-trials/2026-05-field-trial-35.md +0 -0
- {nene2_python-1.8.25 → nene2_python-1.8.27}/docs/field-trials/2026-05-field-trial-36.md +0 -0
- {nene2_python-1.8.25 → nene2_python-1.8.27}/docs/field-trials/2026-05-field-trial-37.md +0 -0
- {nene2_python-1.8.25 → nene2_python-1.8.27}/docs/field-trials/2026-05-field-trial-38.md +0 -0
- {nene2_python-1.8.25 → nene2_python-1.8.27}/docs/field-trials/2026-05-field-trial-39.md +0 -0
- {nene2_python-1.8.25 → nene2_python-1.8.27}/docs/field-trials/2026-05-field-trial-4.md +0 -0
- {nene2_python-1.8.25 → nene2_python-1.8.27}/docs/field-trials/2026-05-field-trial-40.md +0 -0
- {nene2_python-1.8.25 → nene2_python-1.8.27}/docs/field-trials/2026-05-field-trial-41.md +0 -0
- {nene2_python-1.8.25 → nene2_python-1.8.27}/docs/field-trials/2026-05-field-trial-42.md +0 -0
- {nene2_python-1.8.25 → nene2_python-1.8.27}/docs/field-trials/2026-05-field-trial-43.md +0 -0
- {nene2_python-1.8.25 → nene2_python-1.8.27}/docs/field-trials/2026-05-field-trial-44.md +0 -0
- {nene2_python-1.8.25 → nene2_python-1.8.27}/docs/field-trials/2026-05-field-trial-45.md +0 -0
- {nene2_python-1.8.25 → nene2_python-1.8.27}/docs/field-trials/2026-05-field-trial-46.md +0 -0
- {nene2_python-1.8.25 → nene2_python-1.8.27}/docs/field-trials/2026-05-field-trial-47.md +0 -0
- {nene2_python-1.8.25 → nene2_python-1.8.27}/docs/field-trials/2026-05-field-trial-48.md +0 -0
- {nene2_python-1.8.25 → nene2_python-1.8.27}/docs/field-trials/2026-05-field-trial-49.md +0 -0
- {nene2_python-1.8.25 → nene2_python-1.8.27}/docs/field-trials/2026-05-field-trial-5.md +0 -0
- {nene2_python-1.8.25 → nene2_python-1.8.27}/docs/field-trials/2026-05-field-trial-50.md +0 -0
- {nene2_python-1.8.25 → nene2_python-1.8.27}/docs/field-trials/2026-05-field-trial-51.md +0 -0
- {nene2_python-1.8.25 → nene2_python-1.8.27}/docs/field-trials/2026-05-field-trial-52.md +0 -0
- {nene2_python-1.8.25 → nene2_python-1.8.27}/docs/field-trials/2026-05-field-trial-53.md +0 -0
- {nene2_python-1.8.25 → nene2_python-1.8.27}/docs/field-trials/2026-05-field-trial-54.md +0 -0
- {nene2_python-1.8.25 → nene2_python-1.8.27}/docs/field-trials/2026-05-field-trial-55.md +0 -0
- {nene2_python-1.8.25 → nene2_python-1.8.27}/docs/field-trials/2026-05-field-trial-56.md +0 -0
- {nene2_python-1.8.25 → nene2_python-1.8.27}/docs/field-trials/2026-05-field-trial-57.md +0 -0
- {nene2_python-1.8.25 → nene2_python-1.8.27}/docs/field-trials/2026-05-field-trial-58.md +0 -0
- {nene2_python-1.8.25 → nene2_python-1.8.27}/docs/field-trials/2026-05-field-trial-59.md +0 -0
- {nene2_python-1.8.25 → nene2_python-1.8.27}/docs/field-trials/2026-05-field-trial-6.md +0 -0
- {nene2_python-1.8.25 → nene2_python-1.8.27}/docs/field-trials/2026-05-field-trial-60.md +0 -0
- {nene2_python-1.8.25 → nene2_python-1.8.27}/docs/field-trials/2026-05-field-trial-61.md +0 -0
- {nene2_python-1.8.25 → nene2_python-1.8.27}/docs/field-trials/2026-05-field-trial-62.md +0 -0
- {nene2_python-1.8.25 → nene2_python-1.8.27}/docs/field-trials/2026-05-field-trial-63.md +0 -0
- {nene2_python-1.8.25 → nene2_python-1.8.27}/docs/field-trials/2026-05-field-trial-64.md +0 -0
- {nene2_python-1.8.25 → nene2_python-1.8.27}/docs/field-trials/2026-05-field-trial-65.md +0 -0
- {nene2_python-1.8.25 → nene2_python-1.8.27}/docs/field-trials/2026-05-field-trial-66.md +0 -0
- {nene2_python-1.8.25 → nene2_python-1.8.27}/docs/field-trials/2026-05-field-trial-67.md +0 -0
- {nene2_python-1.8.25 → nene2_python-1.8.27}/docs/field-trials/2026-05-field-trial-68.md +0 -0
- {nene2_python-1.8.25 → nene2_python-1.8.27}/docs/field-trials/2026-05-field-trial-69.md +0 -0
- {nene2_python-1.8.25 → nene2_python-1.8.27}/docs/field-trials/2026-05-field-trial-7.md +0 -0
- {nene2_python-1.8.25 → nene2_python-1.8.27}/docs/field-trials/2026-05-field-trial-70.md +0 -0
- {nene2_python-1.8.25 → nene2_python-1.8.27}/docs/field-trials/2026-05-field-trial-71.md +0 -0
- {nene2_python-1.8.25 → nene2_python-1.8.27}/docs/field-trials/2026-05-field-trial-72.md +0 -0
- {nene2_python-1.8.25 → nene2_python-1.8.27}/docs/field-trials/2026-05-field-trial-73.md +0 -0
- {nene2_python-1.8.25 → nene2_python-1.8.27}/docs/field-trials/2026-05-field-trial-74.md +0 -0
- {nene2_python-1.8.25 → nene2_python-1.8.27}/docs/field-trials/2026-05-field-trial-75.md +0 -0
- {nene2_python-1.8.25 → nene2_python-1.8.27}/docs/field-trials/2026-05-field-trial-76.md +0 -0
- {nene2_python-1.8.25 → nene2_python-1.8.27}/docs/field-trials/2026-05-field-trial-77.md +0 -0
- {nene2_python-1.8.25 → nene2_python-1.8.27}/docs/field-trials/2026-05-field-trial-78.md +0 -0
- {nene2_python-1.8.25 → nene2_python-1.8.27}/docs/field-trials/2026-05-field-trial-79.md +0 -0
- {nene2_python-1.8.25 → nene2_python-1.8.27}/docs/field-trials/2026-05-field-trial-8.md +0 -0
- {nene2_python-1.8.25 → nene2_python-1.8.27}/docs/field-trials/2026-05-field-trial-9.md +0 -0
- {nene2_python-1.8.25 → nene2_python-1.8.27}/docs/fr/index.md +0 -0
- {nene2_python-1.8.25 → nene2_python-1.8.27}/docs/fr/tutorials/getting-started.md +0 -0
- {nene2_python-1.8.25 → nene2_python-1.8.27}/docs/how-to/add-new-domain.md +0 -0
- {nene2_python-1.8.25 → nene2_python-1.8.27}/docs/how-to/async-use-case.md +0 -0
- {nene2_python-1.8.25 → nene2_python-1.8.27}/docs/how-to/configure-auth.md +0 -0
- {nene2_python-1.8.25 → nene2_python-1.8.27}/docs/how-to/middleware-stack.md +0 -0
- {nene2_python-1.8.25 → nene2_python-1.8.27}/docs/how-to/new-project.md +0 -0
- {nene2_python-1.8.25 → nene2_python-1.8.27}/docs/how-to/problem-details.md +0 -0
- {nene2_python-1.8.25 → nene2_python-1.8.27}/docs/how-to/run-tests.md +0 -0
- {nene2_python-1.8.25 → nene2_python-1.8.27}/docs/how-to/sqlalchemy-repository.md +0 -0
- {nene2_python-1.8.25 → nene2_python-1.8.27}/docs/how-to/validation.md +0 -0
- {nene2_python-1.8.25 → nene2_python-1.8.27}/docs/howto/mcp-setup.md +0 -0
- {nene2_python-1.8.25 → nene2_python-1.8.27}/docs/index.md +0 -0
- {nene2_python-1.8.25 → nene2_python-1.8.27}/docs/ja/explanation/architecture.md +0 -0
- {nene2_python-1.8.25 → nene2_python-1.8.27}/docs/ja/explanation/design-philosophy.md +0 -0
- {nene2_python-1.8.25 → nene2_python-1.8.27}/docs/ja/how-to/add-new-domain.md +0 -0
- {nene2_python-1.8.25 → nene2_python-1.8.27}/docs/ja/how-to/configure-auth.md +0 -0
- {nene2_python-1.8.25 → nene2_python-1.8.27}/docs/ja/how-to/new-project.md +0 -0
- {nene2_python-1.8.25 → nene2_python-1.8.27}/docs/ja/how-to/run-tests.md +0 -0
- {nene2_python-1.8.25 → nene2_python-1.8.27}/docs/ja/how-to/sqlalchemy-repository.md +0 -0
- {nene2_python-1.8.25 → nene2_python-1.8.27}/docs/ja/howto/mcp-setup.md +0 -0
- {nene2_python-1.8.25 → nene2_python-1.8.27}/docs/ja/index.md +0 -0
- {nene2_python-1.8.25 → nene2_python-1.8.27}/docs/ja/reference/api.md +0 -0
- {nene2_python-1.8.25 → nene2_python-1.8.27}/docs/ja/reference/configuration.md +0 -0
- {nene2_python-1.8.25 → nene2_python-1.8.27}/docs/ja/reference/framework-modules.md +0 -0
- {nene2_python-1.8.25 → nene2_python-1.8.27}/docs/ja/tutorials/first-domain.md +0 -0
- {nene2_python-1.8.25 → nene2_python-1.8.27}/docs/ja/tutorials/getting-started.md +0 -0
- {nene2_python-1.8.25 → nene2_python-1.8.27}/docs/pt-br/index.md +0 -0
- {nene2_python-1.8.25 → nene2_python-1.8.27}/docs/pt-br/tutorials/getting-started.md +0 -0
- {nene2_python-1.8.25 → nene2_python-1.8.27}/docs/reference/api.md +0 -0
- {nene2_python-1.8.25 → nene2_python-1.8.27}/docs/reference/configuration.md +0 -0
- {nene2_python-1.8.25 → nene2_python-1.8.27}/docs/reference/framework-modules.md +0 -0
- {nene2_python-1.8.25 → nene2_python-1.8.27}/docs/roadmap.md +0 -0
- {nene2_python-1.8.25 → nene2_python-1.8.27}/docs/todo/current.md +0 -0
- {nene2_python-1.8.25 → nene2_python-1.8.27}/docs/tutorials/first-domain.md +0 -0
- {nene2_python-1.8.25 → nene2_python-1.8.27}/docs/tutorials/getting-started.md +0 -0
- {nene2_python-1.8.25 → nene2_python-1.8.27}/docs/zh/index.md +0 -0
- {nene2_python-1.8.25 → nene2_python-1.8.27}/docs/zh/tutorials/getting-started.md +0 -0
- {nene2_python-1.8.25 → nene2_python-1.8.27}/package-lock.json +0 -0
- {nene2_python-1.8.25 → nene2_python-1.8.27}/package.json +0 -0
- {nene2_python-1.8.25 → nene2_python-1.8.27}/src/example/__init__.py +0 -0
- {nene2_python-1.8.25 → nene2_python-1.8.27}/src/example/__main__.py +0 -0
- {nene2_python-1.8.25 → nene2_python-1.8.27}/src/example/app.py +0 -0
- {nene2_python-1.8.25 → nene2_python-1.8.27}/src/example/comment/__init__.py +0 -0
- {nene2_python-1.8.25 → nene2_python-1.8.27}/src/example/comment/entity.py +0 -0
- {nene2_python-1.8.25 → nene2_python-1.8.27}/src/example/comment/exceptions.py +0 -0
- {nene2_python-1.8.25 → nene2_python-1.8.27}/src/example/comment/handler.py +0 -0
- {nene2_python-1.8.25 → nene2_python-1.8.27}/src/example/comment/repository.py +0 -0
- {nene2_python-1.8.25 → nene2_python-1.8.27}/src/example/comment/sqlalchemy_repository.py +0 -0
- {nene2_python-1.8.25 → nene2_python-1.8.27}/src/example/comment/use_case.py +0 -0
- {nene2_python-1.8.25 → nene2_python-1.8.27}/src/example/mcp.py +0 -0
- {nene2_python-1.8.25 → nene2_python-1.8.27}/src/example/note/__init__.py +0 -0
- {nene2_python-1.8.25 → nene2_python-1.8.27}/src/example/note/async_use_case.py +0 -0
- {nene2_python-1.8.25 → nene2_python-1.8.27}/src/example/note/entity.py +0 -0
- {nene2_python-1.8.25 → nene2_python-1.8.27}/src/example/note/exceptions.py +0 -0
- {nene2_python-1.8.25 → nene2_python-1.8.27}/src/example/note/handler.py +0 -0
- {nene2_python-1.8.25 → nene2_python-1.8.27}/src/example/note/repository.py +0 -0
- {nene2_python-1.8.25 → nene2_python-1.8.27}/src/example/note/sqlalchemy_repository.py +0 -0
- {nene2_python-1.8.25 → nene2_python-1.8.27}/src/example/note/use_case.py +0 -0
- {nene2_python-1.8.25 → nene2_python-1.8.27}/src/example/schema.py +0 -0
- {nene2_python-1.8.25 → nene2_python-1.8.27}/src/example/tag/__init__.py +0 -0
- {nene2_python-1.8.25 → nene2_python-1.8.27}/src/example/tag/entity.py +0 -0
- {nene2_python-1.8.25 → nene2_python-1.8.27}/src/example/tag/exceptions.py +0 -0
- {nene2_python-1.8.25 → nene2_python-1.8.27}/src/example/tag/handler.py +0 -0
- {nene2_python-1.8.25 → nene2_python-1.8.27}/src/example/tag/repository.py +0 -0
- {nene2_python-1.8.25 → nene2_python-1.8.27}/src/example/tag/sqlalchemy_repository.py +0 -0
- {nene2_python-1.8.25 → nene2_python-1.8.27}/src/example/tag/use_case.py +0 -0
- {nene2_python-1.8.25 → nene2_python-1.8.27}/src/nene2/__init__.py +0 -0
- {nene2_python-1.8.25 → nene2_python-1.8.27}/src/nene2/auth/__init__.py +0 -0
- {nene2_python-1.8.25 → nene2_python-1.8.27}/src/nene2/auth/api_key.py +0 -0
- {nene2_python-1.8.25 → nene2_python-1.8.27}/src/nene2/auth/bearer_token.py +0 -0
- {nene2_python-1.8.25 → nene2_python-1.8.27}/src/nene2/auth/exceptions.py +0 -0
- {nene2_python-1.8.25 → nene2_python-1.8.27}/src/nene2/auth/interfaces.py +0 -0
- {nene2_python-1.8.25 → nene2_python-1.8.27}/src/nene2/auth/local_verifier.py +0 -0
- {nene2_python-1.8.25 → nene2_python-1.8.27}/src/nene2/config/__init__.py +0 -0
- {nene2_python-1.8.25 → nene2_python-1.8.27}/src/nene2/config/settings.py +0 -0
- {nene2_python-1.8.25 → nene2_python-1.8.27}/src/nene2/database/__init__.py +0 -0
- {nene2_python-1.8.25 → nene2_python-1.8.27}/src/nene2/database/exceptions.py +0 -0
- {nene2_python-1.8.25 → nene2_python-1.8.27}/src/nene2/database/health.py +0 -0
- {nene2_python-1.8.25 → nene2_python-1.8.27}/src/nene2/database/interfaces.py +0 -0
- {nene2_python-1.8.25 → nene2_python-1.8.27}/src/nene2/database/sqlalchemy_executor.py +0 -0
- {nene2_python-1.8.25 → nene2_python-1.8.27}/src/nene2/database/utils.py +0 -0
- {nene2_python-1.8.25 → nene2_python-1.8.27}/src/nene2/http/__init__.py +0 -0
- {nene2_python-1.8.25 → nene2_python-1.8.27}/src/nene2/http/health.py +0 -0
- {nene2_python-1.8.25 → nene2_python-1.8.27}/src/nene2/http/pagination.py +0 -0
- {nene2_python-1.8.25 → nene2_python-1.8.27}/src/nene2/http/problem_details.py +0 -0
- {nene2_python-1.8.25 → nene2_python-1.8.27}/src/nene2/log/__init__.py +0 -0
- {nene2_python-1.8.25 → nene2_python-1.8.27}/src/nene2/log/setup.py +0 -0
- {nene2_python-1.8.25 → nene2_python-1.8.27}/src/nene2/mcp/__init__.py +0 -0
- {nene2_python-1.8.25 → nene2_python-1.8.27}/src/nene2/mcp/http_client.py +0 -0
- {nene2_python-1.8.25 → nene2_python-1.8.27}/src/nene2/middleware/__init__.py +0 -0
- {nene2_python-1.8.25 → nene2_python-1.8.27}/src/nene2/middleware/domain_exception.py +0 -0
- {nene2_python-1.8.25 → nene2_python-1.8.27}/src/nene2/middleware/error_handler.py +0 -0
- {nene2_python-1.8.25 → nene2_python-1.8.27}/src/nene2/middleware/request_id.py +0 -0
- {nene2_python-1.8.25 → nene2_python-1.8.27}/src/nene2/middleware/request_logging.py +0 -0
- {nene2_python-1.8.25 → nene2_python-1.8.27}/src/nene2/middleware/request_size_limit.py +0 -0
- {nene2_python-1.8.25 → nene2_python-1.8.27}/src/nene2/middleware/security_headers.py +0 -0
- {nene2_python-1.8.25 → nene2_python-1.8.27}/src/nene2/middleware/throttle.py +0 -0
- {nene2_python-1.8.25 → nene2_python-1.8.27}/src/nene2/py.typed +0 -0
- {nene2_python-1.8.25 → nene2_python-1.8.27}/src/nene2/use_case/__init__.py +0 -0
- {nene2_python-1.8.25 → nene2_python-1.8.27}/src/nene2/use_case/protocols.py +0 -0
- {nene2_python-1.8.25 → nene2_python-1.8.27}/src/nene2/validation/__init__.py +0 -0
- {nene2_python-1.8.25 → nene2_python-1.8.27}/src/nene2/validation/exceptions.py +0 -0
- {nene2_python-1.8.25 → nene2_python-1.8.27}/src/scripts/__init__.py +0 -0
- {nene2_python-1.8.25 → nene2_python-1.8.27}/src/scripts/export_openapi.py +0 -0
- {nene2_python-1.8.25 → nene2_python-1.8.27}/tests/__init__.py +0 -0
- {nene2_python-1.8.25 → nene2_python-1.8.27}/tests/example/__init__.py +0 -0
- {nene2_python-1.8.25 → nene2_python-1.8.27}/tests/example/comment/__init__.py +0 -0
- {nene2_python-1.8.25 → nene2_python-1.8.27}/tests/example/comment/test_comment_http.py +0 -0
- {nene2_python-1.8.25 → nene2_python-1.8.27}/tests/example/comment/test_comment_repository.py +0 -0
- {nene2_python-1.8.25 → nene2_python-1.8.27}/tests/example/comment/test_comment_use_case.py +0 -0
- {nene2_python-1.8.25 → nene2_python-1.8.27}/tests/example/conftest.py +0 -0
- {nene2_python-1.8.25 → nene2_python-1.8.27}/tests/example/note/__init__.py +0 -0
- {nene2_python-1.8.25 → nene2_python-1.8.27}/tests/example/note/test_async_note_use_case.py +0 -0
- {nene2_python-1.8.25 → nene2_python-1.8.27}/tests/example/note/test_list_notes.py +0 -0
- {nene2_python-1.8.25 → nene2_python-1.8.27}/tests/example/note/test_note_repository.py +0 -0
- {nene2_python-1.8.25 → nene2_python-1.8.27}/tests/example/tag/__init__.py +0 -0
- {nene2_python-1.8.25 → nene2_python-1.8.27}/tests/example/tag/test_tag_repository.py +0 -0
- {nene2_python-1.8.25 → nene2_python-1.8.27}/tests/example/tag/test_tags.py +0 -0
- {nene2_python-1.8.25 → nene2_python-1.8.27}/tests/example/test_cors.py +0 -0
- {nene2_python-1.8.25 → nene2_python-1.8.27}/tests/example/test_mcp.py +0 -0
- {nene2_python-1.8.25 → nene2_python-1.8.27}/tests/nene2/__init__.py +0 -0
- {nene2_python-1.8.25 → nene2_python-1.8.27}/tests/nene2/auth/__init__.py +0 -0
- {nene2_python-1.8.25 → nene2_python-1.8.27}/tests/nene2/auth/test_api_key.py +0 -0
- {nene2_python-1.8.25 → nene2_python-1.8.27}/tests/nene2/auth/test_bearer_token.py +0 -0
- {nene2_python-1.8.25 → nene2_python-1.8.27}/tests/nene2/auth/test_token_issuer.py +0 -0
- {nene2_python-1.8.25 → nene2_python-1.8.27}/tests/nene2/config/__init__.py +0 -0
- {nene2_python-1.8.25 → nene2_python-1.8.27}/tests/nene2/config/test_settings.py +0 -0
- {nene2_python-1.8.25 → nene2_python-1.8.27}/tests/nene2/database/__init__.py +0 -0
- {nene2_python-1.8.25 → nene2_python-1.8.27}/tests/nene2/database/test_transaction.py +0 -0
- {nene2_python-1.8.25 → nene2_python-1.8.27}/tests/nene2/database/test_utils.py +0 -0
- {nene2_python-1.8.25 → nene2_python-1.8.27}/tests/nene2/http/__init__.py +0 -0
- {nene2_python-1.8.25 → nene2_python-1.8.27}/tests/nene2/http/test_health.py +0 -0
- {nene2_python-1.8.25 → nene2_python-1.8.27}/tests/nene2/http/test_pagination.py +0 -0
- {nene2_python-1.8.25 → nene2_python-1.8.27}/tests/nene2/http/test_problem_details.py +0 -0
- {nene2_python-1.8.25 → nene2_python-1.8.27}/tests/nene2/log/__init__.py +0 -0
- {nene2_python-1.8.25 → nene2_python-1.8.27}/tests/nene2/log/test_setup.py +0 -0
- {nene2_python-1.8.25 → nene2_python-1.8.27}/tests/nene2/mcp/__init__.py +0 -0
- {nene2_python-1.8.25 → nene2_python-1.8.27}/tests/nene2/mcp/test_http_client.py +0 -0
- {nene2_python-1.8.25 → nene2_python-1.8.27}/tests/nene2/middleware/__init__.py +0 -0
- {nene2_python-1.8.25 → nene2_python-1.8.27}/tests/nene2/middleware/test_error_handler.py +0 -0
- {nene2_python-1.8.25 → nene2_python-1.8.27}/tests/nene2/middleware/test_request_id.py +0 -0
- {nene2_python-1.8.25 → nene2_python-1.8.27}/tests/nene2/middleware/test_request_logging.py +0 -0
- {nene2_python-1.8.25 → nene2_python-1.8.27}/tests/nene2/middleware/test_request_size_limit.py +0 -0
- {nene2_python-1.8.25 → nene2_python-1.8.27}/tests/nene2/middleware/test_security_headers.py +0 -0
- {nene2_python-1.8.25 → nene2_python-1.8.27}/tests/nene2/middleware/test_simple_domain_handler.py +0 -0
- {nene2_python-1.8.25 → nene2_python-1.8.27}/tests/nene2/middleware/test_throttle.py +0 -0
- {nene2_python-1.8.25 → nene2_python-1.8.27}/tests/nene2/use_case/__init__.py +0 -0
- {nene2_python-1.8.25 → nene2_python-1.8.27}/tests/nene2/use_case/test_protocols.py +0 -0
- {nene2_python-1.8.25 → nene2_python-1.8.27}/tests/nene2/use_case/test_run_in_threadpool.py +0 -0
- {nene2_python-1.8.25 → nene2_python-1.8.27}/tests/nene2/validation/__init__.py +0 -0
- {nene2_python-1.8.25 → nene2_python-1.8.27}/tests/nene2/validation/test_exceptions.py +0 -0
- {nene2_python-1.8.25 → nene2_python-1.8.27}/tests/scripts/__init__.py +0 -0
- {nene2_python-1.8.25 → nene2_python-1.8.27}/tests/scripts/test_export_openapi.py +0 -0
|
@@ -5,6 +5,28 @@ Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
|
|
5
5
|
|
|
6
6
|
---
|
|
7
7
|
|
|
8
|
+
## [1.8.26] — 2026-05-20
|
|
9
|
+
|
|
10
|
+
FT80 フィールドトライアル — LocalMcpServer + HttpxMcpClient MCP E2E 検証と list_tools() 追加。
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- `LocalMcpServer` に `list_tools()` メソッドを追加 (#342) (FT80)
|
|
14
|
+
— 登録済みツール名の一覧を返す。デバッグ・イントロスペクション用途
|
|
15
|
+
- Field trial report: `docs/field-trials/2026-05-field-trial-80.md` (FT80)
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## [1.8.25] — 2026-05-20
|
|
20
|
+
|
|
21
|
+
FT79 フィールドトライアル — RequestLoggingMiddleware の構造化ログ検証と context_extractor 追加。
|
|
22
|
+
|
|
23
|
+
### Added
|
|
24
|
+
- `RequestLoggingMiddleware` に `context_extractor` コールバックパラメーターを追加 (#339) (FT79)
|
|
25
|
+
— リクエストごとの動的なログコンテキスト(user_id、テナントID等)を注入できる
|
|
26
|
+
- Field trial report: `docs/field-trials/2026-05-field-trial-79.md` (FT79)
|
|
27
|
+
|
|
28
|
+
---
|
|
29
|
+
|
|
8
30
|
## [1.8.24] — 2026-05-20
|
|
9
31
|
|
|
10
32
|
FT78 フィールドトライアル — ThrottleMiddleware の境界動作検証とドキュメント強化。
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: nene2-python
|
|
3
|
-
Version: 1.8.
|
|
3
|
+
Version: 1.8.27
|
|
4
4
|
Summary: NENE2 Python — minimal API framework following NENE2's design philosophy
|
|
5
5
|
Project-URL: Homepage, https://github.com/hideyukiMORI/nene2-python
|
|
6
6
|
Project-URL: Repository, https://github.com/hideyukiMORI/nene2-python
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
# FT80: MCP E2E — LocalMcpServer + HttpxMcpClient
|
|
2
|
+
|
|
3
|
+
**日付**: 2026-05-20
|
|
4
|
+
**テーマ**: LocalMcpServer にツールを登録し FastAPI アプリと連携する完全往復の検証
|
|
5
|
+
**バージョン**: v1.8.25
|
|
6
|
+
**FTディレクトリ**: `/home/xi/docker/nene2-python-FT/ft80-mcp-e2e/`
|
|
7
|
+
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
## 概要
|
|
11
|
+
|
|
12
|
+
nene2 の MCP 機能(`LocalMcpServer` / `HttpxMcpClient`)を実際に使って
|
|
13
|
+
FastAPI アプリと MCP ツールを組み合わせるパターンを検証した。
|
|
14
|
+
基本機能は期待通り動作するが、MCP ツールから型付きオブジェクトを返す際の
|
|
15
|
+
手動 JSON 変換の要求と、ツール一覧取得 API の欠如が摩擦点として判明した。
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## 実装パターン
|
|
20
|
+
|
|
21
|
+
### FastAPI + LocalMcpServer 共存
|
|
22
|
+
|
|
23
|
+
```python
|
|
24
|
+
from nene2.mcp import LocalMcpServer
|
|
25
|
+
|
|
26
|
+
app = FastAPI()
|
|
27
|
+
mcp_server = LocalMcpServer("recipe-assistant", "Recipe management assistant")
|
|
28
|
+
|
|
29
|
+
@app.post("/recipes")
|
|
30
|
+
def create_recipe(body: RecipeBody) -> JSONResponse:
|
|
31
|
+
...
|
|
32
|
+
|
|
33
|
+
@mcp_server.tool("Create a new recipe")
|
|
34
|
+
def create_new_recipe(title: str, ingredients: list[str]) -> str:
|
|
35
|
+
r = api_client.post("/recipes", json={"title": title, "ingredients": ingredients})
|
|
36
|
+
return r.text # JSON 文字列を返す
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
### McpHttpError を使ったエラーハンドリング
|
|
40
|
+
|
|
41
|
+
```python
|
|
42
|
+
@mcp_server.tool("Get a recipe by ID")
|
|
43
|
+
def get_recipe_by_id(recipe_id: int) -> str:
|
|
44
|
+
r = client.get(f"/recipes/{recipe_id}")
|
|
45
|
+
if r.status_code == 404:
|
|
46
|
+
raise McpHttpError(404, f"Recipe {recipe_id} not found")
|
|
47
|
+
return r.text
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
### HttpxMcpClient をテストトランスポートでモック
|
|
51
|
+
|
|
52
|
+
```python
|
|
53
|
+
transport = httpx.MockTransport(handler=lambda req: httpx.Response(200, json={}))
|
|
54
|
+
http_client = HttpxMcpClient(transport=transport)
|
|
55
|
+
response = http_client.get("http://testserver", "/recipes")
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
---
|
|
59
|
+
|
|
60
|
+
## 発見した問題
|
|
61
|
+
|
|
62
|
+
### 問題1: MCP ツールは文字列しか返せない(手動 JSON 変換が必要)
|
|
63
|
+
|
|
64
|
+
```python
|
|
65
|
+
# ❌ UseCase の Output dataclass を直接返せない
|
|
66
|
+
@mcp_server.tool("Get recipe")
|
|
67
|
+
def get_recipe() -> RecipeOutput: # 型エラー
|
|
68
|
+
...
|
|
69
|
+
|
|
70
|
+
# ✅ JSON 文字列に変換して返す(手動変換が必要)
|
|
71
|
+
@mcp_server.tool("Get recipe")
|
|
72
|
+
def get_recipe() -> str:
|
|
73
|
+
output = use_case.execute(input_)
|
|
74
|
+
return json.dumps(dataclasses.asdict(output))
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
UseCase の型安全性が MCP ツール境界で失われる。
|
|
78
|
+
FastAPI の `response_model` のような仕組みが MCP にはない。
|
|
79
|
+
|
|
80
|
+
### 問題2: ツール一覧取得 API がない
|
|
81
|
+
|
|
82
|
+
```python
|
|
83
|
+
mcp_server = LocalMcpServer("test-server")
|
|
84
|
+
|
|
85
|
+
@mcp_server.tool("Tool 1")
|
|
86
|
+
def tool_one() -> str:
|
|
87
|
+
return "one"
|
|
88
|
+
|
|
89
|
+
# 登録済みツールの確認手段がない
|
|
90
|
+
# mcp_server.list_tools() → 存在しない
|
|
91
|
+
# mcp_server.tools → 存在しない
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
デバッグ時に「このサーバーに何のツールが登録されているか」を確認できない。
|
|
95
|
+
|
|
96
|
+
### 問題3: Pydantic の `max_items` 非推奨警告
|
|
97
|
+
|
|
98
|
+
```python
|
|
99
|
+
# ⚠ PydanticDeprecatedSince20 警告
|
|
100
|
+
ingredients: list[str] = Field(max_items=20)
|
|
101
|
+
|
|
102
|
+
# ✅ 正しい書き方(nene2 ドキュメントに記載が必要)
|
|
103
|
+
ingredients: list[str] = Field(max_length=20)
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
nene2 のドキュメント/例に `max_items` が残っている可能性がある。
|
|
107
|
+
|
|
108
|
+
---
|
|
109
|
+
|
|
110
|
+
## テスト結果(全17件パス)
|
|
111
|
+
|
|
112
|
+
```
|
|
113
|
+
test_create_recipe_returns_201 PASSED
|
|
114
|
+
test_get_recipe_returns_200 PASSED
|
|
115
|
+
test_get_nonexistent_recipe_returns_404 PASSED
|
|
116
|
+
test_list_recipes_empty PASSED
|
|
117
|
+
test_delete_recipe_returns_204 PASSED
|
|
118
|
+
test_mcp_get_all_recipes_returns_json_string PASSED # ツールが JSON 文字列を返す
|
|
119
|
+
test_mcp_create_recipe_tool PASSED
|
|
120
|
+
test_mcp_get_recipe_by_id_found PASSED
|
|
121
|
+
test_mcp_get_recipe_by_id_not_found_raises PASSED # McpHttpError 正常動作
|
|
122
|
+
test_mcp_delete_recipe_tool PASSED
|
|
123
|
+
test_mcp_delete_nonexistent_raises PASSED
|
|
124
|
+
test_mcp_server_is_importable PASSED
|
|
125
|
+
test_mcp_server_tool_registration PASSED
|
|
126
|
+
test_http_mcp_client_with_test_transport PASSED # MockTransport で DI テスト
|
|
127
|
+
test_http_mcp_client_raise_for_error PASSED
|
|
128
|
+
test_friction_mcp_tool_cannot_return_typed_object PASSED
|
|
129
|
+
test_friction_no_mcp_tool_discovery_api PASSED
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
---
|
|
133
|
+
|
|
134
|
+
## 摩擦ポイント一覧
|
|
135
|
+
|
|
136
|
+
| ID | 内容 | 深刻度 |
|
|
137
|
+
|---|---|---|
|
|
138
|
+
| F80-1 | MCP ツールは文字列を返す必要があり、UseCase の型付き Output を直接渡せない | 中 |
|
|
139
|
+
| F80-2 | LocalMcpServer に登録済みツールの一覧取得 API がない | 低 |
|
|
140
|
+
| F80-3 | Pydantic v2 の `max_items` 非推奨(`max_length` に変更必要) | 低 |
|
|
141
|
+
|
|
142
|
+
---
|
|
143
|
+
|
|
144
|
+
## 使用感(主観評価)
|
|
145
|
+
|
|
146
|
+
### 直感性 ★★★★☆
|
|
147
|
+
|
|
148
|
+
`@mcp_server.tool("description")` デコレーターは非常に直感的で、
|
|
149
|
+
FastAPI の `@app.get("/path")` と同じ感覚で使える。
|
|
150
|
+
`McpHttpError` と `raise_for_error()` のパターンも明瞭。
|
|
151
|
+
|
|
152
|
+
### 実害の深刻さ ★★☆☆☆
|
|
153
|
+
|
|
154
|
+
文字列変換の必要性は設計上仕方ない(MCP プロトコルの制約)。
|
|
155
|
+
`json.dumps(dataclasses.asdict(output))` は1行で書けるので実際の摩擦は小さい。
|
|
156
|
+
ツール一覧確認はデバッグ時の不便さのみで、本番には影響しない。
|
|
157
|
+
|
|
158
|
+
### 修正のしやすさ ★★★★★
|
|
159
|
+
|
|
160
|
+
どれも小さな改善で対応できる:
|
|
161
|
+
- `list_tools()` メソッドを `LocalMcpServer` に追加するだけ
|
|
162
|
+
- ドキュメントに `json.dumps(dataclasses.asdict(output))` のパターンを記載するだけ
|
|
163
|
+
|
|
164
|
+
### 総合コメント
|
|
165
|
+
|
|
166
|
+
MCP 機能は他の Python フレームワークにはない nene2 の差別化ポイント。
|
|
167
|
+
FastAPI との共存パターン、`McpHttpError` のエラーハンドリング、
|
|
168
|
+
`HttpxMcpClient` の `MockTransport` DI テストパターンは非常によくできている。
|
|
169
|
+
「他のフレームワークに乗り換えようと思わない」と思わせる独自の価値がある。
|
|
170
|
+
|
|
171
|
+
---
|
|
172
|
+
|
|
173
|
+
## 推奨アクション
|
|
174
|
+
|
|
175
|
+
1. **Issue**: `LocalMcpServer` に `list_tools()` メソッドを追加(デバッグ用)
|
|
176
|
+
2. **minor**: Pydantic v2 の `max_items` → `max_length` への更新(ドキュメント確認)
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
# FT81: CORS 設定 — setup_middlewares() と CORSMiddleware の組み合わせ
|
|
2
|
+
|
|
3
|
+
**日付**: 2026-05-20
|
|
4
|
+
**テーマ**: setup_middlewares() に CORS サポートがない場合の正しい設定パターン検証
|
|
5
|
+
**バージョン**: v1.8.26
|
|
6
|
+
**FTディレクトリ**: `/home/xi/docker/nene2-python-FT/ft81-cors/`
|
|
7
|
+
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
## 概要
|
|
11
|
+
|
|
12
|
+
nene2 の `setup_middlewares()` は CORS をサポートしていないため、
|
|
13
|
+
ブラウザから API を呼び出すアプリで CORS が必要になった際に
|
|
14
|
+
ユーザーは `CORSMiddleware` を手動で追加する必要がある。
|
|
15
|
+
その際、Starlette の LIFO ミドルウェア順序を理解していないと
|
|
16
|
+
OPTIONS プリフライトが正常に動作しない問題を確認した。
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
## 実装パターン
|
|
21
|
+
|
|
22
|
+
### 正しい CORS 設定(CORS を最外側に配置)
|
|
23
|
+
|
|
24
|
+
```python
|
|
25
|
+
from fastapi import FastAPI
|
|
26
|
+
from fastapi.middleware.cors import CORSMiddleware
|
|
27
|
+
from nene2.middleware import setup_middlewares
|
|
28
|
+
|
|
29
|
+
ALLOWED_ORIGINS = [
|
|
30
|
+
"https://app.example.com",
|
|
31
|
+
"https://admin.example.com",
|
|
32
|
+
]
|
|
33
|
+
|
|
34
|
+
app = FastAPI()
|
|
35
|
+
|
|
36
|
+
# ✅ CORS を先に add_middleware → setup_middlewares() 後は LIFO で最外側になる
|
|
37
|
+
app.add_middleware(
|
|
38
|
+
CORSMiddleware,
|
|
39
|
+
allow_origins=ALLOWED_ORIGINS,
|
|
40
|
+
allow_methods=["GET", "POST", "PUT", "DELETE", "OPTIONS"],
|
|
41
|
+
allow_headers=["Authorization", "Content-Type"],
|
|
42
|
+
allow_credentials=True,
|
|
43
|
+
)
|
|
44
|
+
setup_middlewares(app)
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
### 間違ったパターン(CORS が内側になる)
|
|
48
|
+
|
|
49
|
+
```python
|
|
50
|
+
# ❌ setup_middlewares() の後に CORS を追加すると内側に入る
|
|
51
|
+
app = FastAPI()
|
|
52
|
+
setup_middlewares(app)
|
|
53
|
+
app.add_middleware(CORSMiddleware, allow_origins=["https://app.example.com"])
|
|
54
|
+
# → OPTIONS プリフライトが nene2 ミドルウェアに遮断される可能性がある
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
### 禁止パターン(CLAUDE.md ポリシー違反)
|
|
58
|
+
|
|
59
|
+
```python
|
|
60
|
+
# ❌ CLAUDE.md 明示禁止: allow_origins=["*"]
|
|
61
|
+
app.add_middleware(
|
|
62
|
+
CORSMiddleware,
|
|
63
|
+
allow_origins=["*"], # セキュリティリスク
|
|
64
|
+
)
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
---
|
|
68
|
+
|
|
69
|
+
## 発見した問題
|
|
70
|
+
|
|
71
|
+
### 問題1: setup_middlewares() に CORS パラメーターがない
|
|
72
|
+
|
|
73
|
+
```python
|
|
74
|
+
# CORS が必要でも setup_middlewares() のシグネチャに cors パラメーターなし
|
|
75
|
+
setup_middlewares(
|
|
76
|
+
app,
|
|
77
|
+
# cors_allowed_origins=["https://app.example.com"], # 存在しない
|
|
78
|
+
)
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
ユーザーは `FastAPI.add_middleware(CORSMiddleware, ...)` を直接呼ぶ必要がある。
|
|
82
|
+
FastAPI/Starlette のドキュメントを参照しなければ方法がわからない。
|
|
83
|
+
|
|
84
|
+
### 問題2: ミドルウェア順序が直感に反する(LIFO)
|
|
85
|
+
|
|
86
|
+
```python
|
|
87
|
+
# Starlette は LIFO — 最後に add_middleware したものが最外側になる
|
|
88
|
+
# つまり CORS を「最外側にしたい」なら「最初に add する」
|
|
89
|
+
|
|
90
|
+
app.add_middleware(CORSMiddleware, ...) # ← 先に追加 = 最外側(正しい)
|
|
91
|
+
setup_middlewares(app) # ← 後から追加 = 内側
|
|
92
|
+
|
|
93
|
+
# 逆にすると:
|
|
94
|
+
setup_middlewares(app) # ← 先に追加 = 最内側(危険)
|
|
95
|
+
app.add_middleware(CORSMiddleware, ...) # ← 後から追加 = 最外側になってしまう
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
「最外側に置きたいなら先に add する」という反直感的な順序。
|
|
99
|
+
|
|
100
|
+
### 問題3: nene2 が allow_origins=["*"] を禁止しない
|
|
101
|
+
|
|
102
|
+
```python
|
|
103
|
+
# CLAUDE.md で明示禁止されているが、nene2 フレームワークは検証しない
|
|
104
|
+
app.add_middleware(
|
|
105
|
+
CORSMiddleware,
|
|
106
|
+
allow_origins=["*"], # 禁止ポリシーだが動作してしまう
|
|
107
|
+
)
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
フレームワークレベルで `ValueError` を raise することも可能だが、
|
|
111
|
+
`setup_middlewares()` を経由しない場合は検証できない。
|
|
112
|
+
|
|
113
|
+
### 問題4: 複数オリジン・credentials 設定パターンがドキュメントにない
|
|
114
|
+
|
|
115
|
+
本番アプリでは複数オリジン(本番環境 + ステージング環境)や
|
|
116
|
+
`allow_credentials=True` が必要なケースが多いが、
|
|
117
|
+
nene2 のドキュメントにこのパターンの記載がない。
|
|
118
|
+
|
|
119
|
+
---
|
|
120
|
+
|
|
121
|
+
## テスト結果(全13件パス)
|
|
122
|
+
|
|
123
|
+
```
|
|
124
|
+
test_list_items_returns_200 PASSED
|
|
125
|
+
test_create_item_returns_201 PASSED
|
|
126
|
+
test_cors_allowed_origin_returns_access_control_header PASSED
|
|
127
|
+
test_cors_disallowed_origin_no_access_control_header PASSED
|
|
128
|
+
test_cors_preflight_options_returns_200 PASSED # OPTIONS プリフライト正常動作
|
|
129
|
+
test_cors_preflight_disallowed_origin PASSED
|
|
130
|
+
test_cors_credentials_allowed PASSED
|
|
131
|
+
test_security_headers_present_with_cors PASSED # nene2 ミドルウェアと共存
|
|
132
|
+
test_request_id_present_with_cors PASSED # X-Request-Id と共存
|
|
133
|
+
test_friction_no_cors_in_setup_middlewares PASSED # 摩擦: CORS パラメーターなし
|
|
134
|
+
test_friction_cors_order_matters PASSED # 摩擦: LIFO 順序問題
|
|
135
|
+
test_friction_wildcard_origin_is_insecure PASSED # 摩擦: ["*"] を止めない
|
|
136
|
+
test_friction_multiple_origins_not_documented PASSED # 摩擦: ドキュメント不足
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
---
|
|
140
|
+
|
|
141
|
+
## 摩擦ポイント一覧
|
|
142
|
+
|
|
143
|
+
| ID | 内容 | 深刻度 |
|
|
144
|
+
|---|---|---|
|
|
145
|
+
| F81-1 | `setup_middlewares()` に CORS パラメーターがなく手動追加が必要 | 中 |
|
|
146
|
+
| F81-2 | Starlette の LIFO 順序を知らないと OPTIONS プリフライトが壊れる | 中 |
|
|
147
|
+
| F81-3 | `allow_origins=["*"]` を nene2 が禁止しない(ポリシーのみ) | 低 |
|
|
148
|
+
| F81-4 | 複数オリジン・credentials パターンがドキュメントに未記載 | 低 |
|
|
149
|
+
|
|
150
|
+
---
|
|
151
|
+
|
|
152
|
+
## 使用感(主観評価)
|
|
153
|
+
|
|
154
|
+
### 直感性 ★★★☆☆
|
|
155
|
+
|
|
156
|
+
`setup_middlewares()` を使うと CORS は自分で追加しなければならず、
|
|
157
|
+
しかも「先に add する = 最外側になる」という反直感的な順序ルールがある。
|
|
158
|
+
FastAPI や Express.js、Spring Security の CORS 設定を知っているユーザーでも
|
|
159
|
+
nene2 特有の LIFO 順序で一度はつまずく。
|
|
160
|
+
|
|
161
|
+
### 実害の深刻さ ★★★☆☆
|
|
162
|
+
|
|
163
|
+
ブラウザからの CORS エラーは「API が動かない」として即座に表面化する。
|
|
164
|
+
原因が「ミドルウェア順序」であることを特定するのに時間がかかることがある。
|
|
165
|
+
特に OPTIONS プリフライトが通らないと PUT/DELETE/POST with Auth が全滅する。
|
|
166
|
+
|
|
167
|
+
### 修正のしやすさ ★★★★★
|
|
168
|
+
|
|
169
|
+
`setup_middlewares()` に `cors_allowed_origins` パラメーターを追加するだけ。
|
|
170
|
+
CORS は最外側(最初の add_middleware)に固定できるため、
|
|
171
|
+
ユーザーが順序を気にする必要がなくなる。
|
|
172
|
+
|
|
173
|
+
```python
|
|
174
|
+
# 理想の API:
|
|
175
|
+
setup_middlewares(
|
|
176
|
+
app,
|
|
177
|
+
cors_allowed_origins=["https://app.example.com"],
|
|
178
|
+
cors_allow_credentials=True,
|
|
179
|
+
)
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
### 総合コメント
|
|
183
|
+
|
|
184
|
+
CORS は「作ったら必ず必要になる」機能でありながら、
|
|
185
|
+
nene2 の `setup_middlewares()` には組み込まれていない。
|
|
186
|
+
`["*"]` 禁止は CLAUDE.md のポリシーとして正しいが、
|
|
187
|
+
フレームワークが強制しないと誰かが違反する。
|
|
188
|
+
`cors_allowed_origins` を追加してワイルドカードを `ValueError` にすれば
|
|
189
|
+
セキュリティポリシーをコードで強制できる。
|
|
190
|
+
|
|
191
|
+
---
|
|
192
|
+
|
|
193
|
+
## 推奨アクション
|
|
194
|
+
|
|
195
|
+
1. **Issue**: `setup_middlewares()` に `cors_allowed_origins` パラメーターを追加
|
|
196
|
+
— `allow_origins=["*"]` を渡した場合に `ValueError` を raise
|
|
197
|
+
— CORS を最外側に自動配置(ユーザーが順序を意識しなくていい)
|
|
198
|
+
2. **docs**: 複数オリジン・credentials のパターンを how-to ガイドに追加
|
|
@@ -27,6 +27,22 @@ class LocalMcpServer:
|
|
|
27
27
|
"""Register a function as an MCP tool."""
|
|
28
28
|
return self._mcp.tool(description=description)
|
|
29
29
|
|
|
30
|
+
def list_tools(self) -> list[str]:
|
|
31
|
+
"""Return the names of all registered tools.
|
|
32
|
+
|
|
33
|
+
Useful for debugging and introspection::
|
|
34
|
+
|
|
35
|
+
server = LocalMcpServer("my-server")
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@server.tool("Get data")
|
|
39
|
+
def get_data() -> str: ...
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
print(server.list_tools()) # ["get_data"]
|
|
43
|
+
"""
|
|
44
|
+
return [t.name for t in self._mcp._tool_manager.list_tools()]
|
|
45
|
+
|
|
30
46
|
def run(self, transport: Literal["stdio", "sse", "streamable-http"] = "stdio") -> None:
|
|
31
47
|
"""Start the MCP server. Blocks until the client disconnects."""
|
|
32
48
|
self._mcp.run(transport=transport)
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
from typing import Any
|
|
4
4
|
|
|
5
5
|
from starlette.applications import Starlette
|
|
6
|
+
from starlette.middleware.cors import CORSMiddleware
|
|
6
7
|
|
|
7
8
|
from .domain_exception import DomainExceptionHandlerProtocol
|
|
8
9
|
from .error_handler import ErrorHandlerMiddleware
|
|
@@ -15,6 +16,10 @@ from .throttle import ThrottleMiddleware
|
|
|
15
16
|
_DEFAULT_MAX_BYTES = 1_048_576 # 1 MiB
|
|
16
17
|
|
|
17
18
|
|
|
19
|
+
_CORS_ALLOW_METHODS = ["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"]
|
|
20
|
+
_CORS_ALLOW_HEADERS = ["Authorization", "Content-Type"]
|
|
21
|
+
|
|
22
|
+
|
|
18
23
|
def setup_middlewares(
|
|
19
24
|
app: object,
|
|
20
25
|
*,
|
|
@@ -32,6 +37,10 @@ def setup_middlewares(
|
|
|
32
37
|
hsts: bool = False,
|
|
33
38
|
csp: str | None = None,
|
|
34
39
|
security_extra_no_csp_paths: list[str] | None = None,
|
|
40
|
+
cors_allowed_origins: list[str] | None = None,
|
|
41
|
+
cors_allow_credentials: bool = False,
|
|
42
|
+
cors_allow_methods: list[str] | None = None,
|
|
43
|
+
cors_allow_headers: list[str] | None = None,
|
|
35
44
|
) -> None:
|
|
36
45
|
"""Register all nene2 middlewares in the correct order.
|
|
37
46
|
|
|
@@ -41,7 +50,7 @@ def setup_middlewares(
|
|
|
41
50
|
|
|
42
51
|
Effective stack (outermost → innermost)::
|
|
43
52
|
|
|
44
|
-
RequestId → SecurityHeaders → SizeLimit → Throttle → RequestLogging → ErrorHandler
|
|
53
|
+
CORS → RequestId → SecurityHeaders → SizeLimit → Throttle → RequestLogging → ErrorHandler
|
|
45
54
|
|
|
46
55
|
**Minimal usage** — all options have sensible defaults::
|
|
47
56
|
|
|
@@ -50,6 +59,14 @@ def setup_middlewares(
|
|
|
50
59
|
app = FastAPI()
|
|
51
60
|
setup_middlewares(app)
|
|
52
61
|
|
|
62
|
+
**With CORS** (explicit origins required — wildcards are rejected)::
|
|
63
|
+
|
|
64
|
+
setup_middlewares(
|
|
65
|
+
app,
|
|
66
|
+
cors_allowed_origins=["https://app.example.com"],
|
|
67
|
+
cors_allow_credentials=True,
|
|
68
|
+
)
|
|
69
|
+
|
|
53
70
|
**With customisation**::
|
|
54
71
|
|
|
55
72
|
setup_middlewares(
|
|
@@ -103,13 +120,29 @@ def setup_middlewares(
|
|
|
103
120
|
hsts: Enable Strict-Transport-Security header (default: False).
|
|
104
121
|
csp: Custom Content-Security-Policy value. Defaults to nene2's built-in policy.
|
|
105
122
|
security_extra_no_csp_paths: Additional paths to skip CSP (on top of /docs, /redoc).
|
|
123
|
+
cors_allowed_origins: Explicit list of allowed CORS origins.
|
|
124
|
+
Pass ``None`` (default) to skip :class:`CORSMiddleware`.
|
|
125
|
+
Passing ``["*"]`` raises :exc:`ValueError` — wildcard origins are forbidden
|
|
126
|
+
per nene2 security policy.
|
|
127
|
+
cors_allow_credentials: Allow cookies and ``Authorization`` headers in CORS
|
|
128
|
+
requests (default: False).
|
|
129
|
+
cors_allow_methods: HTTP methods exposed via CORS
|
|
130
|
+
(default: GET, POST, PUT, PATCH, DELETE, OPTIONS).
|
|
131
|
+
cors_allow_headers: Request headers exposed via CORS
|
|
132
|
+
(default: Authorization, Content-Type).
|
|
106
133
|
"""
|
|
107
134
|
if not isinstance(app, Starlette):
|
|
108
135
|
raise TypeError(f"app must be a Starlette/FastAPI instance, got {type(app)!r}")
|
|
109
136
|
|
|
137
|
+
if cors_allowed_origins is not None and "*" in cors_allowed_origins:
|
|
138
|
+
raise ValueError(
|
|
139
|
+
"cors_allowed_origins must not contain '*'. "
|
|
140
|
+
"wildcard CORS origins are forbidden — list explicit origins instead."
|
|
141
|
+
)
|
|
142
|
+
|
|
110
143
|
# Add in reverse order — first added = innermost, last added = outermost.
|
|
111
144
|
# Desired outermost → innermost:
|
|
112
|
-
# RequestId → SecurityHeaders → SizeLimit → Throttle → RequestLogging → ErrorHandler
|
|
145
|
+
# CORS → RequestId → SecurityHeaders → SizeLimit → Throttle → RequestLogging → ErrorHandler
|
|
113
146
|
|
|
114
147
|
# 1. Innermost: ErrorHandlerMiddleware (also registers RequestValidationError handler)
|
|
115
148
|
ErrorHandlerMiddleware.install(app, debug=debug, domain_handlers=domain_handlers)
|
|
@@ -144,5 +177,16 @@ def setup_middlewares(
|
|
|
144
177
|
sec_kwargs["extra_no_csp_paths"] = security_extra_no_csp_paths
|
|
145
178
|
app.add_middleware(SecurityHeadersMiddleware, **sec_kwargs)
|
|
146
179
|
|
|
147
|
-
# 6.
|
|
180
|
+
# 6. RequestIdMiddleware
|
|
148
181
|
app.add_middleware(RequestIdMiddleware)
|
|
182
|
+
|
|
183
|
+
# 7. Outermost: CORSMiddleware (optional) — must be outermost so OPTIONS preflight
|
|
184
|
+
# responses are handled before any other middleware processes the request.
|
|
185
|
+
if cors_allowed_origins is not None:
|
|
186
|
+
app.add_middleware(
|
|
187
|
+
CORSMiddleware,
|
|
188
|
+
allow_origins=cors_allowed_origins,
|
|
189
|
+
allow_credentials=cors_allow_credentials,
|
|
190
|
+
allow_methods=cors_allow_methods or _CORS_ALLOW_METHODS,
|
|
191
|
+
allow_headers=cors_allow_headers or _CORS_ALLOW_HEADERS,
|
|
192
|
+
)
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"""Tests for LocalMcpServer."""
|
|
2
|
+
|
|
3
|
+
from nene2.mcp import LocalMcpServer
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def _make_server_with_tools() -> LocalMcpServer:
|
|
7
|
+
server = LocalMcpServer("test-server")
|
|
8
|
+
|
|
9
|
+
@server.tool("Get data")
|
|
10
|
+
def get_data() -> str:
|
|
11
|
+
return "data"
|
|
12
|
+
|
|
13
|
+
@server.tool("Post data")
|
|
14
|
+
def post_data(value: str) -> str:
|
|
15
|
+
return f"posted: {value}"
|
|
16
|
+
|
|
17
|
+
return server
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def test_list_tools_returns_registered_names() -> None:
|
|
21
|
+
server = _make_server_with_tools()
|
|
22
|
+
tools = server.list_tools()
|
|
23
|
+
assert "get_data" in tools
|
|
24
|
+
assert "post_data" in tools
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def test_list_tools_empty_server() -> None:
|
|
28
|
+
server = LocalMcpServer("empty-server")
|
|
29
|
+
assert server.list_tools() == []
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def test_list_tools_count() -> None:
|
|
33
|
+
server = _make_server_with_tools()
|
|
34
|
+
assert len(server.list_tools()) == 2
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def test_list_tools_returns_list_of_str() -> None:
|
|
38
|
+
server = _make_server_with_tools()
|
|
39
|
+
tools = server.list_tools()
|
|
40
|
+
assert all(isinstance(name, str) for name in tools)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def test_tool_function_still_callable() -> None:
|
|
44
|
+
"""tool() デコレーターが関数の呼び出し可能性を壊さない。"""
|
|
45
|
+
server = LocalMcpServer("callable-test")
|
|
46
|
+
|
|
47
|
+
@server.tool("Square")
|
|
48
|
+
def square(x: int) -> str:
|
|
49
|
+
return str(x * x)
|
|
50
|
+
|
|
51
|
+
assert square(3) == "9"
|
{nene2_python-1.8.25 → nene2_python-1.8.27}/tests/nene2/middleware/test_setup_middlewares.py
RENAMED
|
@@ -122,3 +122,68 @@ def test_pydantic_422_formatted_as_nene2() -> None:
|
|
|
122
122
|
def test_raises_type_error_for_non_starlette_app() -> None:
|
|
123
123
|
with pytest.raises(TypeError, match="Starlette/FastAPI"):
|
|
124
124
|
setup_middlewares(object())
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def test_cors_allowed_origin_returns_access_control_header() -> None:
|
|
128
|
+
app = _make_app(cors_allowed_origins=["https://app.example.com"])
|
|
129
|
+
client = TestClient(app, raise_server_exceptions=False)
|
|
130
|
+
r = client.get("/ok", headers={"Origin": "https://app.example.com"})
|
|
131
|
+
assert r.headers.get("access-control-allow-origin") == "https://app.example.com"
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def test_cors_disallowed_origin_no_header() -> None:
|
|
135
|
+
app = _make_app(cors_allowed_origins=["https://app.example.com"])
|
|
136
|
+
client = TestClient(app, raise_server_exceptions=False)
|
|
137
|
+
r = client.get("/ok", headers={"Origin": "https://evil.example.com"})
|
|
138
|
+
assert "access-control-allow-origin" not in r.headers
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def test_cors_preflight_options_returns_200() -> None:
|
|
142
|
+
app = _make_app(cors_allowed_origins=["https://app.example.com"])
|
|
143
|
+
client = TestClient(app, raise_server_exceptions=False)
|
|
144
|
+
r = client.options(
|
|
145
|
+
"/ok",
|
|
146
|
+
headers={
|
|
147
|
+
"Origin": "https://app.example.com",
|
|
148
|
+
"Access-Control-Request-Method": "GET",
|
|
149
|
+
},
|
|
150
|
+
)
|
|
151
|
+
assert r.headers.get("access-control-allow-origin") == "https://app.example.com"
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def test_cors_wildcard_origin_raises_value_error() -> None:
|
|
155
|
+
with pytest.raises(ValueError, match="wildcard"):
|
|
156
|
+
_make_app(cors_allowed_origins=["*"])
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def test_cors_none_means_no_cors_middleware() -> None:
|
|
160
|
+
app = _make_app(cors_allowed_origins=None)
|
|
161
|
+
client = TestClient(app, raise_server_exceptions=False)
|
|
162
|
+
r = client.get("/ok", headers={"Origin": "https://app.example.com"})
|
|
163
|
+
assert "access-control-allow-origin" not in r.headers
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def test_cors_credentials_can_be_enabled() -> None:
|
|
167
|
+
app = _make_app(
|
|
168
|
+
cors_allowed_origins=["https://app.example.com"],
|
|
169
|
+
cors_allow_credentials=True,
|
|
170
|
+
)
|
|
171
|
+
client = TestClient(app, raise_server_exceptions=False)
|
|
172
|
+
r = client.get("/ok", headers={"Origin": "https://app.example.com"})
|
|
173
|
+
assert r.headers.get("access-control-allow-credentials") == "true"
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def test_cors_request_id_still_present() -> None:
|
|
177
|
+
"""CORS ミドルウェアと X-Request-Id が共存する。"""
|
|
178
|
+
app = _make_app(cors_allowed_origins=["https://app.example.com"])
|
|
179
|
+
client = TestClient(app, raise_server_exceptions=False)
|
|
180
|
+
r = client.get("/ok", headers={"Origin": "https://app.example.com"})
|
|
181
|
+
assert "x-request-id" in r.headers
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def test_cors_security_headers_still_present() -> None:
|
|
185
|
+
"""CORS ミドルウェアとセキュリティヘッダーが共存する。"""
|
|
186
|
+
app = _make_app(cors_allowed_origins=["https://app.example.com"])
|
|
187
|
+
client = TestClient(app, raise_server_exceptions=False)
|
|
188
|
+
r = client.get("/ok", headers={"Origin": "https://app.example.com"})
|
|
189
|
+
assert r.headers.get("x-content-type-options") == "nosniff"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|