hyperforge 1.0.0.post437__tar.gz → 1.0.0.post447__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 (148) hide show
  1. {hyperforge-1.0.0.post437 → hyperforge-1.0.0.post447}/PKG-INFO +26 -1
  2. {hyperforge-1.0.0.post437 → hyperforge-1.0.0.post447}/README.md +24 -0
  3. {hyperforge-1.0.0.post437 → hyperforge-1.0.0.post447}/pyproject.toml +2 -1
  4. {hyperforge-1.0.0.post437 → hyperforge-1.0.0.post447}/pyproject.toml.orig +2 -1
  5. {hyperforge-1.0.0.post437 → hyperforge-1.0.0.post447}/src/hyperforge/api/app.py +4 -0
  6. {hyperforge-1.0.0.post437 → hyperforge-1.0.0.post447}/src/hyperforge/api/mcp_server_pool.py +91 -55
  7. {hyperforge-1.0.0.post437 → hyperforge-1.0.0.post447}/src/hyperforge/api/settings.py +1 -0
  8. {hyperforge-1.0.0.post437 → hyperforge-1.0.0.post447}/src/hyperforge/api/v1/mcp_interaction.py +29 -28
  9. {hyperforge-1.0.0.post437 → hyperforge-1.0.0.post447}/src/hyperforge/standalone/app.py +4 -0
  10. {hyperforge-1.0.0.post437 → hyperforge-1.0.0.post447}/src/hyperforge/standalone/settings.py +1 -0
  11. {hyperforge-1.0.0.post437 → hyperforge-1.0.0.post447}/VERSION +0 -0
  12. {hyperforge-1.0.0.post437 → hyperforge-1.0.0.post447}/src/hyperforge/__init__.py +0 -0
  13. {hyperforge-1.0.0.post437 → hyperforge-1.0.0.post447}/src/hyperforge/a2a/__init__.py +0 -0
  14. {hyperforge-1.0.0.post437 → hyperforge-1.0.0.post447}/src/hyperforge/a2a/auth.py +0 -0
  15. {hyperforge-1.0.0.post437 → hyperforge-1.0.0.post447}/src/hyperforge/a2a/card.py +0 -0
  16. {hyperforge-1.0.0.post437 → hyperforge-1.0.0.post447}/src/hyperforge/a2a/context.py +0 -0
  17. {hyperforge-1.0.0.post437 → hyperforge-1.0.0.post447}/src/hyperforge/a2a/executor.py +0 -0
  18. {hyperforge-1.0.0.post437 → hyperforge-1.0.0.post447}/src/hyperforge/a2a/run.py +0 -0
  19. {hyperforge-1.0.0.post437 → hyperforge-1.0.0.post447}/src/hyperforge/a2a/server.py +0 -0
  20. {hyperforge-1.0.0.post437 → hyperforge-1.0.0.post447}/src/hyperforge/a2a/settings.py +0 -0
  21. {hyperforge-1.0.0.post437 → hyperforge-1.0.0.post447}/src/hyperforge/a2a/task_store.py +0 -0
  22. {hyperforge-1.0.0.post437 → hyperforge-1.0.0.post447}/src/hyperforge/agent.py +0 -0
  23. {hyperforge-1.0.0.post437 → hyperforge-1.0.0.post447}/src/hyperforge/alembic/README +0 -0
  24. {hyperforge-1.0.0.post437 → hyperforge-1.0.0.post447}/src/hyperforge/alembic/env.py +0 -0
  25. {hyperforge-1.0.0.post437 → hyperforge-1.0.0.post447}/src/hyperforge/alembic/script.py.mako +0 -0
  26. {hyperforge-1.0.0.post437 → hyperforge-1.0.0.post447}/src/hyperforge/alembic/versions/049a4aed19e1_create_generation_table.py +0 -0
  27. {hyperforge-1.0.0.post437 → hyperforge-1.0.0.post447}/src/hyperforge/alembic/versions/1416cb41bd49_add_downloads_table.py +0 -0
  28. {hyperforge-1.0.0.post437 → hyperforge-1.0.0.post447}/src/hyperforge/alembic/versions/2847934e2d59_unique_driver.py +0 -0
  29. {hyperforge-1.0.0.post437 → hyperforge-1.0.0.post447}/src/hyperforge/alembic/versions/4d89a36c8bda_memory.py +0 -0
  30. {hyperforge-1.0.0.post437 → hyperforge-1.0.0.post447}/src/hyperforge/alembic/versions/6a23d8ef74c1_add_sync_oauth_credentials.py +0 -0
  31. {hyperforge-1.0.0.post437 → hyperforge-1.0.0.post447}/src/hyperforge/alembic/versions/72b997edb05d_identifier_for_drivers.py +0 -0
  32. {hyperforge-1.0.0.post437 → hyperforge-1.0.0.post447}/src/hyperforge/alembic/versions/8c7d2e4f91ab_structure_legacy_llm_fields.py +0 -0
  33. {hyperforge-1.0.0.post437 → hyperforge-1.0.0.post447}/src/hyperforge/alembic/versions/9c6f6a1b4e7f_workflows_default_fk.py +0 -0
  34. {hyperforge-1.0.0.post437 → hyperforge-1.0.0.post447}/src/hyperforge/alembic/versions/b8edb72295e0_.py +0 -0
  35. {hyperforge-1.0.0.post437 → hyperforge-1.0.0.post447}/src/hyperforge/alembic/versions/f3d2c1b0a9e8_soft_delete_workflows.py +0 -0
  36. {hyperforge-1.0.0.post437 → hyperforge-1.0.0.post447}/src/hyperforge/alembic.ini +0 -0
  37. {hyperforge-1.0.0.post437 → hyperforge-1.0.0.post447}/src/hyperforge/api/__init__.py +0 -0
  38. {hyperforge-1.0.0.post437 → hyperforge-1.0.0.post447}/src/hyperforge/api/authentication.py +0 -0
  39. {hyperforge-1.0.0.post437 → hyperforge-1.0.0.post447}/src/hyperforge/api/commands.py +0 -0
  40. {hyperforge-1.0.0.post437 → hyperforge-1.0.0.post447}/src/hyperforge/api/logging.py +0 -0
  41. {hyperforge-1.0.0.post437 → hyperforge-1.0.0.post447}/src/hyperforge/api/models.py +0 -0
  42. {hyperforge-1.0.0.post437 → hyperforge-1.0.0.post447}/src/hyperforge/api/session.py +0 -0
  43. {hyperforge-1.0.0.post437 → hyperforge-1.0.0.post447}/src/hyperforge/api/utils.py +0 -0
  44. {hyperforge-1.0.0.post437 → hyperforge-1.0.0.post447}/src/hyperforge/api/v1/__init__.py +0 -0
  45. {hyperforge-1.0.0.post437 → hyperforge-1.0.0.post447}/src/hyperforge/api/v1/agents.py +0 -0
  46. {hyperforge-1.0.0.post437 → hyperforge-1.0.0.post447}/src/hyperforge/api/v1/interaction.py +0 -0
  47. {hyperforge-1.0.0.post437 → hyperforge-1.0.0.post447}/src/hyperforge/api/v1/mcp_content.py +0 -0
  48. {hyperforge-1.0.0.post437 → hyperforge-1.0.0.post447}/src/hyperforge/api/v1/oauth.py +0 -0
  49. {hyperforge-1.0.0.post437 → hyperforge-1.0.0.post447}/src/hyperforge/api/v1/prompt.py +0 -0
  50. {hyperforge-1.0.0.post437 → hyperforge-1.0.0.post447}/src/hyperforge/api/v1/router.py +0 -0
  51. {hyperforge-1.0.0.post437 → hyperforge-1.0.0.post447}/src/hyperforge/api/v1/schema.py +0 -0
  52. {hyperforge-1.0.0.post437 → hyperforge-1.0.0.post447}/src/hyperforge/api/v1/session.py +0 -0
  53. {hyperforge-1.0.0.post437 → hyperforge-1.0.0.post447}/src/hyperforge/api/v1/utils.py +0 -0
  54. {hyperforge-1.0.0.post437 → hyperforge-1.0.0.post447}/src/hyperforge/api/v1/workflows.py +0 -0
  55. {hyperforge-1.0.0.post437 → hyperforge-1.0.0.post447}/src/hyperforge/arag.py +0 -0
  56. {hyperforge-1.0.0.post437 → hyperforge-1.0.0.post447}/src/hyperforge/broker/__init__.py +0 -0
  57. {hyperforge-1.0.0.post437 → hyperforge-1.0.0.post447}/src/hyperforge/broker/local.py +0 -0
  58. {hyperforge-1.0.0.post437 → hyperforge-1.0.0.post447}/src/hyperforge/broker/redis.py +0 -0
  59. {hyperforge-1.0.0.post437 → hyperforge-1.0.0.post447}/src/hyperforge/codemode/__init__.py +0 -0
  60. {hyperforge-1.0.0.post437 → hyperforge-1.0.0.post447}/src/hyperforge/codemode/model.py +0 -0
  61. {hyperforge-1.0.0.post437 → hyperforge-1.0.0.post447}/src/hyperforge/codemode/sandbox.py +0 -0
  62. {hyperforge-1.0.0.post437 → hyperforge-1.0.0.post447}/src/hyperforge/codemode/worker.py +0 -0
  63. {hyperforge-1.0.0.post437 → hyperforge-1.0.0.post447}/src/hyperforge/completions.py +0 -0
  64. {hyperforge-1.0.0.post437 → hyperforge-1.0.0.post447}/src/hyperforge/configure.py +0 -0
  65. {hyperforge-1.0.0.post437 → hyperforge-1.0.0.post447}/src/hyperforge/context/__init__.py +0 -0
  66. {hyperforge-1.0.0.post437 → hyperforge-1.0.0.post447}/src/hyperforge/context/agent.py +0 -0
  67. {hyperforge-1.0.0.post437 → hyperforge-1.0.0.post447}/src/hyperforge/context/config.py +0 -0
  68. {hyperforge-1.0.0.post437 → hyperforge-1.0.0.post447}/src/hyperforge/database.py +0 -0
  69. {hyperforge-1.0.0.post437 → hyperforge-1.0.0.post447}/src/hyperforge/db/__init__.py +0 -0
  70. {hyperforge-1.0.0.post437 → hyperforge-1.0.0.post447}/src/hyperforge/db/agents.py +0 -0
  71. {hyperforge-1.0.0.post437 → hyperforge-1.0.0.post447}/src/hyperforge/db/encryption.py +0 -0
  72. {hyperforge-1.0.0.post437 → hyperforge-1.0.0.post447}/src/hyperforge/db/exceptions.py +0 -0
  73. {hyperforge-1.0.0.post437 → hyperforge-1.0.0.post447}/src/hyperforge/db/legacy_llm_migration.py +0 -0
  74. {hyperforge-1.0.0.post437 → hyperforge-1.0.0.post447}/src/hyperforge/db/llm_migration_utils.py +0 -0
  75. {hyperforge-1.0.0.post437 → hyperforge-1.0.0.post447}/src/hyperforge/db/settings.py +0 -0
  76. {hyperforge-1.0.0.post437 → hyperforge-1.0.0.post447}/src/hyperforge/db/workflow_cleanup.py +0 -0
  77. {hyperforge-1.0.0.post437 → hyperforge-1.0.0.post447}/src/hyperforge/definition.py +0 -0
  78. {hyperforge-1.0.0.post437 → hyperforge-1.0.0.post447}/src/hyperforge/driver.py +0 -0
  79. {hyperforge-1.0.0.post437 → hyperforge-1.0.0.post447}/src/hyperforge/dummy.py +0 -0
  80. {hyperforge-1.0.0.post437 → hyperforge-1.0.0.post447}/src/hyperforge/engine.py +0 -0
  81. {hyperforge-1.0.0.post437 → hyperforge-1.0.0.post447}/src/hyperforge/errors.py +0 -0
  82. {hyperforge-1.0.0.post437 → hyperforge-1.0.0.post447}/src/hyperforge/exceptions.py +0 -0
  83. {hyperforge-1.0.0.post437 → hyperforge-1.0.0.post447}/src/hyperforge/feature_flag.py +0 -0
  84. {hyperforge-1.0.0.post437 → hyperforge-1.0.0.post447}/src/hyperforge/fixtures.py +0 -0
  85. {hyperforge-1.0.0.post437 → hyperforge-1.0.0.post447}/src/hyperforge/harness/__init__.py +0 -0
  86. {hyperforge-1.0.0.post437 → hyperforge-1.0.0.post447}/src/hyperforge/harness/agent.py +0 -0
  87. {hyperforge-1.0.0.post437 → hyperforge-1.0.0.post447}/src/hyperforge/harness/config.py +0 -0
  88. {hyperforge-1.0.0.post437 → hyperforge-1.0.0.post447}/src/hyperforge/harness_sdk/__init__.py +0 -0
  89. {hyperforge-1.0.0.post437 → hyperforge-1.0.0.post447}/src/hyperforge/harness_sdk/agents.py +0 -0
  90. {hyperforge-1.0.0.post437 → hyperforge-1.0.0.post447}/src/hyperforge/harness_sdk/clients.py +0 -0
  91. {hyperforge-1.0.0.post437 → hyperforge-1.0.0.post447}/src/hyperforge/harness_sdk/context.py +0 -0
  92. {hyperforge-1.0.0.post437 → hyperforge-1.0.0.post447}/src/hyperforge/harness_sdk/harness.py +0 -0
  93. {hyperforge-1.0.0.post437 → hyperforge-1.0.0.post447}/src/hyperforge/harness_sdk/models.py +0 -0
  94. {hyperforge-1.0.0.post437 → hyperforge-1.0.0.post447}/src/hyperforge/harness_sdk/schema.py +0 -0
  95. {hyperforge-1.0.0.post437 → hyperforge-1.0.0.post447}/src/hyperforge/harness_sdk/storage.py +0 -0
  96. {hyperforge-1.0.0.post437 → hyperforge-1.0.0.post447}/src/hyperforge/harness_sdk/tools/__init__.py +0 -0
  97. {hyperforge-1.0.0.post437 → hyperforge-1.0.0.post447}/src/hyperforge/harness_sdk/tools/codemode.py +0 -0
  98. {hyperforge-1.0.0.post437 → hyperforge-1.0.0.post447}/src/hyperforge/harness_sdk/tools/core.py +0 -0
  99. {hyperforge-1.0.0.post437 → hyperforge-1.0.0.post447}/src/hyperforge/harness_sdk/tools/utils.py +0 -0
  100. {hyperforge-1.0.0.post437 → hyperforge-1.0.0.post447}/src/hyperforge/harness_sdk/usage.py +0 -0
  101. {hyperforge-1.0.0.post437 → hyperforge-1.0.0.post447}/src/hyperforge/interaction.py +0 -0
  102. {hyperforge-1.0.0.post437 → hyperforge-1.0.0.post447}/src/hyperforge/json_schema.py +0 -0
  103. {hyperforge-1.0.0.post437 → hyperforge-1.0.0.post447}/src/hyperforge/llm.py +0 -0
  104. {hyperforge-1.0.0.post437 → hyperforge-1.0.0.post447}/src/hyperforge/llm_config.py +0 -0
  105. {hyperforge-1.0.0.post437 → hyperforge-1.0.0.post447}/src/hyperforge/manager.py +0 -0
  106. {hyperforge-1.0.0.post437 → hyperforge-1.0.0.post447}/src/hyperforge/memory/__init__.py +0 -0
  107. {hyperforge-1.0.0.post437 → hyperforge-1.0.0.post447}/src/hyperforge/memory/memory.py +0 -0
  108. {hyperforge-1.0.0.post437 → hyperforge-1.0.0.post447}/src/hyperforge/minimal_fixtures.py +0 -0
  109. {hyperforge-1.0.0.post437 → hyperforge-1.0.0.post447}/src/hyperforge/models.py +0 -0
  110. {hyperforge-1.0.0.post437 → hyperforge-1.0.0.post447}/src/hyperforge/openapi.py +0 -0
  111. {hyperforge-1.0.0.post437 → hyperforge-1.0.0.post447}/src/hyperforge/procedural/__init__.py +0 -0
  112. {hyperforge-1.0.0.post437 → hyperforge-1.0.0.post447}/src/hyperforge/procedural/adapters.py +0 -0
  113. {hyperforge-1.0.0.post437 → hyperforge-1.0.0.post447}/src/hyperforge/procedural/cli.py +0 -0
  114. {hyperforge-1.0.0.post437 → hyperforge-1.0.0.post447}/src/hyperforge/procedural/evolution.py +0 -0
  115. {hyperforge-1.0.0.post437 → hyperforge-1.0.0.post447}/src/hyperforge/procedural/graph.py +0 -0
  116. {hyperforge-1.0.0.post437 → hyperforge-1.0.0.post447}/src/hyperforge/procedural/guidance.py +0 -0
  117. {hyperforge-1.0.0.post437 → hyperforge-1.0.0.post447}/src/hyperforge/prompts.py +0 -0
  118. {hyperforge-1.0.0.post437 → hyperforge-1.0.0.post447}/src/hyperforge/pubsub.py +0 -0
  119. {hyperforge-1.0.0.post437 → hyperforge-1.0.0.post447}/src/hyperforge/py.typed +0 -0
  120. {hyperforge-1.0.0.post437 → hyperforge-1.0.0.post447}/src/hyperforge/redis_utils.py +0 -0
  121. {hyperforge-1.0.0.post437 → hyperforge-1.0.0.post447}/src/hyperforge/result_payload.py +0 -0
  122. {hyperforge-1.0.0.post437 → hyperforge-1.0.0.post447}/src/hyperforge/retrieval/__init__.py +0 -0
  123. {hyperforge-1.0.0.post437 → hyperforge-1.0.0.post447}/src/hyperforge/retrieval/agent.py +0 -0
  124. {hyperforge-1.0.0.post437 → hyperforge-1.0.0.post447}/src/hyperforge/retrieval/config.py +0 -0
  125. {hyperforge-1.0.0.post437 → hyperforge-1.0.0.post447}/src/hyperforge/server/__init__.py +0 -0
  126. {hyperforge-1.0.0.post437 → hyperforge-1.0.0.post447}/src/hyperforge/server/cache.py +0 -0
  127. {hyperforge-1.0.0.post437 → hyperforge-1.0.0.post447}/src/hyperforge/server/run.py +0 -0
  128. {hyperforge-1.0.0.post437 → hyperforge-1.0.0.post447}/src/hyperforge/server/sandbox.py +0 -0
  129. {hyperforge-1.0.0.post437 → hyperforge-1.0.0.post447}/src/hyperforge/server/session.py +0 -0
  130. {hyperforge-1.0.0.post437 → hyperforge-1.0.0.post447}/src/hyperforge/server/settings.py +0 -0
  131. {hyperforge-1.0.0.post437 → hyperforge-1.0.0.post447}/src/hyperforge/server/utils.py +0 -0
  132. {hyperforge-1.0.0.post437 → hyperforge-1.0.0.post447}/src/hyperforge/server/web.py +0 -0
  133. {hyperforge-1.0.0.post437 → hyperforge-1.0.0.post447}/src/hyperforge/settings.py +0 -0
  134. {hyperforge-1.0.0.post437 → hyperforge-1.0.0.post447}/src/hyperforge/standalone/__init__.py +0 -0
  135. {hyperforge-1.0.0.post437 → hyperforge-1.0.0.post447}/src/hyperforge/standalone/agent.py +0 -0
  136. {hyperforge-1.0.0.post437 → hyperforge-1.0.0.post447}/src/hyperforge/standalone/config.py +0 -0
  137. {hyperforge-1.0.0.post437 → hyperforge-1.0.0.post447}/src/hyperforge/standalone/const.py +0 -0
  138. {hyperforge-1.0.0.post437 → hyperforge-1.0.0.post447}/src/hyperforge/standalone/oauth.py +0 -0
  139. {hyperforge-1.0.0.post437 → hyperforge-1.0.0.post447}/src/hyperforge/standalone/run.py +0 -0
  140. {hyperforge-1.0.0.post437 → hyperforge-1.0.0.post447}/src/hyperforge/standalone/ui_router.py +0 -0
  141. {hyperforge-1.0.0.post437 → hyperforge-1.0.0.post447}/src/hyperforge/static/assets/index-DLejS5jC.js +0 -0
  142. {hyperforge-1.0.0.post437 → hyperforge-1.0.0.post447}/src/hyperforge/static/assets/index-MoxkZ1_3.css +0 -0
  143. {hyperforge-1.0.0.post437 → hyperforge-1.0.0.post447}/src/hyperforge/static/favicon.svg +0 -0
  144. {hyperforge-1.0.0.post437 → hyperforge-1.0.0.post447}/src/hyperforge/static/index.html +0 -0
  145. {hyperforge-1.0.0.post437 → hyperforge-1.0.0.post447}/src/hyperforge/trace.py +0 -0
  146. {hyperforge-1.0.0.post437 → hyperforge-1.0.0.post447}/src/hyperforge/utils/__init__.py +0 -0
  147. {hyperforge-1.0.0.post437 → hyperforge-1.0.0.post447}/src/hyperforge/utils/http.py +0 -0
  148. {hyperforge-1.0.0.post437 → hyperforge-1.0.0.post447}/src/hyperforge/workflows.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: hyperforge
3
- Version: 1.0.0.post437
3
+ Version: 1.0.0.post447
4
4
  Summary: Agentic Framework for Orchestrated Runtime, Governance, and Execution
5
5
  Author: AI Data Team
6
6
  Author-email: AI Data Team <learning@nuclia.com>
@@ -23,6 +23,7 @@ Requires-Dist: aiodns
23
23
  Requires-Dist: anyio>=4.15.1
24
24
  Requires-Dist: httpx>=0.28.1,<0.29
25
25
  Requires-Dist: httpcore>=1.0.9,<1.1
26
+ Requires-Dist: urllib3>=2.8.0,<3
26
27
  Requires-Dist: jinja2
27
28
  Requires-Dist: jsonschema
28
29
  Requires-Dist: restrictedpython>=8.5
@@ -132,6 +133,30 @@ All settings are provided through environment variables consumed by Pydantic `Ba
132
133
  | `LOAD_MODULES` | Comma-separated list of agent package modules to load at startup |
133
134
  | `NUCLIA_ZONE` | Nuclia zone (default `arag`) |
134
135
  | `NUCLIA_PUBLIC_URL` | Public Nuclia URL template (default `https://{zone}.nuclia.com`) |
136
+ | `MCP_MAX_SERVERS` | Maximum cached MCP managers (default `100`) |
137
+ | `MCP_MAX_SESSIONS_PER_SERVER` | Maximum concurrent connections per MCP manager (default `100`) |
138
+ | `MCP_SESSION_IDLE_TTL_SECONDS` | Idle lifetime of each MCP connection and cached manager (default `1800`) |
139
+
140
+ ### MCP connections
141
+
142
+ API and standalone applications expose their configured MCP limits through the
143
+ typed `mcp_settings` property, without falling back to session-server settings.
144
+
145
+ Multiple clients can use the same `/api/v1/agent/{agent_id}/session/{session}/mcp`
146
+ URL, including `session/ephemeral`. Each initialization receives its own
147
+ `Mcp-Session-Id`; subsequent requests and DELETE must include that ID. DELETE
148
+ only terminates the requested connection. Missing IDs on GET or DELETE return 400;
149
+ unknown or expired IDs return 404. Clients should initialize a new connection
150
+ after a 404, not reuse the expired ID.
151
+
152
+ Managers whose last connection has closed can be evicted immediately when the
153
+ pool is full.
154
+
155
+ MCP transports remain process-local. Multi-replica deployments must route an
156
+ authenticated user's initialization and subsequent requests to the same API
157
+ replica using a stable, trusted identity header. Hashing only `Mcp-Session-Id`
158
+ does not work because initialization has no session ID. Pod restarts or changes
159
+ to the replica set can still require reinitialization.
135
160
 
136
161
  ### Standalone A2A
137
162
 
@@ -82,6 +82,30 @@ All settings are provided through environment variables consumed by Pydantic `Ba
82
82
  | `LOAD_MODULES` | Comma-separated list of agent package modules to load at startup |
83
83
  | `NUCLIA_ZONE` | Nuclia zone (default `arag`) |
84
84
  | `NUCLIA_PUBLIC_URL` | Public Nuclia URL template (default `https://{zone}.nuclia.com`) |
85
+ | `MCP_MAX_SERVERS` | Maximum cached MCP managers (default `100`) |
86
+ | `MCP_MAX_SESSIONS_PER_SERVER` | Maximum concurrent connections per MCP manager (default `100`) |
87
+ | `MCP_SESSION_IDLE_TTL_SECONDS` | Idle lifetime of each MCP connection and cached manager (default `1800`) |
88
+
89
+ ### MCP connections
90
+
91
+ API and standalone applications expose their configured MCP limits through the
92
+ typed `mcp_settings` property, without falling back to session-server settings.
93
+
94
+ Multiple clients can use the same `/api/v1/agent/{agent_id}/session/{session}/mcp`
95
+ URL, including `session/ephemeral`. Each initialization receives its own
96
+ `Mcp-Session-Id`; subsequent requests and DELETE must include that ID. DELETE
97
+ only terminates the requested connection. Missing IDs on GET or DELETE return 400;
98
+ unknown or expired IDs return 404. Clients should initialize a new connection
99
+ after a 404, not reuse the expired ID.
100
+
101
+ Managers whose last connection has closed can be evicted immediately when the
102
+ pool is full.
103
+
104
+ MCP transports remain process-local. Multi-replica deployments must route an
105
+ authenticated user's initialization and subsequent requests to the same API
106
+ replica using a stable, trusted identity header. Hashing only `Mcp-Session-Id`
107
+ does not work because initialization has no session ID. Pod restarts or changes
108
+ to the replica set can still require reinitialization.
85
109
 
