tina4-python 3.10.31__tar.gz → 3.10.34__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 (142) hide show
  1. {tina4_python-3.10.31 → tina4_python-3.10.34}/PKG-INFO +1 -1
  2. {tina4_python-3.10.31 → tina4_python-3.10.34}/pyproject.toml +1 -1
  3. {tina4_python-3.10.31 → tina4_python-3.10.34}/tina4_python/dev_admin/__init__.py +54 -1
  4. {tina4_python-3.10.31 → tina4_python-3.10.34}/tina4_python/frond/engine.py +28 -4
  5. tina4_python-3.10.34/tina4_python/mcp/__init__.py +361 -0
  6. tina4_python-3.10.34/tina4_python/mcp/protocol.py +75 -0
  7. tina4_python-3.10.34/tina4_python/mcp/tools.py +348 -0
  8. {tina4_python-3.10.31 → tina4_python-3.10.34}/.gitignore +0 -0
  9. {tina4_python-3.10.31 → tina4_python-3.10.34}/README.md +0 -0
  10. {tina4_python-3.10.31 → tina4_python-3.10.34}/tina4_python/CLAUDE.md +0 -0
  11. {tina4_python-3.10.31 → tina4_python-3.10.34}/tina4_python/HtmlElement.py +0 -0
  12. {tina4_python-3.10.31 → tina4_python-3.10.34}/tina4_python/Testing.py +0 -0
  13. {tina4_python-3.10.31 → tina4_python-3.10.34}/tina4_python/__init__.py +0 -0
  14. {tina4_python-3.10.31 → tina4_python-3.10.34}/tina4_python/ai/__init__.py +0 -0
  15. {tina4_python-3.10.31 → tina4_python-3.10.34}/tina4_python/api/__init__.py +0 -0
  16. {tina4_python-3.10.31 → tina4_python-3.10.34}/tina4_python/auth/__init__.py +0 -0
  17. {tina4_python-3.10.31 → tina4_python-3.10.34}/tina4_python/cache/__init__.py +0 -0
  18. {tina4_python-3.10.31 → tina4_python-3.10.34}/tina4_python/cli/__init__.py +0 -0
  19. {tina4_python-3.10.31 → tina4_python-3.10.34}/tina4_python/container/__init__.py +0 -0
  20. {tina4_python-3.10.31 → tina4_python-3.10.34}/tina4_python/core/__init__.py +0 -0
  21. {tina4_python-3.10.31 → tina4_python-3.10.34}/tina4_python/core/cache.py +0 -0
  22. {tina4_python-3.10.31 → tina4_python-3.10.34}/tina4_python/core/constants.py +0 -0
  23. {tina4_python-3.10.31 → tina4_python-3.10.34}/tina4_python/core/events.py +0 -0
  24. {tina4_python-3.10.31 → tina4_python-3.10.34}/tina4_python/core/middleware.py +0 -0
  25. {tina4_python-3.10.31 → tina4_python-3.10.34}/tina4_python/core/request.py +0 -0
  26. {tina4_python-3.10.31 → tina4_python-3.10.34}/tina4_python/core/response.py +0 -0
  27. {tina4_python-3.10.31 → tina4_python-3.10.34}/tina4_python/core/router.py +0 -0
  28. {tina4_python-3.10.31 → tina4_python-3.10.34}/tina4_python/core/server.py +0 -0
  29. {tina4_python-3.10.31 → tina4_python-3.10.34}/tina4_python/crud/__init__.py +0 -0
  30. {tina4_python-3.10.31 → tina4_python-3.10.34}/tina4_python/database/__init__.py +0 -0
  31. {tina4_python-3.10.31 → tina4_python-3.10.34}/tina4_python/database/adapter.py +0 -0
  32. {tina4_python-3.10.31 → tina4_python-3.10.34}/tina4_python/database/connection.py +0 -0
  33. {tina4_python-3.10.31 → tina4_python-3.10.34}/tina4_python/database/firebird.py +0 -0
  34. {tina4_python-3.10.31 → tina4_python-3.10.34}/tina4_python/database/mssql.py +0 -0
  35. {tina4_python-3.10.31 → tina4_python-3.10.34}/tina4_python/database/mysql.py +0 -0
  36. {tina4_python-3.10.31 → tina4_python-3.10.34}/tina4_python/database/odbc.py +0 -0
  37. {tina4_python-3.10.31 → tina4_python-3.10.34}/tina4_python/database/postgres.py +0 -0
  38. {tina4_python-3.10.31 → tina4_python-3.10.34}/tina4_python/database/sqlite.py +0 -0
  39. {tina4_python-3.10.31 → tina4_python-3.10.34}/tina4_python/debug/__init__.py +0 -0
  40. {tina4_python-3.10.31 → tina4_python-3.10.34}/tina4_python/debug/error_overlay.py +0 -0
  41. {tina4_python-3.10.31 → tina4_python-3.10.34}/tina4_python/dev_reload.py +0 -0
  42. {tina4_python-3.10.31 → tina4_python-3.10.34}/tina4_python/dotenv/__init__.py +0 -0
  43. {tina4_python-3.10.31 → tina4_python-3.10.34}/tina4_python/frond/FROND.md +0 -0
  44. {tina4_python-3.10.31 → tina4_python-3.10.34}/tina4_python/frond/__init__.py +0 -0
  45. {tina4_python-3.10.31 → tina4_python-3.10.34}/tina4_python/gallery/auth/meta.json +0 -0
  46. {tina4_python-3.10.31 → tina4_python-3.10.34}/tina4_python/gallery/auth/src/routes/api/gallery_auth.py +0 -0
  47. {tina4_python-3.10.31 → tina4_python-3.10.34}/tina4_python/gallery/database/meta.json +0 -0
  48. {tina4_python-3.10.31 → tina4_python-3.10.34}/tina4_python/gallery/database/src/routes/api/gallery_db.py +0 -0
  49. {tina4_python-3.10.31 → tina4_python-3.10.34}/tina4_python/gallery/error-overlay/meta.json +0 -0
  50. {tina4_python-3.10.31 → tina4_python-3.10.34}/tina4_python/gallery/error-overlay/src/routes/api/gallery_crash.py +0 -0
  51. {tina4_python-3.10.31 → tina4_python-3.10.34}/tina4_python/gallery/orm/meta.json +0 -0
  52. {tina4_python-3.10.31 → tina4_python-3.10.34}/tina4_python/gallery/orm/src/orm/Product.py +0 -0
  53. {tina4_python-3.10.31 → tina4_python-3.10.34}/tina4_python/gallery/orm/src/routes/api/gallery_products.py +0 -0
  54. {tina4_python-3.10.31 → tina4_python-3.10.34}/tina4_python/gallery/queue/meta.json +0 -0
  55. {tina4_python-3.10.31 → tina4_python-3.10.34}/tina4_python/gallery/queue/src/routes/api/gallery_queue.py +0 -0
  56. {tina4_python-3.10.31 → tina4_python-3.10.34}/tina4_python/gallery/rest-api/meta.json +0 -0
  57. {tina4_python-3.10.31 → tina4_python-3.10.34}/tina4_python/gallery/rest-api/src/routes/api/gallery_hello.py +0 -0
  58. {tina4_python-3.10.31 → tina4_python-3.10.34}/tina4_python/gallery/templates/meta.json +0 -0
  59. {tina4_python-3.10.31 → tina4_python-3.10.34}/tina4_python/gallery/templates/src/routes/gallery_page.py +0 -0
  60. {tina4_python-3.10.31 → tina4_python-3.10.34}/tina4_python/gallery/templates/src/templates/gallery_page.twig +0 -0
  61. {tina4_python-3.10.31 → tina4_python-3.10.34}/tina4_python/graphql/__init__.py +0 -0
  62. {tina4_python-3.10.31 → tina4_python-3.10.34}/tina4_python/i18n/__init__.py +0 -0
  63. {tina4_python-3.10.31 → tina4_python-3.10.34}/tina4_python/messenger/__init__.py +0 -0
  64. {tina4_python-3.10.31 → tina4_python-3.10.34}/tina4_python/migration/__init__.py +0 -0
  65. {tina4_python-3.10.31 → tina4_python-3.10.34}/tina4_python/migration/runner.py +0 -0
  66. {tina4_python-3.10.31 → tina4_python-3.10.34}/tina4_python/orm/__init__.py +0 -0
  67. {tina4_python-3.10.31 → tina4_python-3.10.34}/tina4_python/orm/fields.py +0 -0
  68. {tina4_python-3.10.31 → tina4_python-3.10.34}/tina4_python/orm/model.py +0 -0
  69. {tina4_python-3.10.31 → tina4_python-3.10.34}/tina4_python/public/css/tina4.css +0 -0
  70. {tina4_python-3.10.31 → tina4_python-3.10.34}/tina4_python/public/css/tina4.min.css +0 -0
  71. {tina4_python-3.10.31 → tina4_python-3.10.34}/tina4_python/public/favicon.ico +0 -0
  72. {tina4_python-3.10.31 → tina4_python-3.10.34}/tina4_python/public/images/logo.svg +0 -0
  73. {tina4_python-3.10.31 → tina4_python-3.10.34}/tina4_python/public/images/tina4-logo-icon.webp +0 -0
  74. {tina4_python-3.10.31 → tina4_python-3.10.34}/tina4_python/public/js/frond.min.js +0 -0
  75. {tina4_python-3.10.31 → tina4_python-3.10.34}/tina4_python/public/js/tina4-dev-admin.min.js +0 -0
  76. {tina4_python-3.10.31 → tina4_python-3.10.34}/tina4_python/public/js/tina4.min.js +0 -0
  77. {tina4_python-3.10.31 → tina4_python-3.10.34}/tina4_python/public/js/tina4js.min.js +0 -0
  78. {tina4_python-3.10.31 → tina4_python-3.10.34}/tina4_python/public/swagger/index.html +0 -0
  79. {tina4_python-3.10.31 → tina4_python-3.10.34}/tina4_python/public/swagger/oauth2-redirect.html +0 -0
  80. {tina4_python-3.10.31 → tina4_python-3.10.34}/tina4_python/query_builder/__init__.py +0 -0
  81. {tina4_python-3.10.31 → tina4_python-3.10.34}/tina4_python/queue/__init__.py +0 -0
  82. {tina4_python-3.10.31 → tina4_python-3.10.34}/tina4_python/queue_backends/__init__.py +0 -0
  83. {tina4_python-3.10.31 → tina4_python-3.10.34}/tina4_python/queue_backends/kafka_backend.py +0 -0
  84. {tina4_python-3.10.31 → tina4_python-3.10.34}/tina4_python/queue_backends/mongo_backend.py +0 -0
  85. {tina4_python-3.10.31 → tina4_python-3.10.34}/tina4_python/queue_backends/rabbitmq_backend.py +0 -0
  86. {tina4_python-3.10.31 → tina4_python-3.10.34}/tina4_python/scss/__init__.py +0 -0
  87. {tina4_python-3.10.31 → tina4_python-3.10.34}/tina4_python/scss/tina4css/_alerts.scss +0 -0
  88. {tina4_python-3.10.31 → tina4_python-3.10.34}/tina4_python/scss/tina4css/_badges.scss +0 -0
  89. {tina4_python-3.10.31 → tina4_python-3.10.34}/tina4_python/scss/tina4css/_buttons.scss +0 -0
  90. {tina4_python-3.10.31 → tina4_python-3.10.34}/tina4_python/scss/tina4css/_cards.scss +0 -0
  91. {tina4_python-3.10.31 → tina4_python-3.10.34}/tina4_python/scss/tina4css/_forms.scss +0 -0
  92. {tina4_python-3.10.31 → tina4_python-3.10.34}/tina4_python/scss/tina4css/_grid.scss +0 -0
  93. {tina4_python-3.10.31 → tina4_python-3.10.34}/tina4_python/scss/tina4css/_modals.scss +0 -0
  94. {tina4_python-3.10.31 → tina4_python-3.10.34}/tina4_python/scss/tina4css/_nav.scss +0 -0
  95. {tina4_python-3.10.31 → tina4_python-3.10.34}/tina4_python/scss/tina4css/_reset.scss +0 -0
  96. {tina4_python-3.10.31 → tina4_python-3.10.34}/tina4_python/scss/tina4css/_tables.scss +0 -0
  97. {tina4_python-3.10.31 → tina4_python-3.10.34}/tina4_python/scss/tina4css/_typography.scss +0 -0
  98. {tina4_python-3.10.31 → tina4_python-3.10.34}/tina4_python/scss/tina4css/_utilities.scss +0 -0
  99. {tina4_python-3.10.31 → tina4_python-3.10.34}/tina4_python/scss/tina4css/_variables.scss +0 -0
  100. {tina4_python-3.10.31 → tina4_python-3.10.34}/tina4_python/scss/tina4css/base.scss +0 -0
  101. {tina4_python-3.10.31 → tina4_python-3.10.34}/tina4_python/scss/tina4css/colors.scss +0 -0
  102. {tina4_python-3.10.31 → tina4_python-3.10.34}/tina4_python/scss/tina4css/tina4.scss +0 -0
  103. {tina4_python-3.10.31 → tina4_python-3.10.34}/tina4_python/seeder/__init__.py +0 -0
  104. {tina4_python-3.10.31 → tina4_python-3.10.34}/tina4_python/service/__init__.py +0 -0
  105. {tina4_python-3.10.31 → tina4_python-3.10.34}/tina4_python/session/__init__.py +0 -0
  106. {tina4_python-3.10.31 → tina4_python-3.10.34}/tina4_python/session_handlers/__init__.py +0 -0
  107. {tina4_python-3.10.31 → tina4_python-3.10.34}/tina4_python/session_handlers/mongodb_handler.py +0 -0
  108. {tina4_python-3.10.31 → tina4_python-3.10.34}/tina4_python/session_handlers/redis_handler.py +0 -0
  109. {tina4_python-3.10.31 → tina4_python-3.10.34}/tina4_python/session_handlers/valkey_handler.py +0 -0
  110. {tina4_python-3.10.31 → tina4_python-3.10.34}/tina4_python/swagger/__init__.py +0 -0
  111. {tina4_python-3.10.31 → tina4_python-3.10.34}/tina4_python/templates/components/crud.twig +0 -0
  112. {tina4_python-3.10.31 → tina4_python-3.10.34}/tina4_python/templates/docker/distroless/Dockerfile +0 -0
  113. {tina4_python-3.10.31 → tina4_python-3.10.34}/tina4_python/templates/docker/poetry/Dockerfile +0 -0
  114. {tina4_python-3.10.31 → tina4_python-3.10.34}/tina4_python/templates/docker/python/Dockerfile +0 -0
  115. {tina4_python-3.10.31 → tina4_python-3.10.34}/tina4_python/templates/docker/uv/Dockerfile +0 -0
  116. {tina4_python-3.10.31 → tina4_python-3.10.34}/tina4_python/templates/errors/302.twig +0 -0
  117. {tina4_python-3.10.31 → tina4_python-3.10.34}/tina4_python/templates/errors/401.twig +0 -0
  118. {tina4_python-3.10.31 → tina4_python-3.10.34}/tina4_python/templates/errors/403.twig +0 -0
  119. {tina4_python-3.10.31 → tina4_python-3.10.34}/tina4_python/templates/errors/404.twig +0 -0
  120. {tina4_python-3.10.31 → tina4_python-3.10.34}/tina4_python/templates/errors/500.twig +0 -0
  121. {tina4_python-3.10.31 → tina4_python-3.10.34}/tina4_python/templates/errors/502.twig +0 -0
  122. {tina4_python-3.10.31 → tina4_python-3.10.34}/tina4_python/templates/errors/503.twig +0 -0
  123. {tina4_python-3.10.31 → tina4_python-3.10.34}/tina4_python/templates/errors/base.twig +0 -0
  124. {tina4_python-3.10.31 → tina4_python-3.10.34}/tina4_python/templates/frontend/README.md +0 -0
  125. {tina4_python-3.10.31 → tina4_python-3.10.34}/tina4_python/templates/readme.md +0 -0
  126. {tina4_python-3.10.31 → tina4_python-3.10.34}/tina4_python/test_client/__init__.py +0 -0
  127. {tina4_python-3.10.31 → tina4_python-3.10.34}/tina4_python/translations/af/LC_MESSAGES/messages.mo +0 -0
  128. {tina4_python-3.10.31 → tina4_python-3.10.34}/tina4_python/translations/af/LC_MESSAGES/messages.po +0 -0
  129. {tina4_python-3.10.31 → tina4_python-3.10.34}/tina4_python/translations/en/LC_MESSAGES/messages.mo +0 -0
  130. {tina4_python-3.10.31 → tina4_python-3.10.34}/tina4_python/translations/en/LC_MESSAGES/messages.po +0 -0
  131. {tina4_python-3.10.31 → tina4_python-3.10.34}/tina4_python/translations/es/LC_MESSAGES/messages.mo +0 -0
  132. {tina4_python-3.10.31 → tina4_python-3.10.34}/tina4_python/translations/es/LC_MESSAGES/messages.po +0 -0
  133. {tina4_python-3.10.31 → tina4_python-3.10.34}/tina4_python/translations/fr/LC_MESSAGES/messages.mo +0 -0
  134. {tina4_python-3.10.31 → tina4_python-3.10.34}/tina4_python/translations/fr/LC_MESSAGES/messages.po +0 -0
  135. {tina4_python-3.10.31 → tina4_python-3.10.34}/tina4_python/translations/ja/LC_MESSAGES/messages.mo +0 -0
  136. {tina4_python-3.10.31 → tina4_python-3.10.34}/tina4_python/translations/ja/LC_MESSAGES/messages.po +0 -0
  137. {tina4_python-3.10.31 → tina4_python-3.10.34}/tina4_python/translations/zh/LC_MESSAGES/messages.mo +0 -0
  138. {tina4_python-3.10.31 → tina4_python-3.10.34}/tina4_python/translations/zh/LC_MESSAGES/messages.po +0 -0
  139. {tina4_python-3.10.31 → tina4_python-3.10.34}/tina4_python/validator/__init__.py +0 -0
  140. {tina4_python-3.10.31 → tina4_python-3.10.34}/tina4_python/websocket/__init__.py +0 -0
  141. {tina4_python-3.10.31 → tina4_python-3.10.34}/tina4_python/websocket/backplane.py +0 -0
  142. {tina4_python-3.10.31 → tina4_python-3.10.34}/tina4_python/wsdl/__init__.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: tina4-python
