nene2-python 1.1.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.
Files changed (187) hide show
  1. {nene2_python-1.1.0 → nene2_python-1.2.0}/PKG-INFO +1 -1
  2. nene2_python-1.2.0/docs/field-trials/2026-05-field-trial-9.md +151 -0
  3. {nene2_python-1.1.0 → nene2_python-1.2.0}/pyproject.toml +1 -1
  4. {nene2_python-1.1.0 → nene2_python-1.2.0}/src/nene2/database/utils.py +1 -0
  5. {nene2_python-1.1.0 → nene2_python-1.2.0}/src/nene2/mcp/__init__.py +2 -1
  6. {nene2_python-1.1.0 → nene2_python-1.2.0}/src/nene2/mcp/http_client.py +22 -0
  7. {nene2_python-1.1.0 → nene2_python-1.2.0}/src/nene2/mcp/server.py +9 -2
  8. {nene2_python-1.1.0 → nene2_python-1.2.0}/tests/nene2/mcp/test_http_client.py +25 -1
  9. {nene2_python-1.1.0 → nene2_python-1.2.0}/.env.example +0 -0
  10. {nene2_python-1.1.0 → nene2_python-1.2.0}/.github/workflows/ci.yml +0 -0
  11. {nene2_python-1.1.0 → nene2_python-1.2.0}/.github/workflows/docs.yml +0 -0
  12. {nene2_python-1.1.0 → nene2_python-1.2.0}/.github/workflows/publish.yml +0 -0
  13. {nene2_python-1.1.0 → nene2_python-1.2.0}/.gitignore +0 -0
  14. {nene2_python-1.1.0 → nene2_python-1.2.0}/.vitepress/config.mts +0 -0
  15. {nene2_python-1.1.0 → nene2_python-1.2.0}/.vitepress/theme/custom.css +0 -0
  16. {nene2_python-1.1.0 → nene2_python-1.2.0}/.vitepress/theme/index.ts +0 -0
  17. {nene2_python-1.1.0 → nene2_python-1.2.0}/AGENTS.md +0 -0
  18. {nene2_python-1.1.0 → nene2_python-1.2.0}/CHANGELOG.md +0 -0
  19. {nene2_python-1.1.0 → nene2_python-1.2.0}/CLAUDE.md +0 -0
  20. {nene2_python-1.1.0 → nene2_python-1.2.0}/Dockerfile +0 -0
  21. {nene2_python-1.1.0 → nene2_python-1.2.0}/LICENSE +0 -0
  22. {nene2_python-1.1.0 → nene2_python-1.2.0}/README.md +0 -0
  23. {nene2_python-1.1.0 → nene2_python-1.2.0}/alembic/README +0 -0
  24. {nene2_python-1.1.0 → nene2_python-1.2.0}/alembic/env.py +0 -0
  25. {nene2_python-1.1.0 → nene2_python-1.2.0}/alembic/script.py.mako +0 -0
  26. {nene2_python-1.1.0 → nene2_python-1.2.0}/alembic/versions/001_create_notes_and_tags_tables.py +0 -0
  27. {nene2_python-1.1.0 → nene2_python-1.2.0}/alembic.ini +0 -0
  28. {nene2_python-1.1.0 → nene2_python-1.2.0}/compose.yaml +0 -0
  29. {nene2_python-1.1.0 → nene2_python-1.2.0}/docs/adr/0001-toolchain.md +0 -0
  30. {nene2_python-1.1.0 → nene2_python-1.2.0}/docs/adr/0002-clean-architecture.md +0 -0
  31. {nene2_python-1.1.0 → nene2_python-1.2.0}/docs/adr/0003-security-first.md +0 -0
  32. {nene2_python-1.1.0 → nene2_python-1.2.0}/docs/adr/0004-ai-first-design.md +0 -0
  33. {nene2_python-1.1.0 → nene2_python-1.2.0}/docs/adr/0005-logging.md +0 -0
  34. {nene2_python-1.1.0 → nene2_python-1.2.0}/docs/adr/0006-rate-limiting.md +0 -0
  35. {nene2_python-1.1.0 → nene2_python-1.2.0}/docs/adr/0009-mcp-design.md +0 -0
  36. {nene2_python-1.1.0 → nene2_python-1.2.0}/docs/adr/0010-async-use-case.md +0 -0
  37. {nene2_python-1.1.0 → nene2_python-1.2.0}/docs/de/index.md +0 -0
  38. {nene2_python-1.1.0 → nene2_python-1.2.0}/docs/de/tutorials/getting-started.md +0 -0
  39. {nene2_python-1.1.0 → nene2_python-1.2.0}/docs/explanation/architecture.md +0 -0
  40. {nene2_python-1.1.0 → nene2_python-1.2.0}/docs/explanation/design-philosophy.md +0 -0
  41. {nene2_python-1.1.0 → nene2_python-1.2.0}/docs/field-trials/2026-05-field-trial-1.md +0 -0
  42. {nene2_python-1.1.0 → nene2_python-1.2.0}/docs/field-trials/2026-05-field-trial-2.md +0 -0
  43. {nene2_python-1.1.0 → nene2_python-1.2.0}/docs/field-trials/2026-05-field-trial-3.md +0 -0
  44. {nene2_python-1.1.0 → nene2_python-1.2.0}/docs/field-trials/2026-05-field-trial-4.md +0 -0
  45. {nene2_python-1.1.0 → nene2_python-1.2.0}/docs/field-trials/2026-05-field-trial-5.md +0 -0
  46. {nene2_python-1.1.0 → nene2_python-1.2.0}/docs/field-trials/2026-05-field-trial-6.md +0 -0
  47. {nene2_python-1.1.0 → nene2_python-1.2.0}/docs/field-trials/2026-05-field-trial-7.md +0 -0
  48. {nene2_python-1.1.0 → nene2_python-1.2.0}/docs/field-trials/2026-05-field-trial-8.md +0 -0
  49. {nene2_python-1.1.0 → nene2_python-1.2.0}/docs/fr/index.md +0 -0
  50. {nene2_python-1.1.0 → nene2_python-1.2.0}/docs/fr/tutorials/getting-started.md +0 -0
  51. {nene2_python-1.1.0 → nene2_python-1.2.0}/docs/how-to/add-new-domain.md +0 -0
  52. {nene2_python-1.1.0 → nene2_python-1.2.0}/docs/how-to/configure-auth.md +0 -0
  53. {nene2_python-1.1.0 → nene2_python-1.2.0}/docs/how-to/new-project.md +0 -0
  54. {nene2_python-1.1.0 → nene2_python-1.2.0}/docs/how-to/run-tests.md +0 -0
  55. {nene2_python-1.1.0 → nene2_python-1.2.0}/docs/how-to/sqlalchemy-repository.md +0 -0
  56. {nene2_python-1.1.0 → nene2_python-1.2.0}/docs/howto/mcp-setup.md +0 -0
  57. {nene2_python-1.1.0 → nene2_python-1.2.0}/docs/index.md +0 -0
  58. {nene2_python-1.1.0 → nene2_python-1.2.0}/docs/ja/explanation/architecture.md +0 -0
  59. {nene2_python-1.1.0 → nene2_python-1.2.0}/docs/ja/explanation/design-philosophy.md +0 -0
  60. {nene2_python-1.1.0 → nene2_python-1.2.0}/docs/ja/how-to/add-new-domain.md +0 -0
  61. {nene2_python-1.1.0 → nene2_python-1.2.0}/docs/ja/how-to/configure-auth.md +0 -0
  62. {nene2_python-1.1.0 → nene2_python-1.2.0}/docs/ja/how-to/new-project.md +0 -0
  63. {nene2_python-1.1.0 → nene2_python-1.2.0}/docs/ja/how-to/run-tests.md +0 -0
  64. {nene2_python-1.1.0 → nene2_python-1.2.0}/docs/ja/how-to/sqlalchemy-repository.md +0 -0
  65. {nene2_python-1.1.0 → nene2_python-1.2.0}/docs/ja/howto/mcp-setup.md +0 -0
  66. {nene2_python-1.1.0 → nene2_python-1.2.0}/docs/ja/index.md +0 -0
  67. {nene2_python-1.1.0 → nene2_python-1.2.0}/docs/ja/reference/api.md +0 -0
  68. {nene2_python-1.1.0 → nene2_python-1.2.0}/docs/ja/reference/configuration.md +0 -0
  69. {nene2_python-1.1.0 → nene2_python-1.2.0}/docs/ja/reference/framework-modules.md +0 -0
  70. {nene2_python-1.1.0 → nene2_python-1.2.0}/docs/ja/tutorials/first-domain.md +0 -0
  71. {nene2_python-1.1.0 → nene2_python-1.2.0}/docs/ja/tutorials/getting-started.md +0 -0
  72. {nene2_python-1.1.0 → nene2_python-1.2.0}/docs/pt-br/index.md +0 -0
  73. {nene2_python-1.1.0 → nene2_python-1.2.0}/docs/pt-br/tutorials/getting-started.md +0 -0
  74. {nene2_python-1.1.0 → nene2_python-1.2.0}/docs/reference/api.md +0 -0
  75. {nene2_python-1.1.0 → nene2_python-1.2.0}/docs/reference/configuration.md +0 -0
  76. {nene2_python-1.1.0 → nene2_python-1.2.0}/docs/reference/framework-modules.md +0 -0
  77. {nene2_python-1.1.0 → nene2_python-1.2.0}/docs/roadmap.md +0 -0
  78. {nene2_python-1.1.0 → nene2_python-1.2.0}/docs/todo/current.md +0 -0
  79. {nene2_python-1.1.0 → nene2_python-1.2.0}/docs/tutorials/first-domain.md +0 -0
  80. {nene2_python-1.1.0 → nene2_python-1.2.0}/docs/tutorials/getting-started.md +0 -0
  81. {nene2_python-1.1.0 → nene2_python-1.2.0}/docs/zh/index.md +0 -0
  82. {nene2_python-1.1.0 → nene2_python-1.2.0}/docs/zh/tutorials/getting-started.md +0 -0
  83. {nene2_python-1.1.0 → nene2_python-1.2.0}/package-lock.json +0 -0
  84. {nene2_python-1.1.0 → nene2_python-1.2.0}/package.json +0 -0
  85. {nene2_python-1.1.0 → nene2_python-1.2.0}/src/example/__init__.py +0 -0
  86. {nene2_python-1.1.0 → nene2_python-1.2.0}/src/example/__main__.py +0 -0
  87. {nene2_python-1.1.0 → nene2_python-1.2.0}/src/example/app.py +0 -0
  88. {nene2_python-1.1.0 → nene2_python-1.2.0}/src/example/comment/__init__.py +0 -0
  89. {nene2_python-1.1.0 → nene2_python-1.2.0}/src/example/comment/entity.py +0 -0
  90. {nene2_python-1.1.0 → nene2_python-1.2.0}/src/example/comment/exceptions.py +0 -0
  91. {nene2_python-1.1.0 → nene2_python-1.2.0}/src/example/comment/handler.py +0 -0
  92. {nene2_python-1.1.0 → nene2_python-1.2.0}/src/example/comment/repository.py +0 -0
  93. {nene2_python-1.1.0 → nene2_python-1.2.0}/src/example/comment/sqlalchemy_repository.py +0 -0
  94. {nene2_python-1.1.0 → nene2_python-1.2.0}/src/example/comment/use_case.py +0 -0
  95. {nene2_python-1.1.0 → nene2_python-1.2.0}/src/example/mcp.py +0 -0
  96. {nene2_python-1.1.0 → nene2_python-1.2.0}/src/example/note/__init__.py +0 -0
  97. {nene2_python-1.1.0 → nene2_python-1.2.0}/src/example/note/async_use_case.py +0 -0
  98. {nene2_python-1.1.0 → nene2_python-1.2.0}/src/example/note/entity.py +0 -0
  99. {nene2_python-1.1.0 → nene2_python-1.2.0}/src/example/note/exceptions.py +0 -0
  100. {nene2_python-1.1.0 → nene2_python-1.2.0}/src/example/note/handler.py +0 -0
  101. {nene2_python-1.1.0 → nene2_python-1.2.0}/src/example/note/repository.py +0 -0
  102. {nene2_python-1.1.0 → nene2_python-1.2.0}/src/example/note/sqlalchemy_repository.py +0 -0
  103. {nene2_python-1.1.0 → nene2_python-1.2.0}/src/example/note/use_case.py +0 -0
  104. {nene2_python-1.1.0 → nene2_python-1.2.0}/src/example/schema.py +0 -0
  105. {nene2_python-1.1.0 → nene2_python-1.2.0}/src/example/tag/__init__.py +0 -0
  106. {nene2_python-1.1.0 → nene2_python-1.2.0}/src/example/tag/entity.py +0 -0
  107. {nene2_python-1.1.0 → nene2_python-1.2.0}/src/example/tag/exceptions.py +0 -0
  108. {nene2_python-1.1.0 → nene2_python-1.2.0}/src/example/tag/handler.py +0 -0
  109. {nene2_python-1.1.0 → nene2_python-1.2.0}/src/example/tag/repository.py +0 -0
  110. {nene2_python-1.1.0 → nene2_python-1.2.0}/src/example/tag/sqlalchemy_repository.py +0 -0
  111. {nene2_python-1.1.0 → nene2_python-1.2.0}/src/example/tag/use_case.py +0 -0
  112. {nene2_python-1.1.0 → nene2_python-1.2.0}/src/nene2/__init__.py +0 -0
  113. {nene2_python-1.1.0 → nene2_python-1.2.0}/src/nene2/auth/__init__.py +0 -0
  114. {nene2_python-1.1.0 → nene2_python-1.2.0}/src/nene2/auth/api_key.py +0 -0
  115. {nene2_python-1.1.0 → nene2_python-1.2.0}/src/nene2/auth/bearer_token.py +0 -0
  116. {nene2_python-1.1.0 → nene2_python-1.2.0}/src/nene2/auth/exceptions.py +0 -0
  117. {nene2_python-1.1.0 → nene2_python-1.2.0}/src/nene2/auth/interfaces.py +0 -0
  118. {nene2_python-1.1.0 → nene2_python-1.2.0}/src/nene2/auth/local_verifier.py +0 -0
  119. {nene2_python-1.1.0 → nene2_python-1.2.0}/src/nene2/config/__init__.py +0 -0
  120. {nene2_python-1.1.0 → nene2_python-1.2.0}/src/nene2/config/settings.py +0 -0
  121. {nene2_python-1.1.0 → nene2_python-1.2.0}/src/nene2/database/__init__.py +0 -0
  122. {nene2_python-1.1.0 → nene2_python-1.2.0}/src/nene2/database/exceptions.py +0 -0
  123. {nene2_python-1.1.0 → nene2_python-1.2.0}/src/nene2/database/health.py +0 -0
  124. {nene2_python-1.1.0 → nene2_python-1.2.0}/src/nene2/database/interfaces.py +0 -0
  125. {nene2_python-1.1.0 → nene2_python-1.2.0}/src/nene2/database/sqlalchemy_executor.py +0 -0
  126. {nene2_python-1.1.0 → nene2_python-1.2.0}/src/nene2/http/__init__.py +0 -0
  127. {nene2_python-1.1.0 → nene2_python-1.2.0}/src/nene2/http/health.py +0 -0
  128. {nene2_python-1.1.0 → nene2_python-1.2.0}/src/nene2/http/pagination.py +0 -0
  129. {nene2_python-1.1.0 → nene2_python-1.2.0}/src/nene2/http/problem_details.py +0 -0
  130. {nene2_python-1.1.0 → nene2_python-1.2.0}/src/nene2/log/__init__.py +0 -0
  131. {nene2_python-1.1.0 → nene2_python-1.2.0}/src/nene2/log/setup.py +0 -0
  132. {nene2_python-1.1.0 → nene2_python-1.2.0}/src/nene2/middleware/__init__.py +0 -0
  133. {nene2_python-1.1.0 → nene2_python-1.2.0}/src/nene2/middleware/domain_exception.py +0 -0
  134. {nene2_python-1.1.0 → nene2_python-1.2.0}/src/nene2/middleware/error_handler.py +0 -0
  135. {nene2_python-1.1.0 → nene2_python-1.2.0}/src/nene2/middleware/request_id.py +0 -0
  136. {nene2_python-1.1.0 → nene2_python-1.2.0}/src/nene2/middleware/request_logging.py +0 -0
  137. {nene2_python-1.1.0 → nene2_python-1.2.0}/src/nene2/middleware/request_size_limit.py +0 -0
  138. {nene2_python-1.1.0 → nene2_python-1.2.0}/src/nene2/middleware/security_headers.py +0 -0
  139. {nene2_python-1.1.0 → nene2_python-1.2.0}/src/nene2/middleware/throttle.py +0 -0
  140. {nene2_python-1.1.0 → nene2_python-1.2.0}/src/nene2/py.typed +0 -0
  141. {nene2_python-1.1.0 → nene2_python-1.2.0}/src/nene2/use_case/__init__.py +0 -0
  142. {nene2_python-1.1.0 → nene2_python-1.2.0}/src/nene2/use_case/protocols.py +0 -0
  143. {nene2_python-1.1.0 → nene2_python-1.2.0}/src/nene2/validation/__init__.py +0 -0
  144. {nene2_python-1.1.0 → nene2_python-1.2.0}/src/nene2/validation/exceptions.py +0 -0
  145. {nene2_python-1.1.0 → nene2_python-1.2.0}/src/scripts/__init__.py +0 -0
  146. {nene2_python-1.1.0 → nene2_python-1.2.0}/src/scripts/export_openapi.py +0 -0
  147. {nene2_python-1.1.0 → nene2_python-1.2.0}/tests/__init__.py +0 -0
  148. {nene2_python-1.1.0 → nene2_python-1.2.0}/tests/example/__init__.py +0 -0
  149. {nene2_python-1.1.0 → nene2_python-1.2.0}/tests/example/comment/__init__.py +0 -0
  150. {nene2_python-1.1.0 → nene2_python-1.2.0}/tests/example/comment/test_comment_http.py +0 -0
  151. {nene2_python-1.1.0 → nene2_python-1.2.0}/tests/example/comment/test_comment_repository.py +0 -0
  152. {nene2_python-1.1.0 → nene2_python-1.2.0}/tests/example/comment/test_comment_use_case.py +0 -0
  153. {nene2_python-1.1.0 → nene2_python-1.2.0}/tests/example/conftest.py +0 -0
  154. {nene2_python-1.1.0 → nene2_python-1.2.0}/tests/example/note/__init__.py +0 -0
  155. {nene2_python-1.1.0 → nene2_python-1.2.0}/tests/example/note/test_async_note_use_case.py +0 -0
  156. {nene2_python-1.1.0 → nene2_python-1.2.0}/tests/example/note/test_list_notes.py +0 -0
  157. {nene2_python-1.1.0 → nene2_python-1.2.0}/tests/example/note/test_note_repository.py +0 -0
  158. {nene2_python-1.1.0 → nene2_python-1.2.0}/tests/example/tag/__init__.py +0 -0
  159. {nene2_python-1.1.0 → nene2_python-1.2.0}/tests/example/tag/test_tag_repository.py +0 -0
  160. {nene2_python-1.1.0 → nene2_python-1.2.0}/tests/example/tag/test_tags.py +0 -0
  161. {nene2_python-1.1.0 → nene2_python-1.2.0}/tests/example/test_cors.py +0 -0
  162. {nene2_python-1.1.0 → nene2_python-1.2.0}/tests/example/test_mcp.py +0 -0
  163. {nene2_python-1.1.0 → nene2_python-1.2.0}/tests/nene2/__init__.py +0 -0
  164. {nene2_python-1.1.0 → nene2_python-1.2.0}/tests/nene2/auth/__init__.py +0 -0
  165. {nene2_python-1.1.0 → nene2_python-1.2.0}/tests/nene2/auth/test_api_key.py +0 -0
  166. {nene2_python-1.1.0 → nene2_python-1.2.0}/tests/nene2/auth/test_bearer_token.py +0 -0
  167. {nene2_python-1.1.0 → nene2_python-1.2.0}/tests/nene2/auth/test_token_issuer.py +0 -0
  168. {nene2_python-1.1.0 → nene2_python-1.2.0}/tests/nene2/database/__init__.py +0 -0
  169. {nene2_python-1.1.0 → nene2_python-1.2.0}/tests/nene2/database/test_transaction.py +0 -0
  170. {nene2_python-1.1.0 → nene2_python-1.2.0}/tests/nene2/database/test_utils.py +0 -0
  171. {nene2_python-1.1.0 → nene2_python-1.2.0}/tests/nene2/http/__init__.py +0 -0
  172. {nene2_python-1.1.0 → nene2_python-1.2.0}/tests/nene2/http/test_pagination.py +0 -0
  173. {nene2_python-1.1.0 → nene2_python-1.2.0}/tests/nene2/mcp/__init__.py +0 -0
  174. {nene2_python-1.1.0 → nene2_python-1.2.0}/tests/nene2/middleware/__init__.py +0 -0
  175. {nene2_python-1.1.0 → nene2_python-1.2.0}/tests/nene2/middleware/test_error_handler.py +0 -0
  176. {nene2_python-1.1.0 → nene2_python-1.2.0}/tests/nene2/middleware/test_request_id.py +0 -0
  177. {nene2_python-1.1.0 → nene2_python-1.2.0}/tests/nene2/middleware/test_request_logging.py +0 -0
  178. {nene2_python-1.1.0 → nene2_python-1.2.0}/tests/nene2/middleware/test_request_size_limit.py +0 -0
  179. {nene2_python-1.1.0 → nene2_python-1.2.0}/tests/nene2/middleware/test_security_headers.py +0 -0
  180. {nene2_python-1.1.0 → nene2_python-1.2.0}/tests/nene2/middleware/test_throttle.py +0 -0
  181. {nene2_python-1.1.0 → nene2_python-1.2.0}/tests/nene2/use_case/__init__.py +0 -0
  182. {nene2_python-1.1.0 → nene2_python-1.2.0}/tests/nene2/use_case/test_protocols.py +0 -0
  183. {nene2_python-1.1.0 → nene2_python-1.2.0}/tests/nene2/validation/__init__.py +0 -0
  184. {nene2_python-1.1.0 → nene2_python-1.2.0}/tests/nene2/validation/test_exceptions.py +0 -0
  185. {nene2_python-1.1.0 → nene2_python-1.2.0}/tests/scripts/__init__.py +0 -0
  186. {nene2_python-1.1.0 → nene2_python-1.2.0}/tests/scripts/test_export_openapi.py +0 -0
  187. {nene2_python-1.1.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.1.0
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,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` との差異を確認
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "nene2-python"
3
- version = "1.1.0"
3
+ version = "1.2.0"
4
4
  description = "NENE2 Python — minimal API framework following NENE2's design philosophy"
5
5
  readme = "README.md"
6
6
  license = {text = "MIT"}
@@ -16,6 +16,7 @@ def parse_db_datetime(value: str | datetime) -> datetime:
16
16
 
17
17
  from nene2.database import parse_db_datetime
18
18
 
19
+
19
20
  def _to_post(row: dict[str, Any]) -> Post:
20
21
  return Post(
21
22
  id=row["id"],
@@ -1,11 +1,12 @@
1
1
  """NENE2 MCP integration — expose UseCases as MCP tools."""
2
2
 
3
- from .http_client import HttpxMcpClient, McpHttpClientProtocol, McpHttpResponse
3
+ from .http_client import HttpxMcpClient, McpHttpClientProtocol, McpHttpError, McpHttpResponse
4
4
  from .server import LocalMcpServer
5
5
 
6
6
  __all__ = [
7
7
  "HttpxMcpClient",
8
8
  "LocalMcpServer",
9
9
  "McpHttpClientProtocol",
10
+ "McpHttpError",
10
11
  "McpHttpResponse",
11
12
  ]
@@ -11,6 +11,15 @@ import httpx
11
11
  from httpx import BaseTransport
12
12
 
13
13
 
14
+ class McpHttpError(Exception):
15
+ """Raised by McpHttpResponse.raise_for_error() on 4xx / 5xx responses."""
16
+
17
+ def __init__(self, status_code: int, body: str) -> None:
18
+ super().__init__(f"HTTP {status_code}: {body}")
19
+ self.status_code = status_code
20
+ self.body = body
21
+
22
+
14
23
  @dataclass(frozen=True, slots=True)
15
24
  class McpHttpResponse:
16
25
  """HTTP response value object returned by McpHttpClientProtocol."""
@@ -22,6 +31,19 @@ class McpHttpResponse:
22
31
  def is_successful(self) -> bool:
23
32
  return 200 <= self.status_code < 300
24
33
 
34
+ def raise_for_error(self) -> None:
35
+ """Raise McpHttpError if the response status code indicates an error (4xx / 5xx).
36
+
37
+ Use this in MCP tool handlers to propagate HTTP errors as tool errors::
38
+
39
+ def get_recipe(recipe_id: int) -> str:
40
+ response = client.get(API_BASE, f"/recipes/{recipe_id}")
41
+ response.raise_for_error()
42
+ return response.body
43
+ """
44
+ if not self.is_successful():
45
+ raise McpHttpError(self.status_code, self.body)
46
+
25
47
  def request_id(self) -> str | None:
26
48
  return self.headers.get("x-request-id")
27
49
 
@@ -13,8 +13,15 @@ from mcp.server.fastmcp import FastMCP
13
13
  class LocalMcpServer:
14
14
  """MCP server with sensible defaults for local / stdio transport."""
15
15
 
16
- def __init__(self, name: str, instructions: str = "") -> None:
17
- self._mcp = FastMCP(name, instructions=instructions)
16
+ def __init__(
17
+ self,
18
+ name: str,
19
+ instructions: str = "",
20
+ *,
21
+ host: str = "127.0.0.1",
22
+ port: int = 8000,
23
+ ) -> None:
24
+ self._mcp = FastMCP(name, instructions=instructions, host=host, port=port)
18
25
 
19
26
  def tool(self, description: str = "") -> Callable[[Any], Any]:
20
27
  """Register a function as an MCP tool."""
@@ -3,8 +3,9 @@
3
3
  import json
4
4
 
5
5
  import httpx
6
+ import pytest
6
7
 
7
- from nene2.mcp import HttpxMcpClient, McpHttpClientProtocol, McpHttpResponse
8
+ from nene2.mcp import HttpxMcpClient, McpHttpClientProtocol, McpHttpError, McpHttpResponse
8
9
 
9
10
 
10
11
  def _mock_transport(status: int, body: dict[str, object]) -> httpx.MockTransport:
@@ -31,6 +32,29 @@ def test_mcp_http_response_request_id() -> None:
31
32
  assert McpHttpResponse(200, {}, "").request_id() is None
32
33
 
33
34
 
35
+ def test_raise_for_error_does_nothing_on_success() -> None:
36
+ McpHttpResponse(200, {}, "ok").raise_for_error()
37
+ McpHttpResponse(204, {}, "").raise_for_error()
38
+
39
+
40
+ def test_raise_for_error_raises_on_4xx() -> None:
41
+ with pytest.raises(McpHttpError) as exc_info:
42
+ McpHttpResponse(404, {}, '{"detail":"not found"}').raise_for_error()
43
+ assert exc_info.value.status_code == 404
44
+ assert "not found" in exc_info.value.body
45
+
46
+
47
+ def test_raise_for_error_raises_on_5xx() -> None:
48
+ with pytest.raises(McpHttpError):
49
+ McpHttpResponse(500, {}, "server error").raise_for_error()
50
+
51
+
52
+ def test_mcp_http_error_message_includes_status_and_body() -> None:
53
+ error = McpHttpError(404, "not found")
54
+ assert "404" in str(error)
55
+ assert "not found" in str(error)
56
+
57
+
34
58
  def test_httpx_mcp_client_satisfies_protocol() -> None:
35
59
  assert isinstance(HttpxMcpClient(), McpHttpClientProtocol)
36
60
 
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes