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.
- {steerable_agent_runtime-0.6.52 → steerable_agent_runtime-0.6.53}/PKG-INFO +3 -3
- {steerable_agent_runtime-0.6.52 → steerable_agent_runtime-0.6.53}/pyproject.toml +7 -7
- {steerable_agent_runtime-0.6.52 → steerable_agent_runtime-0.6.53}/src/steerable_agent_runtime/__init__.py +6 -0
- {steerable_agent_runtime-0.6.52 → steerable_agent_runtime-0.6.53}/src/steerable_agent_runtime/mcp.py +242 -7
- {steerable_agent_runtime-0.6.52 → steerable_agent_runtime-0.6.53}/src/steerable_agent_runtime.egg-info/PKG-INFO +3 -3
- {steerable_agent_runtime-0.6.52 → steerable_agent_runtime-0.6.53}/src/steerable_agent_runtime.egg-info/requires.txt +2 -2
- {steerable_agent_runtime-0.6.52 → steerable_agent_runtime-0.6.53}/tests/test_mcp.py +155 -0
- {steerable_agent_runtime-0.6.52 → steerable_agent_runtime-0.6.53}/README.md +0 -0
- {steerable_agent_runtime-0.6.52 → steerable_agent_runtime-0.6.53}/setup.cfg +0 -0
- {steerable_agent_runtime-0.6.52 → steerable_agent_runtime-0.6.53}/src/steerable_agent_runtime/antihallucination.py +0 -0
- {steerable_agent_runtime-0.6.52 → steerable_agent_runtime-0.6.53}/src/steerable_agent_runtime/approval.py +0 -0
- {steerable_agent_runtime-0.6.52 → steerable_agent_runtime-0.6.53}/src/steerable_agent_runtime/approval_policy.py +0 -0
- {steerable_agent_runtime-0.6.52 → steerable_agent_runtime-0.6.53}/src/steerable_agent_runtime/ask_user.py +0 -0
- {steerable_agent_runtime-0.6.52 → steerable_agent_runtime-0.6.53}/src/steerable_agent_runtime/branch.py +0 -0
- {steerable_agent_runtime-0.6.52 → steerable_agent_runtime-0.6.53}/src/steerable_agent_runtime/cache_control.py +0 -0
- {steerable_agent_runtime-0.6.52 → steerable_agent_runtime-0.6.53}/src/steerable_agent_runtime/calibration.py +0 -0
- {steerable_agent_runtime-0.6.52 → steerable_agent_runtime-0.6.53}/src/steerable_agent_runtime/compaction.py +0 -0
- {steerable_agent_runtime-0.6.52 → steerable_agent_runtime-0.6.53}/src/steerable_agent_runtime/compaction_policy.py +0 -0
- {steerable_agent_runtime-0.6.52 → steerable_agent_runtime-0.6.53}/src/steerable_agent_runtime/config.py +0 -0
- {steerable_agent_runtime-0.6.52 → steerable_agent_runtime-0.6.53}/src/steerable_agent_runtime/default.harness.json +0 -0
- {steerable_agent_runtime-0.6.52 → steerable_agent_runtime-0.6.53}/src/steerable_agent_runtime/default.harness.yaml +0 -0
- {steerable_agent_runtime-0.6.52 → steerable_agent_runtime-0.6.53}/src/steerable_agent_runtime/delegation_gate.py +0 -0
- {steerable_agent_runtime-0.6.52 → steerable_agent_runtime-0.6.53}/src/steerable_agent_runtime/errors.py +0 -0
- {steerable_agent_runtime-0.6.52 → steerable_agent_runtime-0.6.53}/src/steerable_agent_runtime/gateway_catalog.py +0 -0
- {steerable_agent_runtime-0.6.52 → steerable_agent_runtime-0.6.53}/src/steerable_agent_runtime/handoff.py +0 -0
- {steerable_agent_runtime-0.6.52 → steerable_agent_runtime-0.6.53}/src/steerable_agent_runtime/harness.py +0 -0
- {steerable_agent_runtime-0.6.52 → steerable_agent_runtime-0.6.53}/src/steerable_agent_runtime/harness_spec.py +0 -0
- {steerable_agent_runtime-0.6.52 → steerable_agent_runtime-0.6.53}/src/steerable_agent_runtime/history.py +0 -0
- {steerable_agent_runtime-0.6.52 → steerable_agent_runtime-0.6.53}/src/steerable_agent_runtime/hooks.py +0 -0
- {steerable_agent_runtime-0.6.52 → steerable_agent_runtime-0.6.53}/src/steerable_agent_runtime/llm/__init__.py +0 -0
- {steerable_agent_runtime-0.6.52 → steerable_agent_runtime-0.6.53}/src/steerable_agent_runtime/llm/anthropic_native.py +0 -0
- {steerable_agent_runtime-0.6.52 → steerable_agent_runtime-0.6.53}/src/steerable_agent_runtime/llm/compat.py +0 -0
- {steerable_agent_runtime-0.6.52 → steerable_agent_runtime-0.6.53}/src/steerable_agent_runtime/llm/errors.py +0 -0
- {steerable_agent_runtime-0.6.52 → steerable_agent_runtime-0.6.53}/src/steerable_agent_runtime/llm/google_genai.py +0 -0
- {steerable_agent_runtime-0.6.52 → steerable_agent_runtime-0.6.53}/src/steerable_agent_runtime/llm/openai_compat.py +0 -0
- {steerable_agent_runtime-0.6.52 → steerable_agent_runtime-0.6.53}/src/steerable_agent_runtime/llm/openai_responses.py +0 -0
- {steerable_agent_runtime-0.6.52 → steerable_agent_runtime-0.6.53}/src/steerable_agent_runtime/llm/parts.py +0 -0
- {steerable_agent_runtime-0.6.52 → steerable_agent_runtime-0.6.53}/src/steerable_agent_runtime/llm/presets.py +0 -0
- {steerable_agent_runtime-0.6.52 → steerable_agent_runtime-0.6.53}/src/steerable_agent_runtime/llm/system_proxy.py +0 -0
- {steerable_agent_runtime-0.6.52 → steerable_agent_runtime-0.6.53}/src/steerable_agent_runtime/loop.py +0 -0
- {steerable_agent_runtime-0.6.52 → steerable_agent_runtime-0.6.53}/src/steerable_agent_runtime/maintenance.py +0 -0
- {steerable_agent_runtime-0.6.52 → steerable_agent_runtime-0.6.53}/src/steerable_agent_runtime/mcp_server.py +0 -0
- {steerable_agent_runtime-0.6.52 → steerable_agent_runtime-0.6.53}/src/steerable_agent_runtime/model_catalog.py +0 -0
- {steerable_agent_runtime-0.6.52 → steerable_agent_runtime-0.6.53}/src/steerable_agent_runtime/model_info.py +0 -0
- {steerable_agent_runtime-0.6.52 → steerable_agent_runtime-0.6.53}/src/steerable_agent_runtime/model_resolve.py +0 -0
- {steerable_agent_runtime-0.6.52 → steerable_agent_runtime-0.6.53}/src/steerable_agent_runtime/native_bridge.py +0 -0
- {steerable_agent_runtime-0.6.52 → steerable_agent_runtime-0.6.53}/src/steerable_agent_runtime/observation_aging.py +0 -0
- {steerable_agent_runtime-0.6.52 → steerable_agent_runtime-0.6.53}/src/steerable_agent_runtime/orchestration.py +0 -0
- {steerable_agent_runtime-0.6.52 → steerable_agent_runtime-0.6.53}/src/steerable_agent_runtime/otel.py +0 -0
- {steerable_agent_runtime-0.6.52 → steerable_agent_runtime-0.6.53}/src/steerable_agent_runtime/plugins.py +0 -0
- {steerable_agent_runtime-0.6.52 → steerable_agent_runtime-0.6.53}/src/steerable_agent_runtime/pool.py +0 -0
- {steerable_agent_runtime-0.6.52 → steerable_agent_runtime-0.6.53}/src/steerable_agent_runtime/pricing.py +0 -0
- {steerable_agent_runtime-0.6.52 → steerable_agent_runtime-0.6.53}/src/steerable_agent_runtime/pseudo.py +0 -0
- {steerable_agent_runtime-0.6.52 → steerable_agent_runtime-0.6.53}/src/steerable_agent_runtime/recording.py +0 -0
- {steerable_agent_runtime-0.6.52 → steerable_agent_runtime-0.6.53}/src/steerable_agent_runtime/reminders.py +0 -0
- {steerable_agent_runtime-0.6.52 → steerable_agent_runtime-0.6.53}/src/steerable_agent_runtime/replay.py +0 -0
- {steerable_agent_runtime-0.6.52 → steerable_agent_runtime-0.6.53}/src/steerable_agent_runtime/resume.py +0 -0
- {steerable_agent_runtime-0.6.52 → steerable_agent_runtime-0.6.53}/src/steerable_agent_runtime/retry.py +0 -0
- {steerable_agent_runtime-0.6.52 → steerable_agent_runtime-0.6.53}/src/steerable_agent_runtime/sandboxed.py +0 -0
- {steerable_agent_runtime-0.6.52 → steerable_agent_runtime-0.6.53}/src/steerable_agent_runtime/skills.py +0 -0
- {steerable_agent_runtime-0.6.52 → steerable_agent_runtime-0.6.53}/src/steerable_agent_runtime/spill.py +0 -0
- {steerable_agent_runtime-0.6.52 → steerable_agent_runtime-0.6.53}/src/steerable_agent_runtime/storage/__init__.py +0 -0
- {steerable_agent_runtime-0.6.52 → steerable_agent_runtime-0.6.53}/src/steerable_agent_runtime/storage/in_memory.py +0 -0
- {steerable_agent_runtime-0.6.52 → steerable_agent_runtime-0.6.53}/src/steerable_agent_runtime/storage/sqlalchemy_store.py +0 -0
- {steerable_agent_runtime-0.6.52 → steerable_agent_runtime-0.6.53}/src/steerable_agent_runtime/storage/sqlite_store.py +0 -0
- {steerable_agent_runtime-0.6.52 → steerable_agent_runtime-0.6.53}/src/steerable_agent_runtime/storage/write_lease.py +0 -0
- {steerable_agent_runtime-0.6.52 → steerable_agent_runtime-0.6.53}/src/steerable_agent_runtime/subagent.py +0 -0
- {steerable_agent_runtime-0.6.52 → steerable_agent_runtime-0.6.53}/src/steerable_agent_runtime/todo.py +0 -0
- {steerable_agent_runtime-0.6.52 → steerable_agent_runtime-0.6.53}/src/steerable_agent_runtime/tokens.py +0 -0
- {steerable_agent_runtime-0.6.52 → steerable_agent_runtime-0.6.53}/src/steerable_agent_runtime/tool_presentation.py +0 -0
- {steerable_agent_runtime-0.6.52 → steerable_agent_runtime-0.6.53}/src/steerable_agent_runtime/tool_schema.py +0 -0
- {steerable_agent_runtime-0.6.52 → steerable_agent_runtime-0.6.53}/src/steerable_agent_runtime/tool_search.py +0 -0
- {steerable_agent_runtime-0.6.52 → steerable_agent_runtime-0.6.53}/src/steerable_agent_runtime/tools.py +0 -0
- {steerable_agent_runtime-0.6.52 → steerable_agent_runtime-0.6.53}/src/steerable_agent_runtime/tracing.py +0 -0
- {steerable_agent_runtime-0.6.52 → steerable_agent_runtime-0.6.53}/src/steerable_agent_runtime/transport/__init__.py +0 -0
- {steerable_agent_runtime-0.6.52 → steerable_agent_runtime-0.6.53}/src/steerable_agent_runtime/transport/fastapi_sse.py +0 -0
- {steerable_agent_runtime-0.6.52 → steerable_agent_runtime-0.6.53}/src/steerable_agent_runtime/transport/stdio_jsonrpc.py +0 -0
- {steerable_agent_runtime-0.6.52 → steerable_agent_runtime-0.6.53}/src/steerable_agent_runtime/world_state.py +0 -0
- {steerable_agent_runtime-0.6.52 → steerable_agent_runtime-0.6.53}/src/steerable_agent_runtime.egg-info/SOURCES.txt +0 -0
- {steerable_agent_runtime-0.6.52 → steerable_agent_runtime-0.6.53}/src/steerable_agent_runtime.egg-info/dependency_links.txt +0 -0
- {steerable_agent_runtime-0.6.52 → steerable_agent_runtime-0.6.53}/src/steerable_agent_runtime.egg-info/top_level.txt +0 -0
- {steerable_agent_runtime-0.6.52 → steerable_agent_runtime-0.6.53}/tests/test_antihallucination.py +0 -0
- {steerable_agent_runtime-0.6.52 → steerable_agent_runtime-0.6.53}/tests/test_approval.py +0 -0
- {steerable_agent_runtime-0.6.52 → steerable_agent_runtime-0.6.53}/tests/test_approval_policy.py +0 -0
- {steerable_agent_runtime-0.6.52 → steerable_agent_runtime-0.6.53}/tests/test_ask_user.py +0 -0
- {steerable_agent_runtime-0.6.52 → steerable_agent_runtime-0.6.53}/tests/test_branch.py +0 -0
- {steerable_agent_runtime-0.6.52 → steerable_agent_runtime-0.6.53}/tests/test_cache_control.py +0 -0
- {steerable_agent_runtime-0.6.52 → steerable_agent_runtime-0.6.53}/tests/test_cache_instrumentation.py +0 -0
- {steerable_agent_runtime-0.6.52 → steerable_agent_runtime-0.6.53}/tests/test_calibration.py +0 -0
- {steerable_agent_runtime-0.6.52 → steerable_agent_runtime-0.6.53}/tests/test_compaction.py +0 -0
- {steerable_agent_runtime-0.6.52 → steerable_agent_runtime-0.6.53}/tests/test_compaction_policy.py +0 -0
- {steerable_agent_runtime-0.6.52 → steerable_agent_runtime-0.6.53}/tests/test_compaction_strategy.py +0 -0
- {steerable_agent_runtime-0.6.52 → steerable_agent_runtime-0.6.53}/tests/test_config.py +0 -0
- {steerable_agent_runtime-0.6.52 → steerable_agent_runtime-0.6.53}/tests/test_content_parts.py +0 -0
- {steerable_agent_runtime-0.6.52 → steerable_agent_runtime-0.6.53}/tests/test_delegation_gate.py +0 -0
- {steerable_agent_runtime-0.6.52 → steerable_agent_runtime-0.6.53}/tests/test_error_taxonomy.py +0 -0
- {steerable_agent_runtime-0.6.52 → steerable_agent_runtime-0.6.53}/tests/test_fragment_bounds.py +0 -0
- {steerable_agent_runtime-0.6.52 → steerable_agent_runtime-0.6.53}/tests/test_gateway_catalog.py +0 -0
- {steerable_agent_runtime-0.6.52 → steerable_agent_runtime-0.6.53}/tests/test_golden.py +0 -0
- {steerable_agent_runtime-0.6.52 → steerable_agent_runtime-0.6.53}/tests/test_handoff.py +0 -0
- {steerable_agent_runtime-0.6.52 → steerable_agent_runtime-0.6.53}/tests/test_harness.py +0 -0
- {steerable_agent_runtime-0.6.52 → steerable_agent_runtime-0.6.53}/tests/test_harness_spec.py +0 -0
- {steerable_agent_runtime-0.6.52 → steerable_agent_runtime-0.6.53}/tests/test_history.py +0 -0
- {steerable_agent_runtime-0.6.52 → steerable_agent_runtime-0.6.53}/tests/test_history_persistence.py +0 -0
- {steerable_agent_runtime-0.6.52 → steerable_agent_runtime-0.6.53}/tests/test_hooks.py +0 -0
- {steerable_agent_runtime-0.6.52 → steerable_agent_runtime-0.6.53}/tests/test_in_memory_storage.py +0 -0
- {steerable_agent_runtime-0.6.52 → steerable_agent_runtime-0.6.53}/tests/test_llm_gemini_wire.py +0 -0
- {steerable_agent_runtime-0.6.52 → steerable_agent_runtime-0.6.53}/tests/test_llm_responses_wire.py +0 -0
- {steerable_agent_runtime-0.6.52 → steerable_agent_runtime-0.6.53}/tests/test_llm_stream_e2e.py +0 -0
- {steerable_agent_runtime-0.6.52 → steerable_agent_runtime-0.6.53}/tests/test_llm_wire_helpers.py +0 -0
- {steerable_agent_runtime-0.6.52 → steerable_agent_runtime-0.6.53}/tests/test_long_session.py +0 -0
- {steerable_agent_runtime-0.6.52 → steerable_agent_runtime-0.6.53}/tests/test_loop.py +0 -0
- {steerable_agent_runtime-0.6.52 → steerable_agent_runtime-0.6.53}/tests/test_loop_cancellation.py +0 -0
- {steerable_agent_runtime-0.6.52 → steerable_agent_runtime-0.6.53}/tests/test_loop_replay.py +0 -0
- {steerable_agent_runtime-0.6.52 → steerable_agent_runtime-0.6.53}/tests/test_loop_sandbox_event.py +0 -0
- {steerable_agent_runtime-0.6.52 → steerable_agent_runtime-0.6.53}/tests/test_loop_terminal.py +0 -0
- {steerable_agent_runtime-0.6.52 → steerable_agent_runtime-0.6.53}/tests/test_maintenance.py +0 -0
- {steerable_agent_runtime-0.6.52 → steerable_agent_runtime-0.6.53}/tests/test_mcp_server.py +0 -0
- {steerable_agent_runtime-0.6.52 → steerable_agent_runtime-0.6.53}/tests/test_model_catalog.py +0 -0
- {steerable_agent_runtime-0.6.52 → steerable_agent_runtime-0.6.53}/tests/test_model_equivalence.py +0 -0
- {steerable_agent_runtime-0.6.52 → steerable_agent_runtime-0.6.53}/tests/test_model_info.py +0 -0
- {steerable_agent_runtime-0.6.52 → steerable_agent_runtime-0.6.53}/tests/test_model_resolve.py +0 -0
- {steerable_agent_runtime-0.6.52 → steerable_agent_runtime-0.6.53}/tests/test_native_default.py +0 -0
- {steerable_agent_runtime-0.6.52 → steerable_agent_runtime-0.6.53}/tests/test_observation_aging.py +0 -0
- {steerable_agent_runtime-0.6.52 → steerable_agent_runtime-0.6.53}/tests/test_orchestration.py +0 -0
- {steerable_agent_runtime-0.6.52 → steerable_agent_runtime-0.6.53}/tests/test_otel.py +0 -0
- {steerable_agent_runtime-0.6.52 → steerable_agent_runtime-0.6.53}/tests/test_parallel_tools.py +0 -0
- {steerable_agent_runtime-0.6.52 → steerable_agent_runtime-0.6.53}/tests/test_plugins.py +0 -0
- {steerable_agent_runtime-0.6.52 → steerable_agent_runtime-0.6.53}/tests/test_provider_compat.py +0 -0
- {steerable_agent_runtime-0.6.52 → steerable_agent_runtime-0.6.53}/tests/test_provider_presets.py +0 -0
- {steerable_agent_runtime-0.6.52 → steerable_agent_runtime-0.6.53}/tests/test_pseudo.py +0 -0
- {steerable_agent_runtime-0.6.52 → steerable_agent_runtime-0.6.53}/tests/test_pyo3_api_surface.py +0 -0
- {steerable_agent_runtime-0.6.52 → steerable_agent_runtime-0.6.53}/tests/test_recording.py +0 -0
- {steerable_agent_runtime-0.6.52 → steerable_agent_runtime-0.6.53}/tests/test_reminders.py +0 -0
- {steerable_agent_runtime-0.6.52 → steerable_agent_runtime-0.6.53}/tests/test_replay_crosslang.py +0 -0
- {steerable_agent_runtime-0.6.52 → steerable_agent_runtime-0.6.53}/tests/test_resume.py +0 -0
- {steerable_agent_runtime-0.6.52 → steerable_agent_runtime-0.6.53}/tests/test_retry_hooks.py +0 -0
- {steerable_agent_runtime-0.6.52 → steerable_agent_runtime-0.6.53}/tests/test_safety_gate.py +0 -0
- {steerable_agent_runtime-0.6.52 → steerable_agent_runtime-0.6.53}/tests/test_sandboxed.py +0 -0
- {steerable_agent_runtime-0.6.52 → steerable_agent_runtime-0.6.53}/tests/test_skills.py +0 -0
- {steerable_agent_runtime-0.6.52 → steerable_agent_runtime-0.6.53}/tests/test_soft_timeout.py +0 -0
- {steerable_agent_runtime-0.6.52 → steerable_agent_runtime-0.6.53}/tests/test_spill.py +0 -0
- {steerable_agent_runtime-0.6.52 → steerable_agent_runtime-0.6.53}/tests/test_sqlite_storage.py +0 -0
- {steerable_agent_runtime-0.6.52 → steerable_agent_runtime-0.6.53}/tests/test_steer.py +0 -0
- {steerable_agent_runtime-0.6.52 → steerable_agent_runtime-0.6.53}/tests/test_storage_contract.py +0 -0
- {steerable_agent_runtime-0.6.52 → steerable_agent_runtime-0.6.53}/tests/test_stream_strip.py +0 -0
- {steerable_agent_runtime-0.6.52 → steerable_agent_runtime-0.6.53}/tests/test_subagent.py +0 -0
- {steerable_agent_runtime-0.6.52 → steerable_agent_runtime-0.6.53}/tests/test_system_proxy.py +0 -0
- {steerable_agent_runtime-0.6.52 → steerable_agent_runtime-0.6.53}/tests/test_todo.py +0 -0
- {steerable_agent_runtime-0.6.52 → steerable_agent_runtime-0.6.53}/tests/test_todo_gate.py +0 -0
- {steerable_agent_runtime-0.6.52 → steerable_agent_runtime-0.6.53}/tests/test_tokens.py +0 -0
- {steerable_agent_runtime-0.6.52 → steerable_agent_runtime-0.6.53}/tests/test_tool_exposure.py +0 -0
- {steerable_agent_runtime-0.6.52 → steerable_agent_runtime-0.6.53}/tests/test_tool_hygiene.py +0 -0
- {steerable_agent_runtime-0.6.52 → steerable_agent_runtime-0.6.53}/tests/test_tool_presentation.py +0 -0
- {steerable_agent_runtime-0.6.52 → steerable_agent_runtime-0.6.53}/tests/test_tool_router.py +0 -0
- {steerable_agent_runtime-0.6.52 → steerable_agent_runtime-0.6.53}/tests/test_tool_schema.py +0 -0
- {steerable_agent_runtime-0.6.52 → steerable_agent_runtime-0.6.53}/tests/test_tool_timeout.py +0 -0
- {steerable_agent_runtime-0.6.52 → steerable_agent_runtime-0.6.53}/tests/test_trace_recorder.py +0 -0
- {steerable_agent_runtime-0.6.52 → steerable_agent_runtime-0.6.53}/tests/test_transport_jsonrpc.py +0 -0
- {steerable_agent_runtime-0.6.52 → steerable_agent_runtime-0.6.53}/tests/test_transport_sse.py +0 -0
- {steerable_agent_runtime-0.6.52 → steerable_agent_runtime-0.6.53}/tests/test_usage_attribution.py +0 -0
- {steerable_agent_runtime-0.6.52 → steerable_agent_runtime-0.6.53}/tests/test_world_state.py +0 -0
- {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.
|
|
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.
|
|
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
|
-
|
|
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",
|
{steerable_agent_runtime-0.6.52 → steerable_agent_runtime-0.6.53}/src/steerable_agent_runtime/mcp.py
RENAMED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
"""MCP client seam: qualified names, capped catalogs,
|
|
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`
|
|
26
|
-
(CLI agents, tests, non-Electron hosts);
|
|
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
|
|
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:
|
|
208
|
+
client: McpClient,
|
|
182
209
|
) -> Callable[[str, dict[str, Any]], Awaitable[Any]]:
|
|
183
|
-
"""Adapt `
|
|
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.
|
|
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
|
# ---------------------------------------------------------------------------
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|