3
- Version: 3.10.31
3
+ Version: 3.10.34
4
4
  Summary: Tina4 Python v3 — Zero-dependency, lightweight web framework
5
5
  Author-email: Andre van Zuydam <andrevanzuydam@gmail.com>
6
6
  License: MIT
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "tina4-python"
3
- version = "3.10.31"
3
+ version = "3.10.34"
4
4
  description = "Tina4 Python v3 — Zero-dependency, lightweight web framework"
5
5
  authors = [
6
6
  {name = "Andre van Zuydam", email = "andrevanzuydam@gmail.com"}
@@ -1721,7 +1721,17 @@ def render_dev_toolbar(method: str, path: str, matched_pattern: str,
1721
1721
  poll_interval_ms = int(os.environ.get("TINA4_DEV_POLL_INTERVAL", "3000"))
1722
1722
 
1723
1723
  return f"""<div id="tina4-dev-toolbar" style="position:fixed;bottom:0;left:0;right:0;background:#333;color:#fff;font-family:monospace;font-size:12px;padding:6px 16px;z-index:99999;display:flex;align-items:center;gap:16px;">
1724
- <span style="color:#3572A5;font-weight:bold;">Tina4 v{__version__}</span>
1724
+ <span id="tina4-ver-btn" style="color:#3572A5;font-weight:bold;cursor:pointer;text-decoration:underline dotted;" onclick="tina4VersionModal()" title="Click to check for updates">Tina4 v{__version__}</span>
1725
+ <div id="tina4-ver-modal" style="display:none;position:fixed;bottom:3rem;left:1rem;background:#1e1e2e;border:1px solid #3572A5;border-radius:8px;padding:16px 20px;z-index:100000;min-width:320px;box-shadow:0 8px 32px rgba(0,0,0,0.5);font-family:monospace;font-size:13px;color:#cdd6f4;">
1726
+ <div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:12px;">
1727
+ <strong style="color:#89b4fa;">Version Info</strong>
1728
+ <span onclick="document.getElementById('tina4-ver-modal').style.display='none'" style="cursor:pointer;color:#888;">&times;</span>
1729
+ </div>
1730
+ <div id="tina4-ver-body" style="line-height:1.8;">
1731
+ <div>Current: <strong style="color:#a6e3a1;">v{__version__}</strong></div>
1732
+ <div id="tina4-ver-latest" style="color:#888;">Checking for updates...</div>
1733
+ </div>
1734
+ </div>
1725
1735
  <span style="color:#4caf50;">{method}</span>
1726
1736
  <span>{path}</span>
1727
1737
  <span style="color:#666;">&rarr; {matched_pattern}</span>
@@ -1760,6 +1770,49 @@ def render_dev_toolbar(method: str, path: str, matched_pattern: str,
1760
1770
  }}
1761
1771
  setInterval(_t4_poll,_t4_interval);
1762
1772
  }})();
1773
+ function tina4VersionModal(){{
1774
+ var m=document.getElementById('tina4-ver-modal');
1775
+ if(m.style.display==='block'){{m.style.display='none';return;}}
1776
+ m.style.display='block';
1777
+ var el=document.getElementById('tina4-ver-latest');
1778
+ el.innerHTML='Checking for updates...';
1779
+ el.style.color='#888';
1780
+ fetch('https://pypi.org/pypi/tina4-python/json')
1781
+ .then(function(r){{return r.json()}})
1782
+ .then(function(d){{
1783
+ var latest=d.info.version;
1784
+ var current='{__version__}';
1785
+ if(latest===current){{
1786
+ el.innerHTML='Latest: <strong style="color:#a6e3a1;">v'+latest+'</strong> &mdash; You are up to date!';
1787
+ el.style.color='#a6e3a1';
1788
+ }}else{{
1789
+ var cParts=current.split('.').map(Number);
1790
+ var lParts=latest.split('.').map(Number);
1791
+ var isNewer=false;
1792
+ for(var i=0;i<Math.max(cParts.length,lParts.length);i++){{
1793
+ var c=cParts[i]||0,l=lParts[i]||0;
1794
+ if(l>c){{isNewer=true;break;}}
1795
+ if(l<c)break;
1796
+ }}
1797
+ if(isNewer){{
1798
+ var breaking=(lParts[0]!==cParts[0]||lParts[1]!==cParts[1]);
1799
+ el.innerHTML='Latest: <strong style="color:#f9e2af;">v'+latest+'</strong>';
1800
+ if(breaking){{
1801
+ el.innerHTML+='<div style="color:#f38ba8;margin-top:6px;">&#9888; Major/minor version change &mdash; check the <a href="https://github.com/tina4stack/tina4-python/releases" target="_blank" style="color:#89b4fa;">changelog</a> for breaking changes before upgrading.</div>';
1802
+ }}else{{
1803
+ el.innerHTML+='<div style="color:#f9e2af;margin-top:6px;">Patch update available. Run: <code style="background:#313244;padding:2px 6px;border-radius:3px;">pip install --upgrade tina4-python</code></div>';
1804
+ }}
1805
+ }}else{{
1806
+ el.innerHTML='Latest: <strong style="color:#a6e3a1;">v'+latest+'</strong> &mdash; You are up to date!';
1807
+ el.style.color='#a6e3a1';
1808
+ }}
1809
+ }}
1810
+ }})
1811
+ .catch(function(){{
1812
+ el.innerHTML='Could not check for updates (offline?)';
1813
+ el.style.color='#f38ba8';
1814
+ }});
1815
+ }}
1763
1816
  </script>"""
1764
1817
 
1765
1818
 
@@ -1225,12 +1225,36 @@ class Frond:
1225
1225
  return blocks
1226
1226
 
1227
1227
  def _render_with_blocks(self, parent_source: str, context: dict, child_blocks: dict) -> str:
1228
- """Render parent template, replacing blocks with child content."""
1228
+ """Render parent template, replacing blocks with child content.
1229
+
1230
+ Supports {{ parent() }} / {{ super() }} inside child blocks to include
1231
+ the parent block's content (standard Twig/Jinja2 behavior).
1232
+ """
1233
+ engine = self
1234
+
1229
1235
  def replace_block(m):
1230
1236
  name = m.group(1)
1231
- default_content = m.group(2)
1232
- block_source = child_blocks.get(name, default_content)
1233
- return self._render_tokens(_tokenize(block_source), context)
1237
+ parent_content = m.group(2)
1238
+ block_source = child_blocks.get(name, parent_content)
1239
+
1240
+ # Make parent() and super() available inside child blocks
1241
+ # They return the rendered parent block content
1242
+ rendered_parent = None
1243
+
1244
+ def get_parent():
1245
+ nonlocal rendered_parent
1246
+ if rendered_parent is None:
1247
+ rendered_parent = SafeString(
1248
+ engine._render_tokens(_tokenize(parent_content), context)
1249
+ )
1250
+ return rendered_parent
1251
+
1252
+ # Inject parent/super into a block-local context
1253
+ block_ctx = dict(context)
1254
+ block_ctx["parent"] = get_parent
1255
+ block_ctx["super"] = get_parent
1256
+
1257
+ return engine._render_tokens(_tokenize(block_source), block_ctx)
1234
1258
 
1235
1259
  pattern = _BLOCK_RE
1236
1260
 
@@ -0,0 +1,361 @@
1
+ # Tina4 MCP Server — Model Context Protocol for AI tool integration.
2
+ """
3
+ Built-in MCP server for dev tools + developer API for custom MCP servers.
4
+
5
+ Usage (developer):
6
+
7
+ from tina4_python.mcp import McpServer, mcp_tool, mcp_resource
8
+
9
+ mcp = McpServer("/my-mcp", name="My App Tools")
10
+
11
+ @mcp_tool("lookup_invoice", description="Find invoice by number")
12
+ def lookup_invoice(invoice_no: str):
13
+ return db.fetch_one("SELECT * FROM invoices WHERE invoice_no = ?", [invoice_no])
14
+
15
+ @mcp_resource("app://schema", description="Database schema")
16
+ def get_schema():
17
+ return db.get_tables()
18
+
19
+ Built-in dev tools auto-register when TINA4_DEBUG=true and running on localhost.
20
+ """
21
+ import os
22
+ import json
23
+ import inspect
24
+ import socket
25
+ from pathlib import Path
26
+
27
+ from .protocol import (
28
+ encode_response, encode_error, encode_notification,
29
+ decode_request,
30
+ PARSE_ERROR, INVALID_REQUEST, METHOD_NOT_FOUND,
31
+ INVALID_PARAMS, INTERNAL_ERROR,
32
+ )
33
+
34
+ # Type hint → JSON Schema type mapping (reuse Swagger pattern)
35
+ _TYPE_MAP = {
36
+ str: "string",
37
+ int: "integer",
38
+ float: "number",
39
+ bool: "boolean",
40
+ list: "array",
41
+ dict: "object",
42
+ }
43
+
44
+
45
+ def _schema_from_signature(func) -> dict:
46
+ """Extract JSON Schema input schema from function type hints."""
47
+ sig = inspect.signature(func)
48
+ properties = {}
49
+ required = []
50
+
51
+ for name, param in sig.parameters.items():
52
+ if name == "self":
53
+ continue
54
+ annotation = param.annotation
55
+ prop = {"type": _TYPE_MAP.get(annotation, "string")}
56
+
57
+ if param.default is inspect.Parameter.empty:
58
+ required.append(name)
59
+ else:
60
+ prop["default"] = param.default
61
+
62
+ properties[name] = prop
63
+
64
+ schema = {"type": "object", "properties": properties}
65
+ if required:
66
+ schema["required"] = required
67
+ return schema
68
+
69
+
70
+ def _is_localhost() -> bool:
71
+ """Check if the server is running on localhost."""
72
+ host = os.environ.get("HOST_NAME", "localhost:7145").split(":")[0]
73
+ return host in ("localhost", "127.0.0.1", "0.0.0.0", "::1", "")
74
+
75
+
76
+ class McpServer:
77
+ """MCP server that registers tools and resources on a given HTTP path.
78
+
79
+ Args:
80
+ path: HTTP path to serve the MCP endpoint (e.g. "/my-mcp").
81
+ name: Human-readable server name.
82
+ version: Server version string.
83
+ """
84
+
85
+ # Class-level registry of all MCP server instances
86
+ _instances: list = []
87
+
88
+ def __init__(self, path: str, name: str = "Tina4 MCP", version: str = "1.0.0"):
89
+ self.path = path.rstrip("/")
90
+ self.name = name
91
+ self.version = version
92
+ self._tools: dict[str, dict] = {}
93
+ self._resources: dict[str, dict] = {}
94
+ self._initialized = False
95
+ McpServer._instances.append(self)
96
+
97
+ def register_tool(self, name: str, handler, description: str = "", schema: dict | None = None):
98
+ """Register a tool callable."""
99
+ if schema is None:
100
+ schema = _schema_from_signature(handler)
101
+ self._tools[name] = {
102
+ "name": name,
103
+ "description": description or (handler.__doc__ or "").strip(),
104
+ "inputSchema": schema,
105
+ "handler": handler,
106
+ }
107
+
108
+ def register_resource(self, uri: str, handler, description: str = "", mime_type: str = "application/json"):
109
+ """Register a resource URI."""
110
+ self._resources[uri] = {
111
+ "uri": uri,
112
+ "name": description or uri,
113
+ "description": description or (handler.__doc__ or "").strip(),
114
+ "mimeType": mime_type,
115
+ "handler": handler,
116
+ }
117
+
118
+ def handle_message(self, raw_data: str | dict) -> str:
119
+ """Process an incoming JSON-RPC message and return the response."""
120
+ try:
121
+ method, params, request_id = decode_request(raw_data)
122
+ except ValueError as e:
123
+ return encode_error(None, PARSE_ERROR, str(e))
124
+
125
+ handler = {
126
+ "initialize": self._handle_initialize,
127
+ "notifications/initialized": self._handle_initialized,
128
+ "tools/list": self._handle_tools_list,
129
+ "tools/call": self._handle_tools_call,
130
+ "resources/list": self._handle_resources_list,
131
+ "resources/read": self._handle_resources_read,
132
+ "ping": self._handle_ping,
133
+ }.get(method)
134
+
135
+ if handler is None:
136
+ return encode_error(request_id, METHOD_NOT_FOUND, f"Method not found: {method}")
137
+
138
+ try:
139
+ result = handler(params)
140
+ if request_id is None:
141
+ return "" # Notification — no response
142
+ return encode_response(request_id, result)
143
+ except Exception as e:
144
+ return encode_error(request_id, INTERNAL_ERROR, str(e))
145
+
146
+ def _handle_initialize(self, params: dict) -> dict:
147
+ """Handle initialize request — return server capabilities."""
148
+ self._initialized = True
149
+ return {
150
+ "protocolVersion": "2024-11-05",
151
+ "capabilities": {
152
+ "tools": {"listChanged": False},
153
+ "resources": {"subscribe": False, "listChanged": False},
154
+ },
155
+ "serverInfo": {
156
+ "name": self.name,
157
+ "version": self.version,
158
+ },
159
+ }
160
+
161
+ def _handle_initialized(self, params: dict):
162
+ """Handle initialized notification."""
163
+ pass
164
+
165
+ def _handle_ping(self, params: dict) -> dict:
166
+ return {}
167
+
168
+ def _handle_tools_list(self, params: dict) -> dict:
169
+ """Return list of registered tools."""
170
+ tools = []
171
+ for t in self._tools.values():
172
+ tools.append({
173
+ "name": t["name"],
174
+ "description": t["description"],
175
+ "inputSchema": t["inputSchema"],
176
+ })
177
+ return {"tools": tools}
178
+
179
+ def _handle_tools_call(self, params: dict) -> dict:
180
+ """Invoke a tool by name."""
181
+ tool_name = params.get("name")
182
+ if not tool_name:
183
+ raise ValueError("Missing tool name")
184
+
185
+ tool = self._tools.get(tool_name)
186
+ if not tool:
187
+ raise ValueError(f"Unknown tool: {tool_name}")
188
+
189
+ arguments = params.get("arguments", {})
190
+ handler = tool["handler"]
191
+
192
+ # Call the handler with the provided arguments
193
+ result = handler(**arguments)
194
+
195
+ # Format result as MCP content
196
+ if isinstance(result, str):
197
+ content = [{"type": "text", "text": result}]
198
+ elif isinstance(result, dict) or isinstance(result, list):
199
+ content = [{"type": "text", "text": json.dumps(result, default=str, indent=2)}]
200
+ else:
201
+ content = [{"type": "text", "text": str(result)}]
202
+
203
+ return {"content": content}
204
+
205
+ def _handle_resources_list(self, params: dict) -> dict:
206
+ """Return list of registered resources."""
207
+ resources = []
208
+ for r in self._resources.values():
209
+ resources.append({
210
+ "uri": r["uri"],
211
+ "name": r["name"],
212
+ "description": r["description"],
213
+ "mimeType": r["mimeType"],
214
+ })
215
+ return {"resources": resources}
216
+
217
+ def _handle_resources_read(self, params: dict) -> dict:
218
+ """Read a resource by URI."""
219
+ uri = params.get("uri")
220
+ if not uri:
221
+ raise ValueError("Missing resource URI")
222
+
223
+ resource = self._resources.get(uri)
224
+ if not resource:
225
+ raise ValueError(f"Unknown resource: {uri}")
226
+
227
+ result = resource["handler"]()
228
+
229
+ if isinstance(result, str):
230
+ text = result
231
+ elif isinstance(result, (dict, list)):
232
+ text = json.dumps(result, default=str, indent=2)
233
+ else:
234
+ text = str(result)
235
+
236
+ return {
237
+ "contents": [{
238
+ "uri": uri,
239
+ "mimeType": resource["mimeType"],
240
+ "text": text,
241
+ }]
242
+ }
243
+
244
+ def register_routes(self, router_module):
245
+ """Register HTTP routes for this MCP server on the Tina4 router.
246
+
247
+ Registers:
248
+ POST {path}/message — JSON-RPC message endpoint
249
+ GET {path}/sse — SSE endpoint for streaming
250
+ """
251
+ server = self
252
+ msg_path = f"{self.path}/message"
253
+ sse_path = f"{self.path}/sse"
254
+
255
+ @router_module.post(msg_path)
256
+ @router_module.noauth()
257
+ async def mcp_message(request, response):
258
+ body = request.body
259
+ if isinstance(body, dict):
260
+ raw = body
261
+ else:
262
+ raw = body if isinstance(body, str) else str(body)
263
+ result = server.handle_message(raw)
264
+ if not result:
265
+ return response("", 204)
266
+ return response(json.loads(result))
267
+
268
+ @router_module.get(sse_path)
269
+ @router_module.noauth()
270
+ async def mcp_sse(request, response):
271
+ # SSE endpoint — send initial endpoint message
272
+ endpoint_url = f"{request.url.rsplit('/sse', 1)[0]}/message"
273
+ sse_data = f"event: endpoint\ndata: {endpoint_url}\n\n"
274
+ from tina4_python.core.response import Response as Resp
275
+ r = Resp()
276
+ r.status_code = 200
277
+ r.content_type = "text/event-stream"
278
+ r.content = sse_data.encode()
279
+ r._headers = [
280
+ (b"content-type", b"text/event-stream"),
281
+ (b"cache-control", b"no-cache"),
282
+ (b"connection", b"keep-alive"),
283
+ ]
284
+ return r
285
+
286
+ def write_claude_config(self, port: int = 7145):
287
+ """Write/update .claude/settings.json with this MCP server config."""
288
+ config_dir = Path(".claude")
289
+ config_dir.mkdir(exist_ok=True)
290
+ config_file = config_dir / "settings.json"
291
+
292
+ config = {}
293
+ if config_file.exists():
294
+ try:
295
+ config = json.loads(config_file.read_text())
296
+ except (json.JSONDecodeError, OSError):
297
+ pass
298
+
299
+ if "mcpServers" not in config:
300
+ config["mcpServers"] = {}
301
+
302
+ server_key = self.name.lower().replace(" ", "-")
303
+ config["mcpServers"][server_key] = {
304
+ "url": f"http://localhost:{port}{self.path}/sse"
305
+ }
306
+
307
+ config_file.write_text(json.dumps(config, indent=2) + "\n")
308
+
309
+
310
+ # ── Decorator API ──────────────────────────────────────────────
311
+
312
+ # Default server instance — tools/resources registered via decorators
313
+ # attach to this instance. Developers can create their own McpServer.
314
+ _default_server: McpServer | None = None
315
+
316
+
317
+ def _get_default_server() -> McpServer:
318
+ global _default_server
319
+ if _default_server is None:
320
+ _default_server = McpServer("/__dev/mcp", name="Tina4 Dev Tools")
321
+ return _default_server
322
+
323
+
324
+ def mcp_tool(name: str = "", description: str = "", server: McpServer | None = None):
325
+ """Decorator to register a function or method as an MCP tool.
326
+
327
+ Usage:
328
+ @mcp_tool("lookup_invoice", description="Find invoice by number")
329
+ def lookup_invoice(invoice_no: str):
330
+ return db.fetch_one("SELECT * FROM invoices WHERE invoice_no = ?", [invoice_no])
331
+
332
+ # On a class method
333
+ class Service:
334
+ @mcp_tool("get_report")
335
+ def report(self, month: str):
336
+ return generate_report(month)
337
+ """
338
+ def decorator(func):
339
+ tool_name = name or func.__name__
340
+ tool_desc = description or (func.__doc__ or "").strip()
341
+ target = server or _get_default_server()
342
+ target.register_tool(tool_name, func, tool_desc)
343
+ func._mcp_tool_name = tool_name
344
+ return func
345
+ return decorator
346
+
347
+
348
+ def mcp_resource(uri: str, description: str = "", mime_type: str = "application/json", server: McpServer | None = None):
349
+ """Decorator to register a function as an MCP resource.
350
+
351
+ Usage:
352
+ @mcp_resource("app://tables", description="Database tables")
353
+ def list_tables():
354
+ return db.get_tables()
355
+ """
356
+ def decorator(func):
357
+ target = server or _get_default_server()
358
+ target.register_resource(uri, func, description, mime_type)
359
+ func._mcp_resource_uri = uri
360
+ return func
361
+ return decorator
@@ -0,0 +1,75 @@
1
+ # JSON-RPC 2.0 codec for MCP protocol.
2
+ """
3
+ Encode/decode JSON-RPC 2.0 messages used by the Model Context Protocol.
4
+ Zero dependencies — stdlib json only.
5
+ """
6
+ import json
7
+
8
+ # Standard JSON-RPC 2.0 error codes
9
+ PARSE_ERROR = -32700
10
+ INVALID_REQUEST = -32600
11
+ METHOD_NOT_FOUND = -32601
12
+ INVALID_PARAMS = -32602
13
+ INTERNAL_ERROR = -32603
14
+
15
+
16
+ def encode_response(request_id, result):
17
+ """Encode a successful JSON-RPC 2.0 response."""
18
+ return json.dumps({
19
+ "jsonrpc": "2.0",
20
+ "id": request_id,
21
+ "result": result,
22
+ }, default=str, separators=(",", ":"))
23
+
24
+
25
+ def encode_error(request_id, code: int, message: str, data=None):
26
+ """Encode a JSON-RPC 2.0 error response."""
27
+ error = {"code": code, "message": message}
28
+ if data is not None:
29
+ error["data"] = data
30
+ return json.dumps({
31
+ "jsonrpc": "2.0",
32
+ "id": request_id,
33
+ "error": error,
34
+ }, default=str, separators=(",", ":"))
35
+
36
+
37
+ def encode_notification(method: str, params=None):
38
+ """Encode a JSON-RPC 2.0 notification (no id)."""
39
+ msg = {"jsonrpc": "2.0", "method": method}
40
+ if params is not None:
41
+ msg["params"] = params
42
+ return json.dumps(msg, default=str, separators=(",", ":"))
43
+
44
+
45
+ def decode_request(data: str | bytes | dict) -> tuple:
46
+ """Decode a JSON-RPC 2.0 request.
47
+
48
+ Returns:
49
+ (method, params, request_id) — request_id is None for notifications.
50
+
51
+ Raises:
52
+ ValueError: If the message is malformed.
53
+ """
54
+ if isinstance(data, (str, bytes)):
55
+ try:
56
+ msg = json.loads(data)
57
+ except json.JSONDecodeError as e:
58
+ raise ValueError(f"Invalid JSON: {e}") from e
59
+ else:
60
+ msg = data
61
+
62
+ if not isinstance(msg, dict):
63
+ raise ValueError("Message must be a JSON object")
64
+
65
+ if msg.get("jsonrpc") != "2.0":
66
+ raise ValueError("Missing or invalid jsonrpc version")
67
+
68
+ method = msg.get("method")
69
+ if not method or not isinstance(method, str):
70
+ raise ValueError("Missing or invalid method")
71
+
72
+ params = msg.get("params", {})
73
+ request_id = msg.get("id") # None for notifications
74
+
75
+ return method, params, request_id