steerable-agent-runtime 0.6.52__tar.gz → 0.6.53__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 (163) hide show
  1. {steerable_agent_runtime-0.6.52 → steerable_agent_runtime-0.6.53}/PKG-INFO +3 -3
  2. {steerable_agent_runtime-0.6.52 → steerable_agent_runtime-0.6.53}/pyproject.toml +7 -7
  3. {steerable_agent_runtime-0.6.52 → steerable_agent_runtime-0.6.53}/src/steerable_agent_runtime/__init__.py +6 -0
  4. {steerable_agent_runtime-0.6.52 → steerable_agent_runtime-0.6.53}/src/steerable_agent_runtime/mcp.py +242 -7
  5. {steerable_agent_runtime-0.6.52 → steerable_agent_runtime-0.6.53}/src/steerable_agent_runtime.egg-info/PKG-INFO +3 -3
  6. {steerable_agent_runtime-0.6.52 → steerable_agent_runtime-0.6.53}/src/steerable_agent_runtime.egg-info/requires.txt +2 -2
  7. {steerable_agent_runtime-0.6.52 → steerable_agent_runtime-0.6.53}/tests/test_mcp.py +155 -0
  8. {steerable_agent_runtime-0.6.52 → steerable_agent_runtime-0.6.53}/README.md +0 -0
  9. {steerable_agent_runtime-0.6.52 → steerable_agent_runtime-0.6.53}/setup.cfg +0 -0
  10. {steerable_agent_runtime-0.6.52 → steerable_agent_runtime-0.6.53}/src/steerable_agent_runtime/antihallucination.py +0 -0
  11. {steerable_agent_runtime-0.6.52 → steerable_agent_runtime-0.6.53}/src/steerable_agent_runtime/approval.py +0 -0
  12. {steerable_agent_runtime-0.6.52 → steerable_agent_runtime-0.6.53}/src/steerable_agent_runtime/approval_policy.py +0 -0
  13. {steerable_agent_runtime-0.6.52 → steerable_agent_runtime-0.6.53}/src/steerable_agent_runtime/ask_user.py +0 -0
  14. {steerable_agent_runtime-0.6.52 → steerable_agent_runtime-0.6.53}/src/steerable_agent_runtime/branch.py +0 -0
  15. {steerable_agent_runtime-0.6.52 → steerable_agent_runtime-0.6.53}/src/steerable_agent_runtime/cache_control.py +0 -0
  16. {steerable_agent_runtime-0.6.52 → steerable_agent_runtime-0.6.53}/src/steerable_agent_runtime/calibration.py +0 -0
  17. {steerable_agent_runtime-0.6.52 → steerable_agent_runtime-0.6.53}/src/steerable_agent_runtime/compaction.py +0 -0
  18. {steerable_agent_runtime-0.6.52 → steerable_agent_runtime-0.6.53}/src/steerable_agent_runtime/compaction_policy.py +0 -0
  19. {steerable_agent_runtime-0.6.52 → steerable_agent_runtime-0.6.53}/src/steerable_agent_runtime/config.py +0 -0
  20. {steerable_agent_runtime-0.6.52 → steerable_agent_runtime-0.6.53}/src/steerable_agent_runtime/default.harness.json +0 -0
  21. {steerable_agent_runtime-0.6.52 → steerable_agent_runtime-0.6.53}/src/steerable_agent_runtime/default.harness.yaml +0 -0
  22. {steerable_agent_runtime-0.6.52 → steerable_agent_runtime-0.6.53}/src/steerable_agent_runtime/delegation_gate.py +0 -0
  23. {steerable_agent_runtime-0.6.52 → steerable_agent_runtime-0.6.53}/src/steerable_agent_runtime/errors.py +0 -0
  24. {steerable_agent_runtime-0.6.52 → steerable_agent_runtime-0.6.53}/src/steerable_agent_runtime/gateway_catalog.py +0 -0
  25. {steerable_agent_runtime-0.6.52 → steerable_agent_runtime-0.6.53}/src/steerable_agent_runtime/handoff.py +0 -0
  26. {steerable_agent_runtime-0.6.52 → steerable_agent_runtime-0.6.53}/src/steerable_agent_runtime/harness.py +0 -0
  27. {steerable_agent_runtime-0.6.52 → steerable_agent_runtime-0.6.53}/src/steerable_agent_runtime/harness_spec.py +0 -0
  28. {steerable_agent_runtime-0.6.52 → steerable_agent_runtime-0.6.53}/src/steerable_agent_runtime/history.py +0 -0
  29. {steerable_agent_runtime-0.6.52 → steerable_agent_runtime-0.6.53}/src/steerable_agent_runtime/hooks.py +0 -0
  30. {steerable_agent_runtime-0.6.52 → steerable_agent_runtime-0.6.53}/src/steerable_agent_runtime/llm/__init__.py +0 -0
  31. {steerable_agent_runtime-0.6.52 → steerable_agent_runtime-0.6.53}/src/steerable_agent_runtime/llm/anthropic_native.py +0 -0
  32. {steerable_agent_runtime-0.6.52 → steerable_agent_runtime-0.6.53}/src/steerable_agent_runtime/llm/compat.py +0 -0
  33. {steerable_agent_runtime-0.6.52 → steerable_agent_runtime-0.6.53}/src/steerable_agent_runtime/llm/errors.py +0 -0
  34. {steerable_agent_runtime-0.6.52 → steerable_agent_runtime-0.6.53}/src/steerable_agent_runtime/llm/google_genai.py +0 -0
  35. {steerable_agent_runtime-0.6.52 → steerable_agent_runtime-0.6.53}/src/steerable_agent_runtime/llm/openai_compat.py +0 -0
  36. {steerable_agent_runtime-0.6.52 → steerable_agent_runtime-0.6.53}/src/steerable_agent_runtime/llm/openai_responses.py +0 -0
  37. {steerable_agent_runtime-0.6.52 → steerable_agent_runtime-0.6.53}/src/steerable_agent_runtime/llm/parts.py +0 -0
  38. {steerable_agent_runtime-0.6.52 → steerable_agent_runtime-0.6.53}/src/steerable_agent_runtime/llm/presets.py +0 -0
  39. {steerable_agent_runtime-0.6.52 → steerable_agent_runtime-0.6.53}/src/steerable_agent_runtime/llm/system_proxy.py +0 -0
  40. {steerable_agent_runtime-0.6.52 → steerable_agent_runtime-0.6.53}/src/steerable_agent_runtime/loop.py +0 -0
  41. {steerable_agent_runtime-0.6.52 → steerable_agent_runtime-0.6.53}/src/steerable_agent_runtime/maintenance.py +0 -0
  42. {steerable_agent_runtime-0.6.52 → steerable_agent_runtime-0.6.53}/src/steerable_agent_runtime/mcp_server.py +0 -0
  43. {steerable_agent_runtime-0.6.52 → steerable_agent_runtime-0.6.53}/src/steerable_agent_runtime/model_catalog.py +0 -0
  44. {steerable_agent_runtime-0.6.52 → steerable_agent_runtime-0.6.53}/src/steerable_agent_runtime/model_info.py +0 -0
  45. {steerable_agent_runtime-0.6.52 → steerable_agent_runtime-0.6.53}/src/steerable_agent_runtime/model_resolve.py +0 -0
  46. {steerable_agent_runtime-0.6.52 → steerable_agent_runtime-0.6.53}/src/steerable_agent_runtime/native_bridge.py +0 -0
  47. {steerable_agent_runtime-0.6.52 → steerable_agent_runtime-0.6.53}/src/steerable_agent_runtime/observation_aging.py +0 -0
  48. {steerable_agent_runtime-0.6.52 → steerable_agent_runtime-0.6.53}/src/steerable_agent_runtime/orchestration.py +0 -0
  49. {steerable_agent_runtime-0.6.52 → steerable_agent_runtime-0.6.53}/src/steerable_agent_runtime/otel.py +0 -0
  50. {steerable_agent_runtime-0.6.52 → steerable_agent_runtime-0.6.53}/src/steerable_agent_runtime/plugins.py +0 -0
  51. {steerable_agent_runtime-0.6.52 → steerable_agent_runtime-0.6.53}/src/steerable_agent_runtime/pool.py +0 -0
  52. {steerable_agent_runtime-0.6.52 → steerable_agent_runtime-0.6.53}/src/steerable_agent_runtime/pricing.py +0 -0
  53. {steerable_agent_runtime-0.6.52 → steerable_agent_runtime-0.6.53}/src/steerable_agent_runtime/pseudo.py +0 -0
  54. {steerable_agent_runtime-0.6.52 → steerable_agent_runtime-0.6.53}/src/steerable_agent_runtime/recording.py +0 -0
  55. {steerable_agent_runtime-0.6.52 → steerable_agent_runtime-0.6.53}/src/steerable_agent_runtime/reminders.py +0 -0
  56. {steerable_agent_runtime-0.6.52 → steerable_agent_runtime-0.6.53}/src/steerable_agent_runtime/replay.py +0 -0
  57. {steerable_agent_runtime-0.6.52 → steerable_agent_runtime-0.6.53}/src/steerable_agent_runtime/resume.py +0 -0
  58. {steerable_agent_runtime-0.6.52 → steerable_agent_runtime-0.6.53}/src/steerable_agent_runtime/retry.py +0 -0
  59. {steerable_agent_runtime-0.6.52 → steerable_agent_runtime-0.6.53}/src/steerable_agent_runtime/sandboxed.py +0 -0
  60. {steerable_agent_runtime-0.6.52 → steerable_agent_runtime-0.6.53}/src/steerable_agent_runtime/skills.py +0 -0
  61. {steerable_agent_runtime-0.6.52 → steerable_agent_runtime-0.6.53}/src/steerable_agent_runtime/spill.py +0 -0
  62. {steerable_agent_runtime-0.6.52 → steerable_agent_runtime-0.6.53}/src/steerable_agent_runtime/storage/__init__.py +0 -0
  63. {steerable_agent_runtime-0.6.52 → steerable_agent_runtime-0.6.53}/src/steerable_agent_runtime/storage/in_memory.py +0 -0
  64. {steerable_agent_runtime-0.6.52 → steerable_agent_runtime-0.6.53}/src/steerable_agent_runtime/storage/sqlalchemy_store.py +0 -0
  65. {steerable_agent_runtime-0.6.52 → steerable_agent_runtime-0.6.53}/src/steerable_agent_runtime/storage/sqlite_store.py +0 -0
  66. {steerable_agent_runtime-0.6.52 → steerable_agent_runtime-0.6.53}/src/steerable_agent_runtime/storage/write_lease.py +0 -0
  67. {steerable_agent_runtime-0.6.52 → steerable_agent_runtime-0.6.53}/src/steerable_agent_runtime/subagent.py +0 -0
  68. {steerable_agent_runtime-0.6.52 → steerable_agent_runtime-0.6.53}/src/steerable_agent_runtime/todo.py +0 -0
  69. {steerable_agent_runtime-0.6.52 → steerable_agent_runtime-0.6.53}/src/steerable_agent_runtime/tokens.py +0 -0
  70. {steerable_agent_runtime-0.6.52 → steerable_agent_runtime-0.6.53}/src/steerable_agent_runtime/tool_presentation.py +0 -0
  71. {steerable_agent_runtime-0.6.52 → steerable_agent_runtime-0.6.53}/src/steerable_agent_runtime/tool_schema.py +0 -0
  72. {steerable_agent_runtime-0.6.52 → steerable_agent_runtime-0.6.53}/src/steerable_agent_runtime/tool_search.py +0 -0
  73. {steerable_agent_runtime-0.6.52 → steerable_agent_runtime-0.6.53}/src/steerable_agent_runtime/tools.py +0 -0
  74. {steerable_agent_runtime-0.6.52 → steerable_agent_runtime-0.6.53}/src/steerable_agent_runtime/tracing.py +0 -0
  75. {steerable_agent_runtime-0.6.52 → steerable_agent_runtime-0.6.53}/src/steerable_agent_runtime/transport/__init__.py +0 -0
  76. {steerable_agent_runtime-0.6.52 → steerable_agent_runtime-0.6.53}/src/steerable_agent_runtime/transport/fastapi_sse.py +0 -0
  77. {steerable_agent_runtime-0.6.52 → steerable_agent_runtime-0.6.53}/src/steerable_agent_runtime/transport/stdio_jsonrpc.py +0 -0
  78. {steerable_agent_runtime-0.6.52 → steerable_agent_runtime-0.6.53}/src/steerable_agent_runtime/world_state.py +0 -0
  79. {steerable_agent_runtime-0.6.52 → steerable_agent_runtime-0.6.53}/src/steerable_agent_runtime.egg-info/SOURCES.txt +0 -0
  80. {steerable_agent_runtime-0.6.52 → steerable_agent_runtime-0.6.53}/src/steerable_agent_runtime.egg-info/dependency_links.txt +0 -0
  81. {steerable_agent_runtime-0.6.52 → steerable_agent_runtime-0.6.53}/src/steerable_agent_runtime.egg-info/top_level.txt +0 -0
  82. {steerable_agent_runtime-0.6.52 → steerable_agent_runtime-0.6.53}/tests/test_antihallucination.py +0 -0
  83. {steerable_agent_runtime-0.6.52 → steerable_agent_runtime-0.6.53}/tests/test_approval.py +0 -0
  84. {steerable_agent_runtime-0.6.52 → steerable_agent_runtime-0.6.53}/tests/test_approval_policy.py +0 -0
  85. {steerable_agent_runtime-0.6.52 → steerable_agent_runtime-0.6.53}/tests/test_ask_user.py +0 -0
  86. {steerable_agent_runtime-0.6.52 → steerable_agent_runtime-0.6.53}/tests/test_branch.py +0 -0
  87. {steerable_agent_runtime-0.6.52 → steerable_agent_runtime-0.6.53}/tests/test_cache_control.py +0 -0
  88. {steerable_agent_runtime-0.6.52 → steerable_agent_runtime-0.6.53}/tests/test_cache_instrumentation.py +0 -0
  89. {steerable_agent_runtime-0.6.52 → steerable_agent_runtime-0.6.53}/tests/test_calibration.py +0 -0
  90. {steerable_agent_runtime-0.6.52 → steerable_agent_runtime-0.6.53}/tests/test_compaction.py +0 -0
  91. {steerable_agent_runtime-0.6.52 → steerable_agent_runtime-0.6.53}/tests/test_compaction_policy.py +0 -0
  92. {steerable_agent_runtime-0.6.52 → steerable_agent_runtime-0.6.53}/tests/test_compaction_strategy.py +0 -0
  93. {steerable_agent_runtime-0.6.52 → steerable_agent_runtime-0.6.53}/tests/test_config.py +0 -0
  94. {steerable_agent_runtime-0.6.52 → steerable_agent_runtime-0.6.53}/tests/test_content_parts.py +0 -0
  95. {steerable_agent_runtime-0.6.52 → steerable_agent_runtime-0.6.53}/tests/test_delegation_gate.py +0 -0
  96. {steerable_agent_runtime-0.6.52 → steerable_agent_runtime-0.6.53}/tests/test_error_taxonomy.py +0 -0
  97. {steerable_agent_runtime-0.6.52 → steerable_agent_runtime-0.6.53}/tests/test_fragment_bounds.py +0 -0
  98. {steerable_agent_runtime-0.6.52 → steerable_agent_runtime-0.6.53}/tests/test_gateway_catalog.py +0 -0
  99. {steerable_agent_runtime-0.6.52 → steerable_agent_runtime-0.6.53}/tests/test_golden.py +0 -0
  100. {steerable_agent_runtime-0.6.52 → steerable_agent_runtime-0.6.53}/tests/test_handoff.py +0 -0
  101. {steerable_agent_runtime-0.6.52 → steerable_agent_runtime-0.6.53}/tests/test_harness.py +0 -0
  102. {steerable_agent_runtime-0.6.52 → steerable_agent_runtime-0.6.53}/tests/test_harness_spec.py +0 -0
  103. {steerable_agent_runtime-0.6.52 → steerable_agent_runtime-0.6.53}/tests/test_history.py +0 -0
  104. {steerable_agent_runtime-0.6.52 → steerable_agent_runtime-0.6.53}/tests/test_history_persistence.py +0 -0
  105. {steerable_agent_runtime-0.6.52 → steerable_agent_runtime-0.6.53}/tests/test_hooks.py +0 -0
  106. {steerable_agent_runtime-0.6.52 → steerable_agent_runtime-0.6.53}/tests/test_in_memory_storage.py +0 -0
  107. {steerable_agent_runtime-0.6.52 → steerable_agent_runtime-0.6.53}/tests/test_llm_gemini_wire.py +0 -0
  108. {steerable_agent_runtime-0.6.52 → steerable_agent_runtime-0.6.53}/tests/test_llm_responses_wire.py +0 -0
  109. {steerable_agent_runtime-0.6.52 → steerable_agent_runtime-0.6.53}/tests/test_llm_stream_e2e.py +0 -0
  110. {steerable_agent_runtime-0.6.52 → steerable_agent_runtime-0.6.53}/tests/test_llm_wire_helpers.py +0 -0
  111. {steerable_agent_runtime-0.6.52 → steerable_agent_runtime-0.6.53}/tests/test_long_session.py +0 -0
  112. {steerable_agent_runtime-0.6.52 → steerable_agent_runtime-0.6.53}/tests/test_loop.py +0 -0
  113. {steerable_agent_runtime-0.6.52 → steerable_agent_runtime-0.6.53}/tests/test_loop_cancellation.py +0 -0
  114. {steerable_agent_runtime-0.6.52 → steerable_agent_runtime-0.6.53}/tests/test_loop_replay.py +0 -0
  115. {steerable_agent_runtime-0.6.52 → steerable_agent_runtime-0.6.53}/tests/test_loop_sandbox_event.py +0 -0
  116. {steerable_agent_runtime-0.6.52 → steerable_agent_runtime-0.6.53}/tests/test_loop_terminal.py +0 -0
  117. {steerable_agent_runtime-0.6.52 → steerable_agent_runtime-0.6.53}/tests/test_maintenance.py +0 -0
  118. {steerable_agent_runtime-0.6.52 → steerable_agent_runtime-0.6.53}/tests/test_mcp_server.py +0 -0
  119. {steerable_agent_runtime-0.6.52 → steerable_agent_runtime-0.6.53}/tests/test_model_catalog.py +0 -0
  120. {steerable_agent_runtime-0.6.52 → steerable_agent_runtime-0.6.53}/tests/test_model_equivalence.py +0 -0
  121. {steerable_agent_runtime-0.6.52 → steerable_agent_runtime-0.6.53}/tests/test_model_info.py +0 -0
  122. {steerable_agent_runtime-0.6.52 → steerable_agent_runtime-0.6.53}/tests/test_model_resolve.py +0 -0
  123. {steerable_agent_runtime-0.6.52 → steerable_agent_runtime-0.6.53}/tests/test_native_default.py +0 -0
  124. {steerable_agent_runtime-0.6.52 → steerable_agent_runtime-0.6.53}/tests/test_observation_aging.py +0 -0
  125. {steerable_agent_runtime-0.6.52 → steerable_agent_runtime-0.6.53}/tests/test_orchestration.py +0 -0
  126. {steerable_agent_runtime-0.6.52 → steerable_agent_runtime-0.6.53}/tests/test_otel.py +0 -0
  127. {steerable_agent_runtime-0.6.52 → steerable_agent_runtime-0.6.53}/tests/test_parallel_tools.py +0 -0
  128. {steerable_agent_runtime-0.6.52 → steerable_agent_runtime-0.6.53}/tests/test_plugins.py +0 -0
  129. {steerable_agent_runtime-0.6.52 → steerable_agent_runtime-0.6.53}/tests/test_provider_compat.py +0 -0
  130. {steerable_agent_runtime-0.6.52 → steerable_agent_runtime-0.6.53}/tests/test_provider_presets.py +0 -0
  131. {steerable_agent_runtime-0.6.52 → steerable_agent_runtime-0.6.53}/tests/test_pseudo.py +0 -0
  132. {steerable_agent_runtime-0.6.52 → steerable_agent_runtime-0.6.53}/tests/test_pyo3_api_surface.py +0 -0
  133. {steerable_agent_runtime-0.6.52 → steerable_agent_runtime-0.6.53}/tests/test_recording.py +0 -0
  134. {steerable_agent_runtime-0.6.52 → steerable_agent_runtime-0.6.53}/tests/test_reminders.py +0 -0
  135. {steerable_agent_runtime-0.6.52 → steerable_agent_runtime-0.6.53}/tests/test_replay_crosslang.py +0 -0
  136. {steerable_agent_runtime-0.6.52 → steerable_agent_runtime-0.6.53}/tests/test_resume.py +0 -0
  137. {steerable_agent_runtime-0.6.52 → steerable_agent_runtime-0.6.53}/tests/test_retry_hooks.py +0 -0
  138. {steerable_agent_runtime-0.6.52 → steerable_agent_runtime-0.6.53}/tests/test_safety_gate.py +0 -0
  139. {steerable_agent_runtime-0.6.52 → steerable_agent_runtime-0.6.53}/tests/test_sandboxed.py +0 -0
  140. {steerable_agent_runtime-0.6.52 → steerable_agent_runtime-0.6.53}/tests/test_skills.py +0 -0
  141. {steerable_agent_runtime-0.6.52 → steerable_agent_runtime-0.6.53}/tests/test_soft_timeout.py +0 -0
  142. {steerable_agent_runtime-0.6.52 → steerable_agent_runtime-0.6.53}/tests/test_spill.py +0 -0
  143. {steerable_agent_runtime-0.6.52 → steerable_agent_runtime-0.6.53}/tests/test_sqlite_storage.py +0 -0
  144. {steerable_agent_runtime-0.6.52 → steerable_agent_runtime-0.6.53}/tests/test_steer.py +0 -0
  145. {steerable_agent_runtime-0.6.52 → steerable_agent_runtime-0.6.53}/tests/test_storage_contract.py +0 -0
  146. {steerable_agent_runtime-0.6.52 → steerable_agent_runtime-0.6.53}/tests/test_stream_strip.py +0 -0
  147. {steerable_agent_runtime-0.6.52 → steerable_agent_runtime-0.6.53}/tests/test_subagent.py +0 -0
  148. {steerable_agent_runtime-0.6.52 → steerable_agent_runtime-0.6.53}/tests/test_system_proxy.py +0 -0
  149. {steerable_agent_runtime-0.6.52 → steerable_agent_runtime-0.6.53}/tests/test_todo.py +0 -0
  150. {steerable_agent_runtime-0.6.52 → steerable_agent_runtime-0.6.53}/tests/test_todo_gate.py +0 -0
  151. {steerable_agent_runtime-0.6.52 → steerable_agent_runtime-0.6.53}/tests/test_tokens.py +0 -0
  152. {steerable_agent_runtime-0.6.52 → steerable_agent_runtime-0.6.53}/tests/test_tool_exposure.py +0 -0
  153. {steerable_agent_runtime-0.6.52 → steerable_agent_runtime-0.6.53}/tests/test_tool_hygiene.py +0 -0
  154. {steerable_agent_runtime-0.6.52 → steerable_agent_runtime-0.6.53}/tests/test_tool_presentation.py +0 -0
  155. {steerable_agent_runtime-0.6.52 → steerable_agent_runtime-0.6.53}/tests/test_tool_router.py +0 -0
  156. {steerable_agent_runtime-0.6.52 → steerable_agent_runtime-0.6.53}/tests/test_tool_schema.py +0 -0
  157. {steerable_agent_runtime-0.6.52 → steerable_agent_runtime-0.6.53}/tests/test_tool_timeout.py +0 -0
  158. {steerable_agent_runtime-0.6.52 → steerable_agent_runtime-0.6.53}/tests/test_trace_recorder.py +0 -0
  159. {steerable_agent_runtime-0.6.52 → steerable_agent_runtime-0.6.53}/tests/test_transport_jsonrpc.py +0 -0
  160. {steerable_agent_runtime-0.6.52 → steerable_agent_runtime-0.6.53}/tests/test_transport_sse.py +0 -0
  161. {steerable_agent_runtime-0.6.52 → steerable_agent_runtime-0.6.53}/tests/test_usage_attribution.py +0 -0
  162. {steerable_agent_runtime-0.6.52 → steerable_agent_runtime-0.6.53}/tests/test_world_state.py +0 -0
  163. {steerable_agent_runtime-0.6.52 → steerable_agent_runtime-0.6.53}/tests/test_write_lease.py +0 -0
@@ -1,10 +1,12 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: steerable-agent-runtime
3
- Version: 0.6.52
3
+ Version: 0.6.53
4
4
  Summary: Steerable agent runtime: LLM, tool, storage, and transport adapters.
5
5
  License: Apache-2.0
6
6
  Requires-Python: >=3.10
7
7
  Description-Content-Type: text/markdown
8
+ Requires-Dist: httpx[socks]>=0.27
9
+ Requires-Dist: mcp<2.0.0,>=1.13.0
8
10
  Requires-Dist: pydantic>=2.10.0
9
11
  Requires-Dist: steerable-agent-protocol<1.0.0,>=0.1.0
10
12
  Requires-Dist: steerable-agent-harness<1.0.0,>=0.1.0
@@ -15,14 +17,12 @@ Provides-Extra: fastapi
15
17
  Requires-Dist: fastapi>=0.110; extra == "fastapi"
16
18
  Requires-Dist: starlette>=0.37; extra == "fastapi"
17
19
  Provides-Extra: openai
18
- Requires-Dist: httpx[socks]>=0.27; extra == "openai"
19
20
  Provides-Extra: anthropic
20
21
  Requires-Dist: anthropic>=0.40; extra == "anthropic"
21
22
  Provides-Extra: all
22
23
  Requires-Dist: sqlalchemy>=2.0; extra == "all"