86
110
  ### Standalone A2A
87
111
 
@@ -4,7 +4,7 @@ build-backend = "uv_build"
4
4
 
5
5
  [project]
6
6
  name = "hyperforge"
7
- version = "1.0.0.post437"
7
+ version = "1.0.0.post447"
8
8
  description = "Agentic Framework for Orchestrated Runtime, Governance, and Execution"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.12"
@@ -30,6 +30,7 @@ dependencies = [
30
30
  "anyio>=4.15.1",
31
31
  "httpx>=0.28.1,<0.29",
32
32
  "httpcore>=1.0.9,<1.1",
33
+ "urllib3>=2.8.0,<3",
33
34
  "jinja2",
34
35
  "jsonschema",
35
36
  "RestrictedPython>=8.5",
@@ -4,7 +4,7 @@ build-backend = "uv_build"
4
4
 
5
5
  [project]
6
6
  name = "hyperforge"
7
- version = "1.0.0.post437"
7
+ version = "1.0.0.post447"
8
8
  authors = [{ name = "AI Data Team", email = "learning@nuclia.com" }]
9
9
  description = "Agentic Framework for Orchestrated Runtime, Governance, and Execution"
10
10
  readme = "README.md"
@@ -31,6 +31,7 @@ dependencies = [
31
31
  "anyio>=4.15.1",
32
32
  "httpx>=0.28.1,<0.29",
33
33
  "httpcore>=1.0.9,<1.1",
34
+ "urllib3>=2.8.0,<3",
34
35
  "jinja2",
35
36
  "jsonschema",
36
37
  "RestrictedPython>=8.5",
@@ -83,6 +83,10 @@ class HTTPApplication(FastAPI):
83
83
  for extra_middleware in self.extra_middlewares:
84
84
  self.add_middleware(extra_middleware)
85
85
 
86
+ @property
87
+ def mcp_settings(self) -> Settings:
88
+ return self.settings
89
+
86
90
  async def startup(self) -> None:
87
91
  GLOBAL_REGISTRY.clear()
88
92
  await setup_telemetry(SERVICE_NAME)
@@ -1,5 +1,4 @@
1
1
  import asyncio
2
- import json
3
2
  import time
4
3
  from collections.abc import Awaitable, Callable
5
4
  from typing import Any
@@ -9,17 +8,25 @@ from fastapi import HTTPException
9
8
  from mcp.server.streamable_http import MCP_SESSION_ID_HEADER
10
9
  from mcp.server.streamable_http_manager import StreamableHTTPSessionManager
11
10
  from starlette.requests import Request
11
+ from starlette.responses import Response
12
+ from starlette.types import Message, Receive, Scope, Send
13
+
14
+ from hyperforge.api import logger
12
15
 
13
16
  MCPServerKey = tuple[str, str, str, str, str]
14
17
  MCPServerFactory = Callable[[], Awaitable["ManagedMCPServer"]]
15
18
 
16
19
 
17
20
  class ManagedMCPServer:
18
- def __init__(self, manager: StreamableHTTPSessionManager):
21
+ def __init__(self, manager: StreamableHTTPSessionManager, max_sessions: int = 100):
19
22
  self.manager = manager
20
23
  self.started = asyncio.Event()
21
24
  self.task: asyncio.Task[None] | None = None
22
- self.session_created = False
25
+ self.active_sessions = 0
26
+ self.max_sessions = max_sessions
27
+ self._session_creation_lock = anyio.Lock()
28
+ self._session_closed = anyio.Event()
29
+ self.has_been_leased = False
23
30
  self.active_requests = 0
24
31
  self.last_used = time.monotonic()
25
32
 
@@ -56,6 +63,7 @@ class ManagedMCPServer:
56
63
  await asyncio.gather(self.task, return_exceptions=True)
57
64
 
58
65
  def reserve_request(self) -> None:
66
+ self.has_been_leased = True
59
67
  self.active_requests += 1
60
68
  self.last_used = time.monotonic()
61
69
 
@@ -65,26 +73,42 @@ class ManagedMCPServer:
65
73
  self.active_requests -= 1
66
74
  self.last_used = time.monotonic()
67
75
 
68
- def accept_request(self, request: Request) -> bool:
69
- if request.headers.get(MCP_SESSION_ID_HEADER):
70
- return True
71
- if self.session_created:
72
- return False
73
- self.session_created = True
74
- return True
75
-
76
- def is_reinitialization(self, request: Request, body: bytes) -> bool:
77
- if (
78
- not self.session_created
79
- or request.method != "POST"
80
- or request.headers.get(MCP_SESSION_ID_HEADER)
81
- ):
82
- return False
83
- try:
84
- message = json.loads(body)
85
- except (json.JSONDecodeError, UnicodeDecodeError):
86
- return False
87
- return isinstance(message, dict) and message.get("method") == "initialize"
76
+ def release_session(self) -> None:
77
+ self.active_sessions -= 1
78
+ self._session_closed.set()
79
+ self._session_closed = anyio.Event()
80
+
81
+ async def handle_request(self, scope: Scope, receive: Receive, send: Send) -> None:
82
+ request = Request(scope)
83
+ if request.method == "POST" and not request.headers.get(MCP_SESSION_ID_HEADER):
84
+ async with self._session_creation_lock:
85
+ if self.active_sessions >= self.max_sessions:
86
+ response = Response(
87
+ "MCP session capacity is exhausted", status_code=503
88
+ )
89
+ await response(scope, receive, send)
90
+ return
91
+ await self.manager.handle_request(scope, receive, send)
92
+ elif request.method == "DELETE":
93
+ async with self._session_creation_lock:
94
+ session_closed = self._session_closed
95
+ status: int | None = None
96
+
97
+ async def observe_send(message: Message) -> None:
98
+ nonlocal status
99
+ if message["type"] == "http.response.start":
100
+ status = message["status"]
101
+ await send(message)
102
+
103
+ await self.manager.handle_request(scope, receive, observe_send)
104
+ if status == 200:
105
+ await session_closed.wait()
106
+ # SDK 1.x retains explicitly terminated transports.
107
+ session_id = request.headers["mcp-session-id"]
108
+ self.manager._server_instances.pop(session_id, None)
109
+ self.manager._session_owners.pop(session_id, None)
110
+ else:
111
+ await self.manager.handle_request(scope, receive, send)
88
112
 
89
113
 
90
114
  class MCPRequestLease:
@@ -109,7 +133,7 @@ class MCPServerPool:
109
133
  self._idle_ttl_seconds = idle_ttl_seconds
110
134
  self._startup_timeout_seconds = startup_timeout_seconds
111
135
  self._servers: dict[MCPServerKey, ManagedMCPServer] = {}
112
- self._creating: dict[MCPServerKey, asyncio.Task[Any]] = {}
136
+ self._creating: dict[MCPServerKey, asyncio.Task[ManagedMCPServer]] = {}
113
137
  self._retired_tasks: set[asyncio.Task[Any]] = set()
114
138
  self._lock = anyio.Lock()
115
139
  self._shutting_down = False
@@ -123,10 +147,6 @@ class MCPServerPool:
123
147
  body: bytes,
124
148
  factory: MCPServerFactory,
125
149
  ) -> MCPRequestLease:
126
- current_task = asyncio.current_task()
127
- if current_task is None:
128
- raise RuntimeError("MCP request task is unavailable")
129
-
130
150
  stale_server = None
131
151
  async with self._lock:
132
152
  self._ensure_running()
@@ -139,28 +159,38 @@ class MCPServerPool:
139
159
  and time.monotonic() - server.last_used >= self._idle_ttl_seconds
140
160
  ):
