nene2-python 1.0.0__tar.gz → 1.2.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.
- {nene2_python-1.0.0 → nene2_python-1.2.0}/PKG-INFO +1 -1
- nene2_python-1.2.0/docs/field-trials/2026-05-field-trial-7.md +170 -0
- nene2_python-1.2.0/docs/field-trials/2026-05-field-trial-8.md +218 -0
- nene2_python-1.2.0/docs/field-trials/2026-05-field-trial-9.md +151 -0
- {nene2_python-1.0.0 → nene2_python-1.2.0}/docs/how-to/sqlalchemy-repository.md +176 -20
- {nene2_python-1.0.0 → nene2_python-1.2.0}/pyproject.toml +1 -1
- {nene2_python-1.0.0 → nene2_python-1.2.0}/src/nene2/database/__init__.py +2 -0
- nene2_python-1.2.0/src/nene2/database/utils.py +29 -0
- {nene2_python-1.0.0 → nene2_python-1.2.0}/src/nene2/mcp/__init__.py +2 -1
- {nene2_python-1.0.0 → nene2_python-1.2.0}/src/nene2/mcp/http_client.py +22 -0
- {nene2_python-1.0.0 → nene2_python-1.2.0}/src/nene2/mcp/server.py +9 -2
- nene2_python-1.2.0/tests/nene2/database/test_utils.py +55 -0
- {nene2_python-1.0.0 → nene2_python-1.2.0}/tests/nene2/mcp/test_http_client.py +25 -1
- {nene2_python-1.0.0 → nene2_python-1.2.0}/.env.example +0 -0
- {nene2_python-1.0.0 → nene2_python-1.2.0}/.github/workflows/ci.yml +0 -0
- {nene2_python-1.0.0 → nene2_python-1.2.0}/.github/workflows/docs.yml +0 -0
- {nene2_python-1.0.0 → nene2_python-1.2.0}/.github/workflows/publish.yml +0 -0
- {nene2_python-1.0.0 → nene2_python-1.2.0}/.gitignore +0 -0
- {nene2_python-1.0.0 → nene2_python-1.2.0}/.vitepress/config.mts +0 -0
- {nene2_python-1.0.0 → nene2_python-1.2.0}/.vitepress/theme/custom.css +0 -0
- {nene2_python-1.0.0 → nene2_python-1.2.0}/.vitepress/theme/index.ts +0 -0
- {nene2_python-1.0.0 → nene2_python-1.2.0}/AGENTS.md +0 -0
- {nene2_python-1.0.0 → nene2_python-1.2.0}/CHANGELOG.md +0 -0
- {nene2_python-1.0.0 → nene2_python-1.2.0}/CLAUDE.md +0 -0
- {nene2_python-1.0.0 → nene2_python-1.2.0}/Dockerfile +0 -0
- {nene2_python-1.0.0 → nene2_python-1.2.0}/LICENSE +0 -0
- {nene2_python-1.0.0 → nene2_python-1.2.0}/README.md +0 -0
- {nene2_python-1.0.0 → nene2_python-1.2.0}/alembic/README +0 -0
- {nene2_python-1.0.0 → nene2_python-1.2.0}/alembic/env.py +0 -0
- {nene2_python-1.0.0 → nene2_python-1.2.0}/alembic/script.py.mako +0 -0
- {nene2_python-1.0.0 → nene2_python-1.2.0}/alembic/versions/001_create_notes_and_tags_tables.py +0 -0
- {nene2_python-1.0.0 → nene2_python-1.2.0}/alembic.ini +0 -0
- {nene2_python-1.0.0 → nene2_python-1.2.0}/compose.yaml +0 -0
- {nene2_python-1.0.0 → nene2_python-1.2.0}/docs/adr/0001-toolchain.md +0 -0
- {nene2_python-1.0.0 → nene2_python-1.2.0}/docs/adr/0002-clean-architecture.md +0 -0
- {nene2_python-1.0.0 → nene2_python-1.2.0}/docs/adr/0003-security-first.md +0 -0
- {nene2_python-1.0.0 → nene2_python-1.2.0}/docs/adr/0004-ai-first-design.md +0 -0
- {nene2_python-1.0.0 → nene2_python-1.2.0}/docs/adr/0005-logging.md +0 -0
- {nene2_python-1.0.0 → nene2_python-1.2.0}/docs/adr/0006-rate-limiting.md +0 -0
- {nene2_python-1.0.0 → nene2_python-1.2.0}/docs/adr/0009-mcp-design.md +0 -0
- {nene2_python-1.0.0 → nene2_python-1.2.0}/docs/adr/0010-async-use-case.md +0 -0
- {nene2_python-1.0.0 → nene2_python-1.2.0}/docs/de/index.md +0 -0
- {nene2_python-1.0.0 → nene2_python-1.2.0}/docs/de/tutorials/getting-started.md +0 -0
- {nene2_python-1.0.0 → nene2_python-1.2.0}/docs/explanation/architecture.md +0 -0
- {nene2_python-1.0.0 → nene2_python-1.2.0}/docs/explanation/design-philosophy.md +0 -0
- {nene2_python-1.0.0 → nene2_python-1.2.0}/docs/field-trials/2026-05-field-trial-1.md +0 -0
- {nene2_python-1.0.0 → nene2_python-1.2.0}/docs/field-trials/2026-05-field-trial-2.md +0 -0
- {nene2_python-1.0.0 → nene2_python-1.2.0}/docs/field-trials/2026-05-field-trial-3.md +0 -0
- {nene2_python-1.0.0 → nene2_python-1.2.0}/docs/field-trials/2026-05-field-trial-4.md +0 -0
- {nene2_python-1.0.0 → nene2_python-1.2.0}/docs/field-trials/2026-05-field-trial-5.md +0 -0
- {nene2_python-1.0.0 → nene2_python-1.2.0}/docs/field-trials/2026-05-field-trial-6.md +0 -0
- {nene2_python-1.0.0 → nene2_python-1.2.0}/docs/fr/index.md +0 -0
- {nene2_python-1.0.0 → nene2_python-1.2.0}/docs/fr/tutorials/getting-started.md +0 -0
- {nene2_python-1.0.0 → nene2_python-1.2.0}/docs/how-to/add-new-domain.md +0 -0
- {nene2_python-1.0.0 → nene2_python-1.2.0}/docs/how-to/configure-auth.md +0 -0
- {nene2_python-1.0.0 → nene2_python-1.2.0}/docs/how-to/new-project.md +0 -0
- {nene2_python-1.0.0 → nene2_python-1.2.0}/docs/how-to/run-tests.md +0 -0
- {nene2_python-1.0.0 → nene2_python-1.2.0}/docs/howto/mcp-setup.md +0 -0
- {nene2_python-1.0.0 → nene2_python-1.2.0}/docs/index.md +0 -0
- {nene2_python-1.0.0 → nene2_python-1.2.0}/docs/ja/explanation/architecture.md +0 -0
- {nene2_python-1.0.0 → nene2_python-1.2.0}/docs/ja/explanation/design-philosophy.md +0 -0
- {nene2_python-1.0.0 → nene2_python-1.2.0}/docs/ja/how-to/add-new-domain.md +0 -0
- {nene2_python-1.0.0 → nene2_python-1.2.0}/docs/ja/how-to/configure-auth.md +0 -0
- {nene2_python-1.0.0 → nene2_python-1.2.0}/docs/ja/how-to/new-project.md +0 -0
- {nene2_python-1.0.0 → nene2_python-1.2.0}/docs/ja/how-to/run-tests.md +0 -0
- {nene2_python-1.0.0 → nene2_python-1.2.0}/docs/ja/how-to/sqlalchemy-repository.md +0 -0
- {nene2_python-1.0.0 → nene2_python-1.2.0}/docs/ja/howto/mcp-setup.md +0 -0
- {nene2_python-1.0.0 → nene2_python-1.2.0}/docs/ja/index.md +0 -0
- {nene2_python-1.0.0 → nene2_python-1.2.0}/docs/ja/reference/api.md +0 -0
- {nene2_python-1.0.0 → nene2_python-1.2.0}/docs/ja/reference/configuration.md +0 -0
- {nene2_python-1.0.0 → nene2_python-1.2.0}/docs/ja/reference/framework-modules.md +0 -0
- {nene2_python-1.0.0 → nene2_python-1.2.0}/docs/ja/tutorials/first-domain.md +0 -0
- {nene2_python-1.0.0 → nene2_python-1.2.0}/docs/ja/tutorials/getting-started.md +0 -0
- {nene2_python-1.0.0 → nene2_python-1.2.0}/docs/pt-br/index.md +0 -0
- {nene2_python-1.0.0 → nene2_python-1.2.0}/docs/pt-br/tutorials/getting-started.md +0 -0
- {nene2_python-1.0.0 → nene2_python-1.2.0}/docs/reference/api.md +0 -0
- {nene2_python-1.0.0 → nene2_python-1.2.0}/docs/reference/configuration.md +0 -0
- {nene2_python-1.0.0 → nene2_python-1.2.0}/docs/reference/framework-modules.md +0 -0
- {nene2_python-1.0.0 → nene2_python-1.2.0}/docs/roadmap.md +0 -0
- {nene2_python-1.0.0 → nene2_python-1.2.0}/docs/todo/current.md +0 -0
- {nene2_python-1.0.0 → nene2_python-1.2.0}/docs/tutorials/first-domain.md +0 -0
- {nene2_python-1.0.0 → nene2_python-1.2.0}/docs/tutorials/getting-started.md +0 -0
- {nene2_python-1.0.0 → nene2_python-1.2.0}/docs/zh/index.md +0 -0
- {nene2_python-1.0.0 → nene2_python-1.2.0}/docs/zh/tutorials/getting-started.md +0 -0
- {nene2_python-1.0.0 → nene2_python-1.2.0}/package-lock.json +0 -0
- {nene2_python-1.0.0 → nene2_python-1.2.0}/package.json +0 -0
- {nene2_python-1.0.0 → nene2_python-1.2.0}/src/example/__init__.py +0 -0
- {nene2_python-1.0.0 → nene2_python-1.2.0}/src/example/__main__.py +0 -0
- {nene2_python-1.0.0 → nene2_python-1.2.0}/src/example/app.py +0 -0
- {nene2_python-1.0.0 → nene2_python-1.2.0}/src/example/comment/__init__.py +0 -0
- {nene2_python-1.0.0 → nene2_python-1.2.0}/src/example/comment/entity.py +0 -0
- {nene2_python-1.0.0 → nene2_python-1.2.0}/src/example/comment/exceptions.py +0 -0
- {nene2_python-1.0.0 → nene2_python-1.2.0}/src/example/comment/handler.py +0 -0
- {nene2_python-1.0.0 → nene2_python-1.2.0}/src/example/comment/repository.py +0 -0
- {nene2_python-1.0.0 → nene2_python-1.2.0}/src/example/comment/sqlalchemy_repository.py +0 -0
- {nene2_python-1.0.0 → nene2_python-1.2.0}/src/example/comment/use_case.py +0 -0
- {nene2_python-1.0.0 → nene2_python-1.2.0}/src/example/mcp.py +0 -0
- {nene2_python-1.0.0 → nene2_python-1.2.0}/src/example/note/__init__.py +0 -0
- {nene2_python-1.0.0 → nene2_python-1.2.0}/src/example/note/async_use_case.py +0 -0
- {nene2_python-1.0.0 → nene2_python-1.2.0}/src/example/note/entity.py +0 -0
- {nene2_python-1.0.0 → nene2_python-1.2.0}/src/example/note/exceptions.py +0 -0
- {nene2_python-1.0.0 → nene2_python-1.2.0}/src/example/note/handler.py +0 -0
- {nene2_python-1.0.0 → nene2_python-1.2.0}/src/example/note/repository.py +0 -0
- {nene2_python-1.0.0 → nene2_python-1.2.0}/src/example/note/sqlalchemy_repository.py +0 -0
- {nene2_python-1.0.0 → nene2_python-1.2.0}/src/example/note/use_case.py +0 -0
- {nene2_python-1.0.0 → nene2_python-1.2.0}/src/example/schema.py +0 -0
- {nene2_python-1.0.0 → nene2_python-1.2.0}/src/example/tag/__init__.py +0 -0
- {nene2_python-1.0.0 → nene2_python-1.2.0}/src/example/tag/entity.py +0 -0
- {nene2_python-1.0.0 → nene2_python-1.2.0}/src/example/tag/exceptions.py +0 -0
- {nene2_python-1.0.0 → nene2_python-1.2.0}/src/example/tag/handler.py +0 -0
- {nene2_python-1.0.0 → nene2_python-1.2.0}/src/example/tag/repository.py +0 -0
- {nene2_python-1.0.0 → nene2_python-1.2.0}/src/example/tag/sqlalchemy_repository.py +0 -0
- {nene2_python-1.0.0 → nene2_python-1.2.0}/src/example/tag/use_case.py +0 -0
- {nene2_python-1.0.0 → nene2_python-1.2.0}/src/nene2/__init__.py +0 -0
- {nene2_python-1.0.0 → nene2_python-1.2.0}/src/nene2/auth/__init__.py +0 -0
- {nene2_python-1.0.0 → nene2_python-1.2.0}/src/nene2/auth/api_key.py +0 -0
- {nene2_python-1.0.0 → nene2_python-1.2.0}/src/nene2/auth/bearer_token.py +0 -0
- {nene2_python-1.0.0 → nene2_python-1.2.0}/src/nene2/auth/exceptions.py +0 -0
- {nene2_python-1.0.0 → nene2_python-1.2.0}/src/nene2/auth/interfaces.py +0 -0
- {nene2_python-1.0.0 → nene2_python-1.2.0}/src/nene2/auth/local_verifier.py +0 -0
- {nene2_python-1.0.0 → nene2_python-1.2.0}/src/nene2/config/__init__.py +0 -0
- {nene2_python-1.0.0 → nene2_python-1.2.0}/src/nene2/config/settings.py +0 -0
- {nene2_python-1.0.0 → nene2_python-1.2.0}/src/nene2/database/exceptions.py +0 -0
- {nene2_python-1.0.0 → nene2_python-1.2.0}/src/nene2/database/health.py +0 -0
- {nene2_python-1.0.0 → nene2_python-1.2.0}/src/nene2/database/interfaces.py +0 -0
- {nene2_python-1.0.0 → nene2_python-1.2.0}/src/nene2/database/sqlalchemy_executor.py +0 -0
- {nene2_python-1.0.0 → nene2_python-1.2.0}/src/nene2/http/__init__.py +0 -0
- {nene2_python-1.0.0 → nene2_python-1.2.0}/src/nene2/http/health.py +0 -0
- {nene2_python-1.0.0 → nene2_python-1.2.0}/src/nene2/http/pagination.py +0 -0
- {nene2_python-1.0.0 → nene2_python-1.2.0}/src/nene2/http/problem_details.py +0 -0
- {nene2_python-1.0.0 → nene2_python-1.2.0}/src/nene2/log/__init__.py +0 -0
- {nene2_python-1.0.0 → nene2_python-1.2.0}/src/nene2/log/setup.py +0 -0
- {nene2_python-1.0.0 → nene2_python-1.2.0}/src/nene2/middleware/__init__.py +0 -0
- {nene2_python-1.0.0 → nene2_python-1.2.0}/src/nene2/middleware/domain_exception.py +0 -0
- {nene2_python-1.0.0 → nene2_python-1.2.0}/src/nene2/middleware/error_handler.py +0 -0
- {nene2_python-1.0.0 → nene2_python-1.2.0}/src/nene2/middleware/request_id.py +0 -0
- {nene2_python-1.0.0 → nene2_python-1.2.0}/src/nene2/middleware/request_logging.py +0 -0
- {nene2_python-1.0.0 → nene2_python-1.2.0}/src/nene2/middleware/request_size_limit.py +0 -0
- {nene2_python-1.0.0 → nene2_python-1.2.0}/src/nene2/middleware/security_headers.py +0 -0
- {nene2_python-1.0.0 → nene2_python-1.2.0}/src/nene2/middleware/throttle.py +0 -0
- {nene2_python-1.0.0 → nene2_python-1.2.0}/src/nene2/py.typed +0 -0
- {nene2_python-1.0.0 → nene2_python-1.2.0}/src/nene2/use_case/__init__.py +0 -0
- {nene2_python-1.0.0 → nene2_python-1.2.0}/src/nene2/use_case/protocols.py +0 -0
- {nene2_python-1.0.0 → nene2_python-1.2.0}/src/nene2/validation/__init__.py +0 -0
- {nene2_python-1.0.0 → nene2_python-1.2.0}/src/nene2/validation/exceptions.py +0 -0
- {nene2_python-1.0.0 → nene2_python-1.2.0}/src/scripts/__init__.py +0 -0
- {nene2_python-1.0.0 → nene2_python-1.2.0}/src/scripts/export_openapi.py +0 -0
- {nene2_python-1.0.0 → nene2_python-1.2.0}/tests/__init__.py +0 -0
- {nene2_python-1.0.0 → nene2_python-1.2.0}/tests/example/__init__.py +0 -0
- {nene2_python-1.0.0 → nene2_python-1.2.0}/tests/example/comment/__init__.py +0 -0
- {nene2_python-1.0.0 → nene2_python-1.2.0}/tests/example/comment/test_comment_http.py +0 -0
- {nene2_python-1.0.0 → nene2_python-1.2.0}/tests/example/comment/test_comment_repository.py +0 -0
- {nene2_python-1.0.0 → nene2_python-1.2.0}/tests/example/comment/test_comment_use_case.py +0 -0
- {nene2_python-1.0.0 → nene2_python-1.2.0}/tests/example/conftest.py +0 -0
- {nene2_python-1.0.0 → nene2_python-1.2.0}/tests/example/note/__init__.py +0 -0
- {nene2_python-1.0.0 → nene2_python-1.2.0}/tests/example/note/test_async_note_use_case.py +0 -0
- {nene2_python-1.0.0 → nene2_python-1.2.0}/tests/example/note/test_list_notes.py +0 -0
- {nene2_python-1.0.0 → nene2_python-1.2.0}/tests/example/note/test_note_repository.py +0 -0
- {nene2_python-1.0.0 → nene2_python-1.2.0}/tests/example/tag/__init__.py +0 -0
- {nene2_python-1.0.0 → nene2_python-1.2.0}/tests/example/tag/test_tag_repository.py +0 -0
- {nene2_python-1.0.0 → nene2_python-1.2.0}/tests/example/tag/test_tags.py +0 -0
- {nene2_python-1.0.0 → nene2_python-1.2.0}/tests/example/test_cors.py +0 -0
- {nene2_python-1.0.0 → nene2_python-1.2.0}/tests/example/test_mcp.py +0 -0
- {nene2_python-1.0.0 → nene2_python-1.2.0}/tests/nene2/__init__.py +0 -0
- {nene2_python-1.0.0 → nene2_python-1.2.0}/tests/nene2/auth/__init__.py +0 -0
- {nene2_python-1.0.0 → nene2_python-1.2.0}/tests/nene2/auth/test_api_key.py +0 -0
- {nene2_python-1.0.0 → nene2_python-1.2.0}/tests/nene2/auth/test_bearer_token.py +0 -0
- {nene2_python-1.0.0 → nene2_python-1.2.0}/tests/nene2/auth/test_token_issuer.py +0 -0
- {nene2_python-1.0.0 → nene2_python-1.2.0}/tests/nene2/database/__init__.py +0 -0
- {nene2_python-1.0.0 → nene2_python-1.2.0}/tests/nene2/database/test_transaction.py +0 -0
- {nene2_python-1.0.0 → nene2_python-1.2.0}/tests/nene2/http/__init__.py +0 -0
- {nene2_python-1.0.0 → nene2_python-1.2.0}/tests/nene2/http/test_pagination.py +0 -0
- {nene2_python-1.0.0 → nene2_python-1.2.0}/tests/nene2/mcp/__init__.py +0 -0
- {nene2_python-1.0.0 → nene2_python-1.2.0}/tests/nene2/middleware/__init__.py +0 -0
- {nene2_python-1.0.0 → nene2_python-1.2.0}/tests/nene2/middleware/test_error_handler.py +0 -0
- {nene2_python-1.0.0 → nene2_python-1.2.0}/tests/nene2/middleware/test_request_id.py +0 -0
- {nene2_python-1.0.0 → nene2_python-1.2.0}/tests/nene2/middleware/test_request_logging.py +0 -0
- {nene2_python-1.0.0 → nene2_python-1.2.0}/tests/nene2/middleware/test_request_size_limit.py +0 -0
- {nene2_python-1.0.0 → nene2_python-1.2.0}/tests/nene2/middleware/test_security_headers.py +0 -0
- {nene2_python-1.0.0 → nene2_python-1.2.0}/tests/nene2/middleware/test_throttle.py +0 -0
- {nene2_python-1.0.0 → nene2_python-1.2.0}/tests/nene2/use_case/__init__.py +0 -0
- {nene2_python-1.0.0 → nene2_python-1.2.0}/tests/nene2/use_case/test_protocols.py +0 -0
- {nene2_python-1.0.0 → nene2_python-1.2.0}/tests/nene2/validation/__init__.py +0 -0
- {nene2_python-1.0.0 → nene2_python-1.2.0}/tests/nene2/validation/test_exceptions.py +0 -0
- {nene2_python-1.0.0 → nene2_python-1.2.0}/tests/scripts/__init__.py +0 -0
- {nene2_python-1.0.0 → nene2_python-1.2.0}/tests/scripts/test_export_openapi.py +0 -0
- {nene2_python-1.0.0 → nene2_python-1.2.0}/uv.lock +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: nene2-python
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.2.0
|
|
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,170 @@
|
|
|
1
|
+
# Field Trial 7 — bookmark: PyPI publish フロー DX 検証
|
|
2
|
+
|
|
3
|
+
## Date
|
|
4
|
+
|
|
5
|
+
2026-05-20
|
|
6
|
+
|
|
7
|
+
## Baseline
|
|
8
|
+
|
|
9
|
+
- nene2-python v1.0.0(**PyPI 経由** `uv add nene2-python`)
|
|
10
|
+
- Python 3.14(uv managed)
|
|
11
|
+
- プロジェクト: **bookmark** — ブックマーク管理 JSON API
|
|
12
|
+
- エンティティ: `Bookmark`(id, title, url, description)
|
|
13
|
+
- 5 エンドポイント: CRUD(List / Get / Create / Update / Delete)
|
|
14
|
+
- InMemory リポジトリのみ(SQLite なし)
|
|
15
|
+
- **目標**: `pip install nene2-python` → ゼロ設定で動く API を構築できるか
|
|
16
|
+
|
|
17
|
+
## Goal
|
|
18
|
+
|
|
19
|
+
1. PyPI 公開フローの DX 検証(TestPyPI → PyPI 本番)
|
|
20
|
+
2. `uv add nene2-python`(PyPI 経由)でのインストールが正常に機能するか確認
|
|
21
|
+
3. 公開パッケージだけを使ってブックマーク API を構築する E2E DX 検証
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
## Steps Taken
|
|
26
|
+
|
|
27
|
+
### 1. PyPI 公開フロー
|
|
28
|
+
|
|
29
|
+
#### TestPyPI
|
|
30
|
+
|
|
31
|
+
- GitHub Actions OIDC(Trusted Publishing)を設定
|
|
32
|
+
- `uv publish --trusted-publishing always` を使用
|
|
33
|
+
- `pypa/gh-action-pypi-publish@release/v1` は Docker コンテナ内で実行されるため、
|
|
34
|
+
GitHub Actions の Docker ネットワークで DNS 解決が失敗する問題が発生
|
|
35
|
+
→ `uv publish` をランナー直接実行に切り替えて解決
|
|
36
|
+
|
|
37
|
+
#### PyPI 本番
|
|
38
|
+
|
|
39
|
+
- TestPyPI 確認後、同一ワークフローで本番 PyPI へ publish
|
|
40
|
+
- GitHub Release を自動生成(`gh release create`)
|
|
41
|
+
|
|
42
|
+
### 2. プロジェクト初期化
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
uv init --name bookmark --no-workspace
|
|
46
|
+
uv add nene2-python # PyPI から v1.0.0 をインストール
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
追加設定不要。`nene2-python` が FastAPI・Pydantic・SQLAlchemy・structlog をすべて束ねているため、
|
|
50
|
+
依存関係の解決は `uv add nene2-python` 一行で完了。
|
|
51
|
+
|
|
52
|
+
### 3. ドメイン層の実装
|
|
53
|
+
|
|
54
|
+
Clean Architecture の4層(entity / exceptions / repository / use_case)を独立して記述:
|
|
55
|
+
|
|
56
|
+
```python
|
|
57
|
+
# entity.py
|
|
58
|
+
@dataclass(frozen=True, slots=True)
|
|
59
|
+
class Bookmark:
|
|
60
|
+
id: int
|
|
61
|
+
title: str
|
|
62
|
+
url: str
|
|
63
|
+
description: str
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
`nene2` からインポートが必要なのは exceptions のみ:
|
|
67
|
+
```python
|
|
68
|
+
from nene2.http import problem_details_response
|
|
69
|
+
from nene2.middleware.domain_exception import DomainExceptionHandlerProtocol
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
UseCase・Repository・Entity は nene2 に依存しないため、フレームワーク非依存のビジネスロジックとして成立。
|
|
73
|
+
|
|
74
|
+
### 4. HTTP 層の実装
|
|
75
|
+
|
|
76
|
+
`handler.py` で `nene2.http`・`nene2.validation` を使用:
|
|
77
|
+
|
|
78
|
+
```python
|
|
79
|
+
from nene2.http import PaginationQueryParser, PaginationResponse
|
|
80
|
+
from nene2.validation.exceptions import ValidationError, ValidationException
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
パターンは example/note/handler.py と完全に同じ。5分で移植可能。
|
|
84
|
+
|
|
85
|
+
### 5. アプリケーションファクトリ
|
|
86
|
+
|
|
87
|
+
`app.py` でミドルウェアをスタックして完成:
|
|
88
|
+
|
|
89
|
+
```python
|
|
90
|
+
from nene2.config import AppSettings
|
|
91
|
+
from nene2.middleware import (
|
|
92
|
+
ErrorHandlerMiddleware, RequestIdMiddleware,
|
|
93
|
+
RequestLoggingMiddleware, RequestSizeLimitMiddleware,
|
|
94
|
+
SecurityHeadersMiddleware,
|
|
95
|
+
)
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
ミドルウェア登録は innermost-first 順で明示的(ドキュメントにコメントあり)。
|
|
99
|
+
|
|
100
|
+
### 6. 動作確認
|
|
101
|
+
|
|
102
|
+
```bash
|
|
103
|
+
uv run uvicorn app:app --port 8090
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
```
|
|
107
|
+
POST /bookmarks {"title":"GitHub","url":"https://github.com","description":"Code hosting"}
|
|
108
|
+
→ 201 {"id":1,"title":"GitHub","url":"https://github.com","description":"Code hosting"}
|
|
109
|
+
|
|
110
|
+
GET /bookmarks?limit=10&offset=0
|
|
111
|
+
→ 200 {"items":[...],"limit":10,"offset":0,"total":2}
|
|
112
|
+
|
|
113
|
+
GET /bookmarks/1
|
|
114
|
+
→ 200 {"id":1,...}
|
|
115
|
+
|
|
116
|
+
PUT /bookmarks/1 {"title":"GitHub Updated","url":"https://github.com","description":"World's largest code host"}
|
|
117
|
+
→ 200 {"id":1,"title":"GitHub Updated",...}
|
|
118
|
+
|
|
119
|
+
DELETE /bookmarks/2
|
|
120
|
+
→ 204
|
|
121
|
+
|
|
122
|
+
GET /bookmarks/99
|
|
123
|
+
→ 404 {"type":"https://nene2.dev/problems/not-found","title":"Not Found","status":404,"detail":"Bookmark 99 not found."}
|
|
124
|
+
|
|
125
|
+
POST /bookmarks {"title":" ","url":"https://example.com","description":""}
|
|
126
|
+
→ 422 {"type":"https://nene2.dev/problems/validation-failed","errors":[{"field":"title","message":"Title must not be empty.","code":"required"}]}
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
全エンドポイントが期待通りに動作。Problem Details(RFC 9457)レスポンスも正常。
|
|
130
|
+
|
|
131
|
+
---
|
|
132
|
+
|
|
133
|
+
## Friction Points
|
|
134
|
+
|
|
135
|
+
### FT7-1: Docker DNS 問題(`pypa/gh-action-pypi-publish`)
|
|
136
|
+
|
|
137
|
+
- **摩擦**: `pypa/gh-action-pypi-publish@release/v1` を使用すると、Docker コンテナ内で
|
|
138
|
+
`upload.test.pypi.org` の DNS 解決に失敗する
|
|
139
|
+
- **深刻度**: HIGH(公開ブロック)
|
|
140
|
+
- **解決策**: `uv publish --trusted-publishing always` をランナー直接実行に切り替え
|
|
141
|
+
- **所要時間**: ワークフロー修正・デバッグで約 1 時間
|
|
142
|
+
|
|
143
|
+
### FT7-2: `[tool.uv] dev-dependencies` → `[dependency-groups]` 変更
|
|
144
|
+
|
|
145
|
+
- **摩擦**: `uv init` が生成した `pyproject.toml` に旧形式の `[tool.uv] dev-dependencies` が含まれる場合がある(uv バージョン依存)
|
|
146
|
+
- **深刻度**: LOW(警告のみ)
|
|
147
|
+
- **解決策**: `[dependency-groups] dev` 形式に更新
|
|
148
|
+
|
|
149
|
+
### FT7-3: `InMemoryRepository` を毎回自前実装
|
|
150
|
+
|
|
151
|
+
- **摩擦**: テスト用 InMemory Repository をドメインごとにゼロから書く必要がある
|
|
152
|
+
- **深刻度**: LOW(パターンが明確なため機械的に書ける)
|
|
153
|
+
- **検討**: `GenericInMemoryRepository[T]` のような汎用実装をフレームワークに含めるか検討余地あり
|
|
154
|
+
|
|
155
|
+
---
|
|
156
|
+
|
|
157
|
+
## Summary
|
|
158
|
+
|
|
159
|
+
| ID | 摩擦 | 深刻度 | 種別 | Follow-up |
|
|
160
|
+
|--------|-------------------------------------------|--------|---------------|-----------|
|
|
161
|
+
| FT7-1 | Docker DNS で pypa action が失敗 | HIGH | インフラ | 解決済み(uv publish に切り替え) |
|
|
162
|
+
| FT7-2 | dev-dependencies フォーマット警告 | LOW | DX | 解決済み(PR #154) |
|
|
163
|
+
| FT7-3 | InMemory Repository を毎回書く | LOW | フレームワーク | 検討余地あり |
|
|
164
|
+
|
|
165
|
+
FT7 の主目的(PyPI 公開 → インストール → 動く API 構築)は達成。
|
|
166
|
+
`uv add nene2-python` の一行でフレームワーク全体が入り、
|
|
167
|
+
Clean Architecture パターンをゼロから構築するのに要した時間は **30分以内**(ドメイン4ファイル + handler + app)。
|
|
168
|
+
|
|
169
|
+
**PyPI publish パイプライン(TestPyPI → PyPI → GitHub Release)は確立済み。**
|
|
170
|
+
FT8 候補:MySQL/PostgreSQL アダプターの実地検証、または親子リソース(nested REST)DX 検証。
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
# Field Trial 8 — blog: 親子リソース (Nested REST) + datetime + SQLite 複数エンティティ
|
|
2
|
+
|
|
3
|
+
## Date
|
|
4
|
+
|
|
5
|
+
2026-05-20
|
|
6
|
+
|
|
7
|
+
## Baseline
|
|
8
|
+
|
|
9
|
+
- nene2-python v1.0.0(PyPI 経由 `uv add nene2-python`)
|
|
10
|
+
- Python 3.14(uv managed)
|
|
11
|
+
- プロジェクト: **blog** — ブログ API(投稿 + コメント)
|
|
12
|
+
- エンティティ:
|
|
13
|
+
- `Post(id, title, body, created_at: datetime)`
|
|
14
|
+
- `Comment(id, post_id, author, body, created_at: datetime)`
|
|
15
|
+
- SQLite ファイル永続化(`blog.db`)
|
|
16
|
+
- 8 エンドポイント: Post CRUD + コメント List/Create/Delete(Nested REST)
|
|
17
|
+
|
|
18
|
+
## Goal
|
|
19
|
+
|
|
20
|
+
FT1〜FT7 で未探索のパターンを一度に検証:
|
|
21
|
+
|
|
22
|
+
1. **親子リソース(Nested REST)**: `GET /posts/{id}/comments`、`POST /posts/{id}/comments`
|
|
23
|
+
2. **`datetime` フィールドを持つエンティティ**: `created_at` が SQLite → entity → JSON でどう流れるか
|
|
24
|
+
3. **SQLite 外部キー**: 2エンティティ間の参照整合性(`ON DELETE CASCADE`)
|
|
25
|
+
4. **`DatabaseHealthCheck`** の実際の動作確認
|
|
26
|
+
|
|
27
|
+
---
|
|
28
|
+
|
|
29
|
+
## Steps Taken
|
|
30
|
+
|
|
31
|
+
### 1. プロジェクト初期化
|
|
32
|
+
|
|
33
|
+
問題なし。`uv add nene2-python` 一行で完了。
|
|
34
|
+
|
|
35
|
+
### 2. エンティティに `datetime` フィールドを追加
|
|
36
|
+
|
|
37
|
+
```python
|
|
38
|
+
@dataclass(frozen=True, slots=True)
|
|
39
|
+
class Post:
|
|
40
|
+
id: int
|
|
41
|
+
title: str
|
|
42
|
+
body: str
|
|
43
|
+
created_at: datetime # ← FT1〜FT7 で一度も使われなかったフィールド
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
FT1〜FT7 の全エンティティ(Note, Book, Task, Snippet, Wallet, City, Bookmark)は
|
|
47
|
+
`created_at` を持っていない。example の Note も schema に `created_at` カラムはあるが
|
|
48
|
+
エンティティ定義には含まれていない。**datetime フィールドの扱いは前例なし。**
|
|
49
|
+
|
|
50
|
+
### 3. SQLite スキーマ(外部キー + `CURRENT_TIMESTAMP`)
|
|
51
|
+
|
|
52
|
+
```python
|
|
53
|
+
# PRAGMA foreign_keys=ON で参照整合性を有効化
|
|
54
|
+
@event.listens_for(Engine, "connect")
|
|
55
|
+
def _set_sqlite_pragma(dbapi_conn, _):
|
|
56
|
+
if isinstance(dbapi_conn, sqlite3.Connection):
|
|
57
|
+
dbapi_conn.execute("PRAGMA foreign_keys=ON")
|
|
58
|
+
|
|
59
|
+
# comments テーブルに外部キー
|
|
60
|
+
"post_id INTEGER NOT NULL REFERENCES posts(id) ON DELETE CASCADE"
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
`PRAGMA foreign_keys=ON` は example の schema.py を参照して発見。ドキュメントには記載なし。
|
|
64
|
+
|
|
65
|
+
### 4. SQLAlchemy Repository での datetime 変換
|
|
66
|
+
|
|
67
|
+
```python
|
|
68
|
+
def _to_post(row: dict[str, Any]) -> Post:
|
|
69
|
+
return Post(
|
|
70
|
+
id=row["id"],
|
|
71
|
+
title=row["title"],
|
|
72
|
+
body=row["body"],
|
|
73
|
+
# SQLite は DATETIME を text 型で返す: "2026-05-20 12:34:56"
|
|
74
|
+
created_at=datetime.fromisoformat(str(row["created_at"])).replace(tzinfo=timezone.utc),
|
|
75
|
+
)
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
### 5. SELECT-after-INSERT パターン(DB生成タイムスタンプを取得)
|
|
79
|
+
|
|
80
|
+
```python
|
|
81
|
+
def save(self, title: str, body: str) -> Post:
|
|
82
|
+
new_id = self._executor.write(
|
|
83
|
+
"INSERT INTO posts (title, body) VALUES (:title, :body)",
|
|
84
|
+
{"title": title, "body": body},
|
|
85
|
+
)
|
|
86
|
+
row = self._executor.fetch_one(
|
|
87
|
+
"SELECT id, title, body, created_at FROM posts WHERE id = :id",
|
|
88
|
+
{"id": new_id},
|
|
89
|
+
)
|
|
90
|
+
if row is None:
|
|
91
|
+
raise RuntimeError(f"Row {new_id} not found after INSERT into posts")
|
|
92
|
+
return _to_post(row)
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
INSERT → `write()` → `lastrowid` → `fetch_one()` の2往復が必要。
|
|
96
|
+
|
|
97
|
+
### 6. Nested REST ハンドラー
|
|
98
|
+
|
|
99
|
+
```python
|
|
100
|
+
@router.get("/posts/{post_id}/comments")
|
|
101
|
+
async def list_comments(post_id: int, request: Request) -> JSONResponse:
|
|
102
|
+
pagination = PaginationQueryParser.parse(request)
|
|
103
|
+
output = list_comments_use_case.execute(
|
|
104
|
+
ListCommentsInput(post_id, pagination.limit, pagination.offset)
|
|
105
|
+
)
|
|
106
|
+
...
|
|
107
|
+
|
|
108
|
+
@router.post("/posts/{post_id}/comments", status_code=201)
|
|
109
|
+
async def create_comment(post_id: int, body: CreateCommentBody) -> JSONResponse:
|
|
110
|
+
...
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
FastAPI がパスパラメータ `post_id` を UseCase に渡すため、ネストの実装自体は自然。
|
|
114
|
+
|
|
115
|
+
### 7. 動作確認
|
|
116
|
+
|
|
117
|
+
```
|
|
118
|
+
GET /health
|
|
119
|
+
→ 200 {"status":"ok","checks":{"database":"ok"}} ← DatabaseHealthCheck 正常
|
|
120
|
+
|
|
121
|
+
POST /posts {"title":"Hello nene2","body":"First post"}
|
|
122
|
+
→ 201 {"id":1,"title":"Hello nene2","body":"First post","created_at":"2026-05-19T18:39:52+00:00"}
|
|
123
|
+
|
|
124
|
+
POST /posts/1/comments {"author":"Alice","body":"Great!"}
|
|
125
|
+
→ 201 {"id":1,"post_id":1,"author":"Alice","body":"Great!","created_at":"2026-05-19T18:39:52+00:00"}
|
|
126
|
+
|
|
127
|
+
GET /posts/1/comments?limit=10
|
|
128
|
+
→ 200 {"items":[...],"limit":10,"offset":0,"total":1}
|
|
129
|
+
|
|
130
|
+
POST /posts/999/comments {...}
|
|
131
|
+
→ 404 {"type":"...not-found","detail":"Post 999 not found."}
|
|
132
|
+
|
|
133
|
+
DELETE /posts/1
|
|
134
|
+
→ 204 (cascade: comments も自動削除)
|
|
135
|
+
|
|
136
|
+
GET /posts/1/comments (削除後)
|
|
137
|
+
→ 404 Post 1 not found.
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
---
|
|
141
|
+
|
|
142
|
+
## Friction Points
|
|
143
|
+
|
|
144
|
+
### FT8-1: SQLite `CURRENT_TIMESTAMP` が naive datetime を返す — タイムゾーン情報が失われる
|
|
145
|
+
|
|
146
|
+
- **摩擦**: `CURRENT_TIMESTAMP` は UTC 時刻だが SQLite は `"2026-05-20 12:34:56"` という文字列で格納・返却する
|
|
147
|
+
- `datetime.fromisoformat("2026-05-20 12:34:56")` は **naive datetime**(tzinfo なし)を返す
|
|
148
|
+
- JSON にシリアライズすると `"2026-05-20T12:34:56"` — タイムゾーンが不明な時刻として API に漏れる
|
|
149
|
+
- **深刻度**: MEDIUM(API 利用者が UTC か JST か判断できない)
|
|
150
|
+
- **解決策**: `.replace(tzinfo=timezone.utc)` を明示的に追加
|
|
151
|
+
- **nene2 の対応**: `fetch_one` / `fetch_all` の戻り値に datetime フィールドのガイダンスがない
|
|
152
|
+
|
|
153
|
+
### FT8-2: `dict[str, object]` vs `dict[str, Any]` — 型注釈の落とし穴
|
|
154
|
+
|
|
155
|
+
- **摩擦**: `_to_post(row: dict[str, object])` と書くと `int(row["id"])` が mypy `call-overload` エラー
|
|
156
|
+
- `# type: ignore[arg-type]` を付けると「wrong error code」で `unused-ignore` が発生(二重エラー)
|
|
157
|
+
- **深刻度**: MEDIUM(mypy --strict を使う場合にブロッキング)
|
|
158
|
+
- **解決策**: `dict[str, Any]` を使う(`fetch_one()` の実際の戻り値型と一致)
|
|
159
|
+
- **根本原因**: nene2 の「row-to-entity 変換」サンプルに datetime フィールドがない
|
|
160
|
+
|
|
161
|
+
### FT8-3: SELECT-after-INSERT のボイラープレート
|
|
162
|
+
|
|
163
|
+
- **摩擦**: DB 生成の `CURRENT_TIMESTAMP` を取得するために INSERT → `lastrowid` → `fetch_one` の2往復が必要
|
|
164
|
+
- 毎 `save()` メソッドに同じパターンを書く(PostRepository と CommentRepository の両方)
|
|
165
|
+
- `fetch_one()` は `dict | None` を返すため、`if row is None: raise RuntimeError(...)` が必要
|
|
166
|
+
- `assert row is not None` は ruff S101(本番コードで assert 禁止)で弾かれる
|
|
167
|
+
- **深刻度**: LOW(機能するが冗長)
|
|
168
|
+
- **検討**: `write_and_return(sql, params, select_sql)` のようなヘルパーをフレームワークに追加するか
|
|
169
|
+
|
|
170
|
+
### FT8-4: if/else で異なる実装を変数に代入すると mypy エラー
|
|
171
|
+
|
|
172
|
+
- **摩擦**:
|
|
173
|
+
|
|
174
|
+
```python
|
|
175
|
+
if cfg.db_adapter == "sqlite":
|
|
176
|
+
post_repo = SqlAlchemyPostRepository(executor)
|
|
177
|
+
else:
|
|
178
|
+
post_repo = InMemoryPostRepository() # ← mypy: Incompatible types
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
- `mypy --strict` が `Incompatible types in assignment` を報告
|
|
182
|
+
- **解決策**: 事前に型注釈を宣言する
|
|
183
|
+
|
|
184
|
+
```python
|
|
185
|
+
post_repo: PostRepositoryInterface
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
- **深刻度**: LOW(パターンを一度知れば1行で解決)
|
|
189
|
+
- example の `app.py` は `_build_repositories()` 関数で隠蔽しているため、このエラーが見えにくい
|
|
190
|
+
|
|
191
|
+
### FT8-5: Nested REST の `post_id` パスパラメータが DELETE で無視される — 設計の穴
|
|
192
|
+
|
|
193
|
+
- **摩擦**: `DELETE /posts/{post_id}/comments/{comment_id}` の `post_id` を DELETE ハンドラーが無視する
|
|
194
|
+
- `DELETE /posts/1/comments/2` が post 2 のコメント2(別ポストのコメント)を削除できてしまう
|
|
195
|
+
- REST セマンティクス的には `/posts/1/comments/2` は「post 1 に属するコメント2」を指すべき
|
|
196
|
+
- **深刻度**: MEDIUM(アクセス制御がある場合はセキュリティ問題になりうる)
|
|
197
|
+
- **解決策**: DELETE UseCase に `post_id` を含め、コメントの `post_id` と一致するか検証する
|
|
198
|
+
- **nene2 の対応**: ネストリソースの DELETE 設計についてガイダンスなし
|
|
199
|
+
|
|
200
|
+
---
|
|
201
|
+
|
|
202
|
+
## Summary
|
|
203
|
+
|
|
204
|
+
| ID | 摩擦 | 深刻度 | 種別 | 解決策 |
|
|
205
|
+
|--------|-----------------------------------------------|--------|----------------|-------------------------------------|
|
|
206
|
+
| FT8-1 | SQLite naive datetime — TZ情報が失われる | MEDIUM | 設計/DB | `.replace(tzinfo=timezone.utc)` |
|
|
207
|
+
| FT8-2 | `dict[str, object]` vs `Any` — mypy エラー | MEDIUM | 型安全 | `dict[str, Any]` を使う |
|
|
208
|
+
| FT8-3 | SELECT-after-INSERT ボイラープレート | LOW | フレームワーク | `write_and_return()` ヘルパー検討 |
|
|
209
|
+
| FT8-4 | if/else 分岐での型注釈宣言が必要 | LOW | 型安全 | `var: InterfaceType` を先に宣言 |
|
|
210
|
+
| FT8-5 | Nested DELETE で `post_id` が無視される | MEDIUM | 設計 | UseCase に `post_id` を追加して検証 |
|
|
211
|
+
|
|
212
|
+
親子リソース(Nested REST)そのものの実装は **摩擦ゼロ** — FastAPI のパスパラメータが自然に機能した。
|
|
213
|
+
主な摩擦は `datetime` フィールドと `mypy --strict` 周辺に集中。
|
|
214
|
+
|
|
215
|
+
FT9 候補:
|
|
216
|
+
- **FT8-1/FT8-3 の改善**: nene2 フレームワークに `datetime` ユーティリティ / `write_and_return()` を追加して検証
|
|
217
|
+
- **MySQL/PostgreSQL**: SQLite 以外のアダプター(`RETURNING` 句が使えるので FT8-3 が解消されるか検証)
|
|
218
|
+
- **HttpxMcpClient**: HTTP モードの MCP クライアントを実地検証
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
# Field Trial 9 — recipe: HttpxMcpClient + streamable-http トランスポート DX 検証
|
|
2
|
+
|
|
3
|
+
## Date
|
|
4
|
+
|
|
5
|
+
2026-05-20
|
|
6
|
+
|
|
7
|
+
## Baseline
|
|
8
|
+
|
|
9
|
+
- nene2-python v1.1.0(PyPI 経由)→ LocalMcpServer 修正後は local editable
|
|
10
|
+
- Python 3.14(uv managed)
|
|
11
|
+
- プロジェクト: **recipe** — レシピ管理 API
|
|
12
|
+
- エンティティ: `Recipe(id, title, description, servings)`
|
|
13
|
+
- HTTP API: ポート 8100(FastAPI + InMemory)
|
|
14
|
+
- MCP サーバー: ポート 8101(`streamable-http` トランスポート)
|
|
15
|
+
- MCP ツール: `HttpxMcpClient` で HTTP API を呼び出してデータ共有
|
|
16
|
+
|
|
17
|
+
## Goal
|
|
18
|
+
|
|
19
|
+
FT1〜FT8 で未探索のパターンを検証:
|
|
20
|
+
|
|
21
|
+
1. **`LocalMcpServer.run(transport="streamable-http")`** — HTTP モード MCP の起動
|
|
22
|
+
2. **`HttpxMcpClient`** — MCP ツールハンドラーから HTTP API を呼び出してデータ共有
|
|
23
|
+
3. FT3-F2(MCP と HTTP API のメモリ非共有問題)の正しい解決パターンを実証
|
|
24
|
+
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
## Steps Taken
|
|
28
|
+
|
|
29
|
+
### 1. HTTP API + MCP サーバーの二段構成
|
|
30
|
+
|
|
31
|
+
```
|
|
32
|
+
HTTP API(app.py, port 8100)
|
|
33
|
+
↑ HTTP calls
|
|
34
|
+
MCP Server(mcp_server.py, port 8101)
|
|
35
|
+
↓ streamable-http
|
|
36
|
+
Claude / MCP Client
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
MCP ツールが `HttpxMcpClient` で HTTP API を叩くため、どちらの経路から書いても同じデータが見える。
|
|
40
|
+
|
|
41
|
+
### 2. HttpxMcpClient でのツール実装
|
|
42
|
+
|
|
43
|
+
```python
|
|
44
|
+
from nene2.mcp import LocalMcpServer
|
|
45
|
+
from nene2.mcp.http_client import HttpxMcpClient
|
|
46
|
+
|
|
47
|
+
client = HttpxMcpClient()
|
|
48
|
+
server = LocalMcpServer("recipe-api", instructions="...", port=8101)
|
|
49
|
+
|
|
50
|
+
@server.tool("List all recipes. Returns paginated results.")
|
|
51
|
+
def list_recipes(limit: int = 20, offset: int = 0) -> str:
|
|
52
|
+
response = client.get(API_BASE, f"/recipes?limit={limit}&offset={offset}")
|
|
53
|
+
return response.body
|
|
54
|
+
|
|
55
|
+
@server.tool("Create a new recipe.")
|
|
56
|
+
def create_recipe(title: str, description: str, servings: int) -> str:
|
|
57
|
+
response = client.post(API_BASE, "/recipes",
|
|
58
|
+
{"title": title, "description": description, "servings": servings})
|
|
59
|
+
return response.body
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
### 3. 動作確認
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
# HTTP API で作成 → MCP ツールから見える
|
|
66
|
+
curl -X POST http://localhost:8100/recipes -d '{"title":"Ramen",...}' → {"id":1,...}
|
|
67
|
+
MCP: list_recipes() → {"items":[{"id":1,"title":"Ramen",...}],...}
|
|
68
|
+
|
|
69
|
+
# MCP ツールで作成 → HTTP API から見える
|
|
70
|
+
MCP: create_recipe("Sushi",...) → {"id":2,...}
|
|
71
|
+
curl GET http://localhost:8100/recipes → [Ramen, Sushi]
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
**FT3-F2 の解決確認**: HTTP API と MCP が `HttpxMcpClient` 経由で完全にデータを共有。
|
|
75
|
+
|
|
76
|
+
### 4. streamable-http プロトコルのフロー
|
|
77
|
+
|
|
78
|
+
```
|
|
79
|
+
POST /mcp (initialize) → Session-ID: {uuid} を取得
|
|
80
|
+
POST /mcp + Mcp-Session-Id: {uuid} (tools/list, tools/call)
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
stdio と異なり、MCP セッション管理が必要。HTTP クライアントライブラリや Claude Desktop が処理するため、手動操作は検証時のみ。
|
|
84
|
+
|
|
85
|
+
---
|
|
86
|
+
|
|
87
|
+
## Friction Points
|
|
88
|
+
|
|
89
|
+
### FT9-1: `LocalMcpServer` にポート指定がない
|
|
90
|
+
|
|
91
|
+
- **摩擦**: `LocalMcpServer.run(transport="streamable-http")` のデフォルトポートが 8000 で変更不可
|
|
92
|
+
- `FastMCP.__init__` は `host`/`port` を受け取るが、`LocalMcpServer.__init__` が渡していなかった
|
|
93
|
+
- HTTP API(8100)と MCP(8101)を同一マシンで動かす際にポート衝突が起きる
|
|
94
|
+
- **深刻度**: HIGH(2サービス構成で必ずぶつかる)
|
|
95
|
+
- **解決策**: `LocalMcpServer.__init__(name, instructions, *, host, port)` に追加済み(PR #174)
|
|
96
|
+
|
|
97
|
+
### FT9-2: HTTP エラー(404 等)が `isError: false` で返る
|
|
98
|
+
|
|
99
|
+
- **摩擦**: MCP ツールが `return response.body` で 404 レスポンスをそのまま返すと、
|
|
100
|
+
MCP プロトコルの `isError` フラグが `false` になる
|
|
101
|
+
- AI クライアントがエラーを成功として解釈する可能性がある
|
|
102
|
+
- **深刻度**: MEDIUM(AI がエラーを無視して誤った操作を続けるリスク)
|
|
103
|
+
- **解決策**: `McpHttpResponse.raise_for_error()` を追加 → FastMCP が例外を `isError: true` に変換(PR #174)
|
|
104
|
+
|
|
105
|
+
```python
|
|
106
|
+
def get_recipe(recipe_id: int) -> str:
|
|
107
|
+
response = client.get(API_BASE, f"/recipes/{recipe_id}")
|
|
108
|
+
response.raise_for_error() # ← 追加: 4xx/5xx を例外に変換
|
|
109
|
+
return response.body
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
### FT9-3: DELETE が 204 No Content を返し `response.body` が空
|
|
113
|
+
|
|
114
|
+
- **摩擦**: DELETE エンドポイントは 204 を返すため `response.body` が空文字列 `""`
|
|
115
|
+
- MCP ツールの戻り値が `""` になり、AI が「削除完了」を確認できない
|
|
116
|
+
- **解決策**: 明示的に確認メッセージを組み立てる(フレームワークは解決不要、パターンとして文書化)
|
|
117
|
+
|
|
118
|
+
```python
|
|
119
|
+
def delete_recipe(recipe_id: int) -> str:
|
|
120
|
+
response = client.delete(API_BASE, f"/recipes/{recipe_id}")
|
|
121
|
+
response.raise_for_error()
|
|
122
|
+
if response.status_code == 204:
|
|
123
|
+
return json.dumps({"deleted": True, "recipe_id": recipe_id})
|
|
124
|
+
return response.body
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
### FT9-4: streamable-http はセッション管理が必要(stdio より複雑)
|
|
128
|
+
|
|
129
|
+
- **摩擦**: stdio はプロセス間通信で自動管理されるが、HTTP モードは `initialize` → Session ID → ツール呼び出し の2ステップ
|
|
130
|
+
- Claude Desktop や MCP SDK クライアントが透過的に処理するため、**実際の利用では問題にならない**
|
|
131
|
+
- ただしデバッグ時(curl で直接叩く)には手順が煩雑
|
|
132
|
+
- **深刻度**: LOW(ツール側の問題ではなく、デバッグ時の利便性の問題)
|
|
133
|
+
|
|
134
|
+
---
|
|
135
|
+
|
|
136
|
+
## Summary
|
|
137
|
+
|
|
138
|
+
| ID | 摩擦 | 深刻度 | 解決策 |
|
|
139
|
+
|--------|-------------------------------------------------|--------|-----------------------------------------------|
|
|
140
|
+
| FT9-1 | LocalMcpServer にポート指定がない | HIGH | `host`/`port` 引数追加済み(PR #174) |
|
|
141
|
+
| FT9-2 | HTTP エラーが isError: false で返る | MEDIUM | `raise_for_error()` 追加済み(PR #174) |
|
|
142
|
+
| FT9-3 | DELETE 204 で body が空 → AI が確認できない | LOW | パターンとして文書化(json.dumps で補完) |
|
|
143
|
+
| FT9-4 | streamable-http のセッション管理が手動で煩雑 | LOW | クライアント SDK が透過処理するので運用上は問題なし |
|
|
144
|
+
|
|
145
|
+
**`HttpxMcpClient` + `streamable-http` の組み合わせは FT3-F2 の正しい解決策として機能した。**
|
|
146
|
+
HTTP API を中継することで、MCP と HTTP クライアントが完全にデータを共有できる。
|
|
147
|
+
|
|
148
|
+
FT10 候補:
|
|
149
|
+
- **MySQL/PostgreSQL アダプター**: SQLite 以外の DB を初めて FT で使用
|
|
150
|
+
- **`BearerTokenMiddleware` + `HttpxMcpClient`**: 認証付き API に対して MCP から呼び出す
|
|
151
|
+
- **`LocalMcpServer` の SSE トランスポート**: `streamable-http` との差異を確認
|