23
24
  Requires-Dist: fastapi>=0.110; extra == "all"
24
25
  Requires-Dist: starlette>=0.37; extra == "all"
25
- Requires-Dist: httpx[socks]>=0.27; extra == "all"
26
26
  Requires-Dist: anthropic>=0.40; extra == "all"
27
27
 
28
28
  # steerable-agent-runtime
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "steerable-agent-runtime"
3
- version = "0.6.52"
3
+ version = "0.6.53"
4
4
  description = "Steerable agent runtime: LLM, tool, storage, and transport adapters."
5
5
  license = { text = "Apache-2.0" }
6
6
  readme = "README.md"
@@ -13,6 +13,11 @@ requires-python = ">=3.10"
13
13
  # CoreLoop execution is the Rust engine. The matching native wheel
14
14
  # is a hard dependency: install or startup fails if it is missing.
15
15
  dependencies = [
16
+ # MCP Streamable HTTP and OpenAI-compatible providers both construct httpx
17
+ # clients. Keep SOCKS support in the base install because exported proxy
18
+ # variables are evaluated during client construction.
19
+ "httpx[socks]>=0.27",
20
+ "mcp>=1.13.0,<2.0.0",
16
21
  "pydantic>=2.10.0",
17
22
  "steerable-agent-protocol>=0.1.0,<1.0.0",
18
23
  "steerable-agent-harness>=0.1.0,<1.0.0",
@@ -22,17 +27,12 @@ dependencies = [
22
27
  [project.optional-dependencies]
23
28
  sqlalchemy = ["sqlalchemy>=2.0"]
24
29
  fastapi = ["fastapi>=0.110", "starlette>=0.37"]
25
- # The `socks` extra is not optional in practice: httpx builds a transport for
26
- # every proxy the environment declares while the client is constructed, so a
27
- # host exporting `all_proxy=socks5://…` — the common shell proxy snippet — makes
28
- # construction raise without it, failing provider calls that never reach a URL.
29
- openai = ["httpx[socks]>=0.27"]
30
+ openai = []
30
31
  anthropic = ["anthropic>=0.40"]
31
32
  all = [
32
33
  "sqlalchemy>=2.0",
33
34
  "fastapi>=0.110",
34
35
  "starlette>=0.37",
35
- "httpx[socks]>=0.27",
36
36
  "anthropic>=0.40",
37
37
  ]
38
38
 
@@ -163,9 +163,12 @@ from .mcp import (
163
163
  DEFAULT_MAX_SERVER_TOOLS,
164
164
  McpCallResult,
165
165
  McpCatalogError,
166
+ McpClient,
166
167
  McpError,
168
+ McpStreamableHttpClient,
167
169
  McpStdioClient,
168
170
  McpToolInfo,
171
+ create_mcp_client,
169
172
  mcp_invoker,
170
173
  parse_mcp_name,
171
174
  qualify_mcp_name,
@@ -374,7 +377,9 @@ __all__ = [
374
377
  "LoopHooks",
375
378
  "McpCallResult",
376
379
  "McpCatalogError",
380
+ "McpClient",
377
381
  "McpError",
382
+ "McpStreamableHttpClient",
378
383
  "McpStdioClient",
379
384
  "McpToolInfo",
380
385
  "ModelCalibration",
@@ -472,6 +477,7 @@ __all__ = [
472
477
  "make_todo_write_tool",
473
478
  "parent_input_tool_descriptor",
474
479
  "matches_conditions",
480
+ "create_mcp_client",
475
481
  "mcp_invoker",
476
482
  "merge_patch",
477
483
  "message_from_dict",
@@ -1,4 +1,4 @@
1
- """MCP client seam: qualified names, capped catalogs, minimal stdio client.
1
+ """MCP client seam: qualified names, capped catalogs, transport clients.
2
2
 
3
3
  MCP is the largest unbounded, third-party, mutable source of model-visible
4
4
  context an agent system can add, so it lands on the Wave 2 foundation:
@@ -22,9 +22,9 @@ caps.
22
22
  Architecture (recorded in docs/roadmap.md): in the desktop product, MCP
23
23
  servers are launched host-side (Electron main) and reached through
24
24
  `ToolRouter.register_remote` — a Seatbelt-confined sidecar never spawns
25
- them. `McpStdioClient` exists for hosts that embed the runtime directly
26
- (CLI agents, tests, non-Electron hosts); it is transport-only and holds no
27
- loop state.
25
+ them. `McpStdioClient` and `McpStreamableHttpClient` exist for hosts that
26
+ embed the runtime directly (CLI agents, tests, non-Electron hosts); both
27
+ are transport-only and hold no loop state.
28
28
  """
29
29
 
30
30
  from __future__ import annotations
@@ -34,8 +34,15 @@ import json
34
34
  import logging
35
35
  import os
36
36
  from collections.abc import Awaitable, Callable, Mapping, Sequence
37
+ from contextlib import AsyncExitStack
37
38
  from dataclasses import dataclass, field
38
- from typing import Any
39
+ from datetime import timedelta
40
+ from typing import Any, Protocol
41
+ from urllib.parse import urlparse
42
+
43
+ import httpx
44
+ from mcp import ClientSession
45
+ from mcp.client.streamable_http import streamable_http_client
39
46
 
40
47
  from .errors import RuntimeError as SteerableRuntimeError
41
48
  from .tools import RegisteredTool, ToolExposure, ToolRouter
@@ -126,6 +133,26 @@ class McpCallResult:
126
133
  structured: Any = None
127
134
 
128
135
 
136
+ class McpClient(Protocol):
137
+ """Transport-independent client operations consumed by MCP mounting paths."""
138
+
139
+ server_info: dict[str, Any]
140
+
141
+ async def __aenter__(self) -> McpClient: ...
142
+
143
+ async def __aexit__(self, *exc: Any) -> None: ...
144
+
145
+ async def start(self) -> None: ...
146
+
147
+ async def aclose(self) -> None: ...
148
+
149
+ async def list_tools(self) -> list[McpToolInfo]: ...
150
+
151
+ async def call_tool(
152
+ self, name: str, arguments: dict[str, Any]
153
+ ) -> McpCallResult: ...
154
+
155
+
129
156
  def register_mcp_catalog(
130
157
  router: ToolRouter,
131
158
  *,
@@ -178,9 +205,9 @@ def register_mcp_catalog(
178
205
 
179
206
 
180
207
  def mcp_invoker(
181
- client: "McpStdioClient",
208
+ client: McpClient,
182
209
  ) -> Callable[[str, dict[str, Any]], Awaitable[Any]]:
183
- """Adapt `McpStdioClient.call_tool` to the catalog invoker contract.
210
+ """Adapt an MCP client's `call_tool` to the catalog invoker contract.
184
211
 
185
212
  Maps ``isError`` results to failed `ToolResult`s (follow-up allowed —
186
213
  the model sees the server's error text and can correct its arguments)
@@ -199,6 +226,214 @@ def mcp_invoker(
199
226
  return _invoke
200
227
 
201
228
 
229
+ def create_mcp_client(
230
+ config: Mapping[str, Any],
231
+ *,
232
+ environ: Mapping[str, str] | None = None,
233
+ request_timeout: float = 30.0,
234
+ ) -> McpClient:
235
+ """Build a client from an explicit or legacy-discriminated config entry.
236
+
237
+ ``transport`` accepts ``stdio`` and ``streamable-http``. For existing
238
+ callers, an omitted discriminator is inferred from exactly one of
239
+ ``command`` or ``url``. Environment-sourced header values are resolved
240
+ only into the client and are redacted from transport failures.
241
+ """
242
+ command = config.get("command")
243
+ url = config.get("url")
244
+ if command and url:
245
+ raise McpError("MCP config cannot contain both 'command' and 'url'")
246
+
247
+ transport = config.get("transport")
248
+ if transport is None:
249
+ if command:
250
+ transport = "stdio"
251
+ elif url:
252
+ transport = "streamable-http"
253
+ else:
254
+ raise McpError("MCP config requires a non-empty 'command' or 'url'")
255
+
256
+ if transport == "stdio":
257
+ if not command:
258
+ raise McpError("stdio MCP config requires a non-empty 'command'")
259
+ if url:
260
+ raise McpError("stdio MCP config cannot contain 'url'")
261
+ raw_env = config.get("env") or {}
262
+ if not isinstance(raw_env, Mapping):
263
+ raise McpError("stdio MCP config 'env' must be an object")
264
+ return McpStdioClient(
265
+ str(command),
266
+ [str(arg) for arg in config.get("args") or []],
267
+ env={str(key): str(value) for key, value in raw_env.items()} or None,
268
+ request_timeout=request_timeout,
269
+ )
270
+
271
+ if transport != "streamable-http":
272
+ raise McpError(f"unsupported MCP transport: {transport!r}")
273
+ if not url:
274
+ raise McpError("streamable-http MCP config requires a non-empty 'url'")
275
+ if command:
276
+ raise McpError("streamable-http MCP config cannot contain 'command'")
277
+ parsed = urlparse(str(url))
278
+ if parsed.scheme not in {"http", "https"} or not parsed.netloc:
279
+ raise McpError("streamable-http MCP config 'url' must be an HTTP(S) URL")
280
+ if parsed.username or parsed.password:
281
+ raise McpError("streamable-http MCP config 'url' cannot contain credentials")
282
+
283
+ raw_headers = config.get("headers") or {}
284
+ raw_headers_from_env = config.get("headersFromEnv") or {}
285
+ if not isinstance(raw_headers, Mapping):
286
+ raise McpError("streamable-http MCP config 'headers' must be an object")
287
+ if not isinstance(raw_headers_from_env, Mapping):
288
+ raise McpError(
289
+ "streamable-http MCP config 'headersFromEnv' must be an object"
290
+ )
291
+ source = os.environ if environ is None else environ
292
+ headers = {str(name): str(value) for name, value in raw_headers.items()}
293
+ secret_values = [value for value in headers.values() if value]
294
+ for raw_name, raw_env_name in raw_headers_from_env.items():
295
+ env_name = str(raw_env_name)
296
+ value = source.get(env_name)
297
+ if not value:
298
+ raise McpError(
299
+ f"streamable-http MCP header environment variable "
300
+ f"{env_name!r} is not set"
301
+ )
302
+ headers[str(raw_name)] = value
303
+ secret_values.append(value)
304
+ bearer_env = config.get("bearerTokenEnvVar")
305
+ if bearer_env is not None:
306
+ env_name = str(bearer_env)
307
+ value = source.get(env_name)
308
+ if not value:
309
+ raise McpError(
310
+ f"streamable-http MCP bearer environment variable "
311
+ f"{env_name!r} is not set"
312
+ )
313
+ headers["Authorization"] = f"Bearer {value}"
314
+ secret_values.append(value)
315
+
316
+ return McpStreamableHttpClient(
317
+ str(url),
318
+ headers=headers,
319
+ secret_values=secret_values,
320
+ request_timeout=request_timeout,
321
+ )
322
+
323
+
324
+ class McpStreamableHttpClient:
325
+ """MCP Streamable HTTP client backed by the official Python MCP SDK."""
326
+
327
+ def __init__(
328
+ self,
329
+ url: str,
330
+ *,
331
+ headers: Mapping[str, str] | None = None,
332
+ secret_values: Sequence[str] = (),
333
+ request_timeout: float = 30.0,
334
+ ) -> None:
335
+ self._url = url
336
+ self._headers = dict(headers or {})
337
+ self._secret_values = tuple(value for value in secret_values if value)
338
+ self._request_timeout = request_timeout
339
+ self._stack: AsyncExitStack | None = None
340
+ self._session: ClientSession | None = None
341
+ self.server_info: dict[str, Any] = {}
342
+
343
+ async def __aenter__(self) -> McpStreamableHttpClient:
344
+ await self.start()
345
+ return self
346
+
347
+ async def __aexit__(self, *exc: Any) -> None:
348
+ await self.aclose()
349
+
350
+ async def start(self) -> None:
351
+ if self._stack is not None:
352
+ raise McpError("McpStreamableHttpClient already started")
353
+ stack = AsyncExitStack()
354
+ self._stack = stack
355
+ try:
356
+ http_client = await stack.enter_async_context(
357
+ httpx.AsyncClient(headers=self._headers)
358
+ )
359
+ read_stream, write_stream, _session_id = await stack.enter_async_context(
360
+ streamable_http_client(self._url, http_client=http_client)
361
+ )
362
+ session = await stack.enter_async_context(
363
+ ClientSession(
364
+ read_stream,
365
+ write_stream,
366
+ read_timeout_seconds=timedelta(seconds=self._request_timeout),
367
+ )
368
+ )
369
+ initialized = await session.initialize()
370
+ self._session = session
371
+ self.server_info = initialized.serverInfo.model_dump(
372
+ by_alias=True, exclude_none=True
373
+ )
374
+ except Exception as exc:
375
+ await self.aclose()
376
+ raise self._error("initialize", exc) from exc
377
+
378
+ async def aclose(self) -> None:
379
+ stack, self._stack = self._stack, None
380
+ self._session = None
381
+ if stack is not None:
382
+ await stack.aclose()
383
+
384
+ async def list_tools(self) -> list[McpToolInfo]:
385
+ session = self._require_session()
386
+ tools: list[McpToolInfo] = []
387
+ cursor: str | None = None
388
+ try:
389
+ while True:
390
+ result = await session.list_tools(cursor=cursor)
391
+ tools.extend(
392
+ McpToolInfo(
393
+ name=tool.name,
394
+ description=tool.description or "",
395
+ schema=dict(tool.inputSchema),
396
+ )
397
+ for tool in result.tools
398
+ )
399
+ cursor = result.nextCursor
400
+ if cursor is None:
401
+ return tools
402
+ except Exception as exc:
403
+ raise self._error("tools/list", exc) from exc
404
+
405
+ async def call_tool(
406
+ self, name: str, arguments: dict[str, Any]
407
+ ) -> McpCallResult:
408
+ session = self._require_session()
409
+ try:
410
+ result = await session.call_tool(name, arguments)
411
+ except Exception as exc:
412
+ raise self._error("tools/call", exc) from exc
413
+ parts: list[str] = []
414
+ for content in result.content:
415
+ if content.type == "text":
416
+ parts.append(content.text)
417
+ else:
418
+ parts.append(f"[mcp {content.type} content]")
419
+ return McpCallResult(
420
+ text="\n".join(parts),
421
+ is_error=result.isError,
422
+ structured=result.structuredContent,
423
+ )
424
+
425
+ def _require_session(self) -> ClientSession:
426
+ if self._session is None:
427
+ raise McpError("MCP Streamable HTTP client is not running")
428
+ return self._session
429
+
430
+ def _error(self, operation: str, exc: Exception) -> McpError:
431
+ message = str(exc)
432
+ for secret in self._secret_values:
433
+ message = message.replace(secret, "[redacted]")
434
+ return McpError(f"MCP Streamable HTTP {operation} failed: {message}")
435
+
436
+
202
437
  # ---------------------------------------------------------------------------
203
438
  # Minimal stdio client (NDJSON JSON-RPC 2.0)
204
439
  # ---------------------------------------------------------------------------
@@ -1,10 +1,12 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: steerable-agent-runtime
3
- Version: 0.6.52
3
+ Version: 0.6.53
4
4
  Summary: Steerable agent runtime: LLM, tool, storage, and transport adapters.
5
5
  License: Apache-2.0
6
6
  Requires-Python: >=3.10
7
7
  Description-Content-Type: text/markdown
8
+ Requires-Dist: httpx[socks]>=0.27
9
+ Requires-Dist: mcp<2.0.0,>=1.13.0
8
10
  Requires-Dist: pydantic>=2.10.0
9
11
  Requires-Dist: steerable-agent-protocol<1.0.0,>=0.1.0
10
12
  Requires-Dist: steerable-agent-harness<1.0.0,>=0.1.0
@@ -15,14 +17,12 @@ Provides-Extra: fastapi
15
17
  Requires-Dist: fastapi>=0.110; extra == "fastapi"
16
18
  Requires-Dist: starlette>=0.37; extra == "fastapi"
17
19
  Provides-Extra: openai
18
- Requires-Dist: httpx[socks]>=0.27; extra == "openai"
19
20
  Provides-Extra: anthropic
20
21
  Requires-Dist: anthropic>=0.40; extra == "anthropic"
21
22
  Provides-Extra: all
22
23
  Requires-Dist: sqlalchemy>=2.0; extra == "all"
23
24
  Requires-Dist: fastapi>=0.110; extra == "all"
24
25
  Requires-Dist: starlette>=0.37; extra == "all"
25
- Requires-Dist: httpx[socks]>=0.27; extra == "all"
26
26
  Requires-Dist: anthropic>=0.40; extra == "all"
27
27
 
28
28
  # steerable-agent-runtime
@@ -1,3 +1,5 @@
1
+ httpx[socks]>=0.27
2
+ mcp<2.0.0,>=1.13.0
1
3
  pydantic>=2.10.0
2
4
  steerable-agent-protocol<1.0.0,>=0.1.0
3
5
  steerable-agent-harness<1.0.0,>=0.1.0
@@ -7,7 +9,6 @@ steerable-agent-runtime-native==1.0.3
7
9
  sqlalchemy>=2.0
8
10
  fastapi>=0.110
9
11
  starlette>=0.37
10
- httpx[socks]>=0.27
11
12
  anthropic>=0.40
12
13
 
13
14
  [anthropic]
@@ -18,7 +19,6 @@ fastapi>=0.110
18
19
  starlette>=0.37
19
20
 
20
21
  [openai]
21
- httpx[socks]>=0.27
22
22
 
23
23
  [sqlalchemy]
24
24
  sqlalchemy>=2.0
@@ -10,8 +10,11 @@ The end-to-end test runs the full chain: server → catalog → deferred tier
10
10
 
11
11
  from __future__ import annotations
12
12
 
13
+ import socket
14
+ import subprocess
13
15
  import sys
14
16
  import textwrap
17
+ import time
15
18
  from collections.abc import AsyncIterator
16
19
  from pathlib import Path
17
20
  from typing import Any
@@ -24,10 +27,12 @@ from steerable_agent_runtime import (
24
27
  LLMMessage,
25
28
  McpCatalogError,
26
29
  McpError,
30
+ McpStreamableHttpClient,
27
31
  McpStdioClient,
28
32
  McpToolInfo,
29
33
  RouterToolExecutor,
30
34
  ToolRouter,
35
+ create_mcp_client,
31
36
  mcp_invoker,
32
37
  parse_mcp_name,
33
38
  qualify_mcp_name,
@@ -319,6 +324,69 @@ def fake_server(tmp_path: Path) -> Path:
319
324
  return script
320
325
 
321
326
 
327
+ _FAKE_HTTP_SERVER = textwrap.dedent(
328
+ """
329
+ import sys
330
+
331
+ from mcp.server.fastmcp import Context, FastMCP
332
+
333
+ server = FastMCP(
334
+ "fake-http-mcp",
335
+ host="127.0.0.1",
336
+ port=int(sys.argv[1]),
337
+ stateless_http=True,
338
+ )
339
+
340
+ @server.tool()
341
+ def echo_headers(text: str, ctx: Context) -> str:
342
+ headers = ctx.request_context.request.headers
343
+ return "|".join(
344
+ (
345
+ "echo:" + text,
346
+ "tenant:" + str(headers.get("x-tenant") == "acme"),
347
+ "api:" + str(headers.get("x-api-key") == "api-secret"),
348
+ "auth:" + str(
349
+ headers.get("authorization") == "Bearer bearer-secret"
350
+ ),
351
+ )
352
+ )
353
+
354
+ server.run(transport="streamable-http")
355
+ """
356
+ )
357
+
358
+
359
+ @pytest.fixture()
360
+ def fake_http_server(tmp_path: Path) -> str:
361
+ script = tmp_path / "fake_http_mcp_server.py"
362
+ script.write_text(_FAKE_HTTP_SERVER)
363
+ with socket.socket() as sock:
364
+ sock.bind(("127.0.0.1", 0))
365
+ port = sock.getsockname()[1]
366
+ process = subprocess.Popen(
367
+ [sys.executable, str(script), str(port)],
368
+ stdout=subprocess.DEVNULL,
369
+ stderr=subprocess.DEVNULL,
370
+ )
371
+ deadline = time.monotonic() + 10
372
+ while time.monotonic() < deadline:
373
+ if process.poll() is not None:
374
+ raise RuntimeError("fake Streamable HTTP MCP server exited during startup")
375
+ try:
376
+ with socket.create_connection(("127.0.0.1", port), timeout=0.1):
377
+ break
378
+ except OSError:
379
+ time.sleep(0.05)
380
+ else:
381
+ process.terminate()
382
+ raise RuntimeError("fake Streamable HTTP MCP server did not start")
383
+ try:
384
+ yield f"http://127.0.0.1:{port}/mcp"
385
+ finally:
386
+ process.terminate()
387
+ process.wait(timeout=5)
388
+
389
+
322
390
  @pytest.mark.asyncio
323
391
  async def test_stdio_client_handshake_list_and_call(fake_server: Path) -> None:
324
392
  async with McpStdioClient(sys.executable, [str(fake_server)]) as client:
@@ -376,6 +444,93 @@ async def test_stdio_client_use_before_start_fails(fake_server: Path) -> None:
376
444
  await client.call_tool("echo", {})
377
445
 
378
446
 
447
+ def test_create_mcp_client_supports_explicit_and_legacy_transports() -> None:
448
+ assert isinstance(
449
+ create_mcp_client({"transport": "stdio", "command": "server"}),
450
+ McpStdioClient,
451
+ )
452
+ assert isinstance(create_mcp_client({"command": "server"}), McpStdioClient)
453
+ assert isinstance(
454
+ create_mcp_client(
455
+ {"transport": "streamable-http", "url": "https://example.test/mcp"}
456
+ ),
457
+ McpStreamableHttpClient,
458
+ )
459
+ assert isinstance(
460
+ create_mcp_client({"url": "https://example.test/mcp"}),
461
+ McpStreamableHttpClient,
462
+ )
463
+
464
+
465
+ def test_create_mcp_client_rejects_mixed_transport_config() -> None:
466
+ with pytest.raises(McpError, match="both 'command' and 'url'"):
467
+ create_mcp_client(
468
+ {"command": "server", "url": "https://example.test/mcp"}
469
+ )
470
+
471
+
472
+ @pytest.mark.asyncio
473
+ async def test_streamable_http_client_uses_sdk_and_env_headers(
474
+ fake_http_server: str,
475
+ monkeypatch: pytest.MonkeyPatch,
476
+ ) -> None:
477
+ monkeypatch.setenv("MCP_API_KEY", "api-secret")
478
+ monkeypatch.setenv("MCP_BEARER", "bearer-secret")
479
+ config = {
480
+ "transport": "streamable-http",
481
+ "url": fake_http_server,
482
+ "headers": {"X-Tenant": "acme"},
483
+ "headersFromEnv": {"X-Api-Key": "MCP_API_KEY"},
484
+ "bearerTokenEnvVar": "MCP_BEARER",
485
+ }
486
+ async with create_mcp_client(config) as client:
487
+ tools = await client.list_tools()
488
+ assert [tool.name for tool in tools] == ["echo_headers"]
489
+ result = await client.call_tool("echo_headers", {"text": "hello"})
490
+ assert result.text == "echo:hello|tenant:True|api:True|auth:True"
491
+ assert "api-secret" not in result.text
492
+ assert "bearer-secret" not in result.text
493
+ assert "api-secret" not in repr(config)
494
+ assert "bearer-secret" not in repr(config)
495
+
496
+
497
+ def test_streamable_http_client_requires_env_header_values(
498
+ monkeypatch: pytest.MonkeyPatch,
499
+ ) -> None:
500
+ monkeypatch.delenv("MISSING_MCP_TOKEN", raising=False)
501
+ with pytest.raises(McpError, match="MISSING_MCP_TOKEN"):
502
+ create_mcp_client(
503
+ {
504
+ "url": "https://example.test/mcp",
505
+ "headersFromEnv": {"X-Api-Key": "MISSING_MCP_TOKEN"},
506
+ }
507
+ )
508
+
509
+
510
+ def test_streamable_http_client_rejects_url_credentials() -> None:
511
+ with pytest.raises(McpError, match="credentials"):
512
+ create_mcp_client({"url": "https://user:secret@example.test/mcp"})
513
+
514
+
515
+ def test_streamable_http_client_redacts_header_values_from_errors() -> None:
516
+ client = create_mcp_client(
517
+ {
518
+ "url": "https://example.test/mcp",
519
+ "headers": {"X-Api-Key": "direct-secret"},
520
+ "headersFromEnv": {"X-Other-Key": "OTHER_KEY"},
521
+ "bearerTokenEnvVar": "BEARER_KEY",
522
+ },
523
+ environ={"OTHER_KEY": "env-secret", "BEARER_KEY": "bearer-secret"},
524
+ )
525
+ assert isinstance(client, McpStreamableHttpClient)
526
+ error = client._error(
527
+ "test", RuntimeError("direct-secret env-secret bearer-secret")
528
+ )
529
+ assert str(error) == (
530
+ "MCP Streamable HTTP test failed: [redacted] [redacted] [redacted]"
531
+ )
532
+
533
+
379
534
  # ---------------------------------------------------------------------------
380
535
  # End to end: server → catalog → deferred tier → tool_search → loop
381
536
  # ---------------------------------------------------------------------------