141
161
  stale_server = self._servers.pop(key)
142
- stale_server.close()
162
+ self._retire_server(stale_server)
143
163
  server = None
144
- if server is not None and server.is_reinitialization(request, body):
145
- if server.active_requests:
146
- raise HTTPException(
147
- status_code=409,
148
- detail="MCP session is still active for this path",
149
- )
150
- stale_server = self._servers.pop(key)
151
- stale_server.close()
152
- server = None
153
-
154
164
  if server is not None:
155
- return self._lease(server, request)
156
- if key in self._creating:
165
+ return self._lease(server)
166
+ if request.headers.get(MCP_SESSION_ID_HEADER):
167
+ raise HTTPException(status_code=404, detail="MCP session not found")
168
+ if request.method != "POST":
169
+ raise HTTPException(status_code=400, detail="Missing Mcp-Session-Id")
170
+ creating_task = self._creating.get(key)
171
+ if creating_task is None:
172
+ self._reserve_capacity()
173
+ creating_task = asyncio.create_task(
174
+ self._create_server(key, factory, stale_server)
175
+ )
176
+ self._creating[key] = creating_task
177
+ creating_task.add_done_callback(self._log_creation_failure)
178
+
179
+ created = await asyncio.shield(creating_task)
180
+ async with self._lock:
181
+ self._ensure_running()
182
+ if self._servers.get(key) is not created:
157
183
  raise HTTPException(
158
- status_code=409,
159
- detail="MCP session initialization is already in progress",
184
+ status_code=503, detail="MCP server initialization was interrupted"
160
185
  )
161
- self._reserve_capacity()
162
- self._creating[key] = current_task
186
+ return self._lease(created)
163
187
 
188
+ async def _create_server(
189
+ self,
190
+ key: MCPServerKey,
191
+ factory: MCPServerFactory,
192
+ stale_server: ManagedMCPServer | None,
193
+ ) -> ManagedMCPServer:
164
194
  candidate = None
165
195
  registered = False
166
196
  try:
@@ -172,19 +202,26 @@ class MCPServerPool:
172
202
  async with self._lock:
173
203
  self._ensure_running()
174
204
  self._evict_for_capacity()
175
- lease = self._lease(candidate, request)
176
205
  self._servers[key] = candidate
177
206
  self._creating.pop(key, None)
178
207
  registered = True
179
- return lease
208
+ return candidate
180
209
  except BaseException:
181
210
  if candidate is not None and not registered:
182
211
  await candidate.stop()
183
212
  async with self._lock:
184
- if self._creating.get(key) is current_task:
213
+ if self._creating.get(key) is asyncio.current_task():
185
214
  self._creating.pop(key, None)
186
215
  raise
187
216
 
217
+ @staticmethod
218
+ def _log_creation_failure(task: asyncio.Task[ManagedMCPServer]) -> None:
219
+ if not task.cancelled() and (error := task.exception()) is not None:
220
+ logger.error(
221
+ "MCP server initialization failed",
222
+ exc_info=(type(error), error, error.__traceback__),
223
+ )
224
+
188
225
  async def remove(self, key: MCPServerKey) -> None:
189
226
  async with self._lock:
190
227
  server = self._servers.pop(key, None)
@@ -257,12 +294,7 @@ class MCPServerPool:
257
294
  if self._shutting_down:
258
295
  raise HTTPException(status_code=503, detail="MCP service is shutting down")
259
296
 
260
- def _lease(self, server: ManagedMCPServer, request: Request) -> MCPRequestLease:
261
- if not server.accept_request(request):
262
- raise HTTPException(
263
- status_code=409,
264
- detail="MCP session already initialized for this path",
265
- )
297
+ def _lease(self, server: ManagedMCPServer) -> MCPRequestLease:
266
298
  server.reserve_request()
267
299
  return MCPRequestLease(server)
268
300
 
@@ -281,7 +313,7 @@ class MCPServerPool:
281
313
  for key, server in self._servers.items()
282
314
  if server.active_requests == 0
283
315
  and (
284
- not server.session_created
316
+ (server.has_been_leased and server.active_sessions == 0)
285
317
  or now - server.last_used >= self._idle_ttl_seconds
286
318
  )
287
319
  ]
@@ -292,6 +324,10 @@ class MCPServerPool:
292
324
  )
293
325
  key, server = min(candidates, key=lambda item: item[1].last_used)
294
326
  self._servers.pop(key)
327
+
328
+ self._retire_server(server)
329
+
330
+ def _retire_server(self, server: ManagedMCPServer) -> None:
295
331
  server.close()
296
332
  if server.task is not None:
297
333
  self._retired_tasks.add(server.task)
@@ -43,5 +43,6 @@ class Settings(BaseSettings):
43
43
  mcp_max_request_bytes: int = Field(default=1024 * 1024, ge=1)
44
44
  mcp_max_response_bytes: int = Field(default=4 * 1024 * 1024, ge=1)
45
45
  mcp_max_servers: int = Field(default=100, ge=1)
46
+ mcp_max_sessions_per_server: int = Field(default=100, ge=1)
46
47
  mcp_session_idle_ttl_seconds: int = Field(default=1800, ge=1)
47
48
  mcp_startup_timeout_seconds: int = Field(default=30, ge=1)
@@ -1,8 +1,9 @@
1
1
  import asyncio
2
2
  import json
3
3
  import uuid
4
+ from contextlib import asynccontextmanager
4
5
  from functools import partial
5
- from typing import TYPE_CHECKING, Any, Iterable, Sequence
6
+ from typing import TYPE_CHECKING, Any, AsyncIterator, Iterable, Sequence
6
7
 
7
8
  from fastapi import Header, HTTPException
8
9
  from mcp.server.fastmcp.exceptions import ResourceError
@@ -283,12 +284,10 @@ def _get_mcp_auth_config(app: "HTTPApplication", agent_id: str):
283
284
 
284
285
 
285
286
  def _get_first_enabled_mcp_auth_config(app: "HTTPApplication"):
286
- agents_cfg = getattr(app, "_agents_cfg", None)
287
- if isinstance(agents_cfg, dict):
288
- for agent_id in agents_cfg:
289
- auth_config = get_enabled_mcp_auth(agents_cfg, agent_id)
290
- if auth_config is not None:
291
- return auth_config
287
+ for agent_id in app._agents_cfg:
288
+ auth_config = get_enabled_mcp_auth(app._agents_cfg, agent_id)
289
+ if auth_config is not None:
290
+ return auth_config
292
291
  return None
293
292
 
294
293
 
@@ -300,6 +299,17 @@ async def _create_mcp_server(
300
299
  account: str,
301
300
  max_request_bytes: int,
302
301
  ) -> ManagedMCPServer:
302
+ runtime_settings = app.mcp_settings
303
+
304
+ @asynccontextmanager
305
+ async def mcp_lifespan(server: MCPServer) -> AsyncIterator[dict[str, Any]]:
306
+ managed_server.active_sessions += 1
307
+ try:
308
+ async with default_lifespan(server) as context:
309
+ yield context
310
+ finally:
311
+ managed_server.release_session()
312
+
303
313
  agent_manager: AgentManager = request.app.agent_manager
304
314
  workflows, agent_config, prompts = await asyncio.gather(
305
315
  agent_manager.workflows_list(account=account, agent_id=agent_id),
@@ -310,7 +320,7 @@ async def _create_mcp_server(
310
320
  name=agent_id,
311
321
  version="1.0.0",
312
322
  instructions=agent_config.instructions,
313
- lifespan=default_lifespan,
323
+ lifespan=mcp_lifespan,
314
324
  )
315
325
  mcp_server.list_tools()(partial(list_tools, workflows))
316
326
  mcp_server.call_tool()(
@@ -331,9 +341,12 @@ async def _create_mcp_server(
331
341
  json_response=True,
332
342
  stateless=False,
333
343
  security_settings=None,
344
+ session_idle_timeout=runtime_settings.mcp_session_idle_ttl_seconds,
334
345
  max_request_body_size=max_request_bytes,
335
346
  )
336
- managed_server = ManagedMCPServer(manager)
347
+ managed_server = ManagedMCPServer(
348
+ manager, max_sessions=runtime_settings.mcp_max_sessions_per_server
349
+ )
337
350
  await managed_server.start()
338
351
  return managed_server
339
352
 
@@ -374,9 +387,7 @@ class _MCPTransportResponse(Response):
374
387
  return await receive()
375
388
 
376
389
  if self.request.method == "GET":
377
- await self.managed_server.manager.handle_request(
378
- scope, patched_receive, send
379
- )
390
+ await self.managed_server.handle_request(scope, patched_receive, send)
380
391
  return
381
392
 
382
393
  response_status = 200
@@ -404,7 +415,7 @@ class _MCPTransportResponse(Response):
404
415
  elif not response_too_large:
405
416
  body_chunks.append(chunk)
406
417
 
407
- await self.managed_server.manager.handle_request(
418
+ await self.managed_server.handle_request(
408
419
  scope, patched_receive, intercepting_send
409
420
  )
410
421
  if response_too_large:
@@ -498,20 +509,6 @@ async def mcp_interaction_protected_resource_metadata(
498
509
 
499
510
 
500
511
  @router.delete("/api/v1/agent/{agent_id}/session/{session}/mcp", tags=["MCP"])
501
- @requires_one([AgentRole.MEMBER])
502
- async def mcp_handler_delete(
503
- request: Request,
504
- agent_id: str,
505
- session: str,
506
- x_stf_user: str = Header(..., include_in_schema=False),
507
- x_stf_account: str = Header(..., include_in_schema=False),
508
- x_stf_account_type: str = Header(..., include_in_schema=False),
509
- ):
510
- app: HTTPApplication = request.app
511
- key = (x_stf_account, x_stf_user, x_stf_account_type, agent_id, session)
512
- await app.mcp_server_pool.remove(key)
513
-
514
-
515
512
  @router.get("/api/v1/agent/{agent_id}/session/{session}/mcp", tags=["MCP"])
516
513
  @router.post("/api/v1/agent/{agent_id}/session/{session}/mcp", tags=["MCP"])
517
514
  @requires_one([AgentRole.MEMBER])
@@ -524,7 +521,11 @@ async def interaction_mcp_handler(
524
521
  x_stf_account_type: str = Header(..., include_in_schema=False),
525
522
  ):
526
523
  app: HTTPApplication = request.app
527
- runtime_settings = getattr(app, "_standalone_settings", app.settings)
524
+ if request.method in {"GET", "DELETE"} and not request.headers.get(
525
+ "mcp-session-id"
526
+ ):
527
+ raise HTTPException(status_code=400, detail="Missing Mcp-Session-Id")
528
+ runtime_settings = app.mcp_settings
528
529
  max_request_bytes = runtime_settings.mcp_max_request_bytes
529
530
  max_response_bytes = runtime_settings.mcp_max_response_bytes
530
531
  content_length = request.headers.get("content-length")
@@ -321,6 +321,10 @@ class StandaloneApplication(FastAPI):
321
321
  def settings(self) -> ServerSettings:
322
322
  return self._server_settings
323
323
 
324
+ @property
325
+ def mcp_settings(self) -> StandaloneSettings:
326
+ return self._standalone_settings
327
+
324
328
  # ------------------------------------------------------------------
325
329
  # Lifecycle
326
330
  # ------------------------------------------------------------------
@@ -192,6 +192,7 @@ class StandaloneSettings(BaseSettings):
192
192
  mcp_max_request_bytes: int = Field(default=1024 * 1024, ge=1)
193
193
  mcp_max_response_bytes: int = Field(default=4 * 1024 * 1024, ge=1)
194
194
  mcp_max_servers: int = Field(default=100, ge=1)
195
+ mcp_max_sessions_per_server: int = Field(default=100, ge=1)
195
196
  mcp_session_idle_ttl_seconds: int = Field(default=1800, ge=1)
196
197
  mcp_startup_timeout_seconds: int = Field(default=30, ge=1)
197
198