alysis-code 0.13.0__py3-none-any.whl
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.
- alysis_code/__init__.py +3 -0
- alysis_code/__main__.py +4 -0
- alysis_code/_build_info.py +14 -0
- alysis_code/account_login.py +468 -0
- alysis_code/agent/README.md +35 -0
- alysis_code/agent/__init__.py +11 -0
- alysis_code/agent/acceptance_contract.py +2217 -0
- alysis_code/agent/blast_radius.py +1403 -0
- alysis_code/agent/cache_keepalive.py +227 -0
- alysis_code/agent/completion_certificate.py +366 -0
- alysis_code/agent/completion_gate.py +306 -0
- alysis_code/agent/empty_response_stall.py +403 -0
- alysis_code/agent/errors.py +28 -0
- alysis_code/agent/llm_calls.py +475 -0
- alysis_code/agent/mutation_classification.py +227 -0
- alysis_code/agent/prompt_context.py +2508 -0
- alysis_code/agent/read_ledger.py +253 -0
- alysis_code/agent/regression_baseline.py +642 -0
- alysis_code/agent/reproduction_first.py +610 -0
- alysis_code/agent/sensitive_output.py +629 -0
- alysis_code/agent/session.py +3218 -0
- alysis_code/agent/steering.py +191 -0
- alysis_code/agent/subagent_execution.py +5177 -0
- alysis_code/agent/subagent_workspace.py +666 -0
- alysis_code/agent/tools_assembly.py +4728 -0
- alysis_code/agent/turn/__init__.py +101 -0
- alysis_code/agent/turn/core.py +8483 -0
- alysis_code/agent/turn/events.py +113 -0
- alysis_code/agent/turn/exploration.py +590 -0
- alysis_code/agent/turn/interventions.py +65 -0
- alysis_code/agent/turn/read_cache.py +420 -0
- alysis_code/agent/turn/snapshot.py +179 -0
- alysis_code/agent/turn_contract.py +661 -0
- alysis_code/agent/turn_path.py +129 -0
- alysis_code/agent/verification.py +2885 -0
- alysis_code/agent/verification_commands.py +512 -0
- alysis_code/agent/verification_evidence.py +738 -0
- alysis_code/agent_loop.py +693 -0
- alysis_code/agent_runtimes/__init__.py +51 -0
- alysis_code/agent_runtimes/base.py +114 -0
- alysis_code/agent_runtimes/builtins.py +129 -0
- alysis_code/agent_runtimes/codex_cli.py +664 -0
- alysis_code/agent_runtimes/host.py +263 -0
- alysis_code/agent_runtimes/registry.py +64 -0
- alysis_code/agent_runtimes/service.py +150 -0
- alysis_code/agentbox_client.py +416 -0
- alysis_code/agentbox_integration.py +310 -0
- alysis_code/alysis_cloud.py +152 -0
- alysis_code/approval_scope.py +276 -0
- alysis_code/assets/README.md +33 -0
- alysis_code/assets/__init__.py +126 -0
- alysis_code/assets/asset_read_core.py +281 -0
- alysis_code/assets/budget_allocator.py +456 -0
- alysis_code/assets/comprehender.py +759 -0
- alysis_code/assets/index.py +654 -0
- alysis_code/assets/ingestion.py +275 -0
- alysis_code/assets/legacy_migration.py +413 -0
- alysis_code/assets/models.py +263 -0
- alysis_code/assets/ocr.py +239 -0
- alysis_code/assets/owl/ascii/f-000.txt +13 -0
- alysis_code/assets/owl/ascii/f-001.txt +13 -0
- alysis_code/assets/owl/ascii/f-002.txt +13 -0
- alysis_code/assets/owl/ascii/f-003.txt +13 -0
- alysis_code/assets/owl/ascii/f-004.txt +13 -0
- alysis_code/assets/owl/ascii/f-005.txt +13 -0
- alysis_code/assets/owl/ascii/f-006.txt +13 -0
- alysis_code/assets/owl/ascii/f-007.txt +13 -0
- alysis_code/assets/owl/ascii/f-008.txt +13 -0
- alysis_code/assets/owl/ascii/f-009.txt +13 -0
- alysis_code/assets/owl/ascii/f-010.txt +13 -0
- alysis_code/assets/owl/ascii/f-011.txt +13 -0
- alysis_code/assets/owl/ascii/f-012.txt +13 -0
- alysis_code/assets/owl/ascii/f-013.txt +13 -0
- alysis_code/assets/owl/ascii/f-014.txt +13 -0
- alysis_code/assets/owl/ascii/f-015.txt +13 -0
- alysis_code/assets/owl/ascii/f-016.txt +13 -0
- alysis_code/assets/owl/ascii/f-017.txt +13 -0
- alysis_code/assets/owl/ascii/f-018.txt +13 -0
- alysis_code/assets/owl/ascii/f-019.txt +13 -0
- alysis_code/assets/owl/ascii/f-020.txt +13 -0
- alysis_code/assets/owl/index.html +98 -0
- alysis_code/assets/owl/show-owl.sh +761 -0
- alysis_code/assets/paths.py +49 -0
- alysis_code/assets/plan_binding.py +326 -0
- alysis_code/assets/planner_context.py +466 -0
- alysis_code/assets/planner_tools.py +184 -0
- alysis_code/assets/prompts.py +101 -0
- alysis_code/assets/replanner_context.py +239 -0
- alysis_code/assets/surface.py +521 -0
- alysis_code/assets/untrusted_content.py +48 -0
- alysis_code/assets/usage_logger.py +94 -0
- alysis_code/assets/worker_mirror.py +428 -0
- alysis_code/assets/worker_section.py +303 -0
- alysis_code/assets/worker_tools.py +468 -0
- alysis_code/atomic_io.py +83 -0
- alysis_code/auth_diagnostics.py +272 -0
- alysis_code/background_runner.py +366 -0
- alysis_code/branding.py +270 -0
- alysis_code/budget_policy.py +390 -0
- alysis_code/build_identity.py +465 -0
- alysis_code/builtin_hooks/__init__.py +7 -0
- alysis_code/builtin_hooks/notify_done_windows.py +65 -0
- alysis_code/bwrap_etc.py +76 -0
- alysis_code/cancellation.py +41 -0
- alysis_code/capabilities.py +137 -0
- alysis_code/chatgpt_codex_static_provider.py +133 -0
- alysis_code/cli.py +51 -0
- alysis_code/cli_impl/__init__.py +1 -0
- alysis_code/cli_impl/assets_cli.py +537 -0
- alysis_code/cli_impl/assets_modal.py +412 -0
- alysis_code/cli_impl/chat/__init__.py +156 -0
- alysis_code/cli_impl/chat/commands.py +2616 -0
- alysis_code/cli_impl/chat/loop.py +4508 -0
- alysis_code/cli_impl/chat/mid_turn_policy.py +125 -0
- alysis_code/cli_impl/chat/rendering.py +444 -0
- alysis_code/cli_impl/chat/state.py +124 -0
- alysis_code/cli_impl/chat_resume.py +830 -0
- alysis_code/cli_impl/chat_slash_completer.py +258 -0
- alysis_code/cli_impl/commands/__init__.py +11 -0
- alysis_code/cli_impl/commands/_shared.py +89 -0
- alysis_code/cli_impl/commands/auth.py +623 -0
- alysis_code/cli_impl/commands/chat_resume_helpers.py +1531 -0
- alysis_code/cli_impl/commands/chat_state.py +158 -0
- alysis_code/cli_impl/commands/chat_status.py +1248 -0
- alysis_code/cli_impl/commands/chat_terminal.py +942 -0
- alysis_code/cli_impl/commands/chat_tui_panels.py +1018 -0
- alysis_code/cli_impl/commands/cli_common.py +1223 -0
- alysis_code/cli_impl/commands/cli_surface.py +77 -0
- alysis_code/cli_impl/commands/config.py +131 -0
- alysis_code/cli_impl/commands/conventions.py +85 -0
- alysis_code/cli_impl/commands/execution_helpers.py +350 -0
- alysis_code/cli_impl/commands/extensions.py +401 -0
- alysis_code/cli_impl/commands/forge.py +1282 -0
- alysis_code/cli_impl/commands/forge_asset_view.py +121 -0
- alysis_code/cli_impl/commands/forge_helpers.py +1215 -0
- alysis_code/cli_impl/commands/hooks.py +737 -0
- alysis_code/cli_impl/commands/ide_bridge.py +31 -0
- alysis_code/cli_impl/commands/mcp.py +700 -0
- alysis_code/cli_impl/commands/profile.py +453 -0
- alysis_code/cli_impl/commands/prompt_helpers.py +307 -0
- alysis_code/cli_impl/commands/report.py +88 -0
- alysis_code/cli_impl/commands/root.py +1118 -0
- alysis_code/cli_impl/commands/sandbox.py +184 -0
- alysis_code/cli_impl/commands/server.py +54 -0
- alysis_code/cli_impl/commands/sessions.py +252 -0
- alysis_code/cli_impl/commands/skills.py +404 -0
- alysis_code/cli_impl/commands/startup.py +946 -0
- alysis_code/cli_impl/commands/tools.py +335 -0
- alysis_code/cli_impl/commands/update.py +364 -0
- alysis_code/cli_impl/commands/welcome.py +972 -0
- alysis_code/cli_impl/config_menu.py +3882 -0
- alysis_code/cli_impl/forge.py +4509 -0
- alysis_code/cli_impl/forge_recovery.py +485 -0
- alysis_code/cli_impl/setup_wizard.py +2409 -0
- alysis_code/cli_impl/tui/__init__.py +58 -0
- alysis_code/cli_impl/tui/app.py +4551 -0
- alysis_code/cli_impl/tui/config.py +32 -0
- alysis_code/cli_impl/tui/config_flow.py +2754 -0
- alysis_code/cli_impl/tui/config_overlay.py +566 -0
- alysis_code/cli_impl/tui/content.py +78 -0
- alysis_code/cli_impl/tui/footer.py +218 -0
- alysis_code/cli_impl/tui/forge_status.py +136 -0
- alysis_code/cli_impl/tui/markdown.py +244 -0
- alysis_code/cli_impl/tui/owl.py +109 -0
- alysis_code/cli_impl/tui/plan_meta.py +477 -0
- alysis_code/cli_impl/tui/setup_app.py +519 -0
- alysis_code/cli_impl/tui/setup_flow.py +1622 -0
- alysis_code/cli_impl/tui/state.py +101 -0
- alysis_code/cli_impl/tui/subagent_identity.py +66 -0
- alysis_code/cli_impl/tui/subagent_panel.py +186 -0
- alysis_code/cli_impl/tui/surface.py +796 -0
- alysis_code/cli_impl/tui/transcript.py +514 -0
- alysis_code/cli_impl/tui/update_prompt.py +79 -0
- alysis_code/cli_impl/tui/workspace_guard.py +384 -0
- alysis_code/clipboard.py +172 -0
- alysis_code/code_review.py +1211 -0
- alysis_code/compaction/__init__.py +28 -0
- alysis_code/compaction/conversation_compactor.py +2932 -0
- alysis_code/compaction/importance.py +177 -0
- alysis_code/compaction/settings.py +297 -0
- alysis_code/compaction/tool_output_offload.py +447 -0
- alysis_code/config.py +3509 -0
- alysis_code/conflict_auto_resolver.py +895 -0
- alysis_code/context/__init__.py +1 -0
- alysis_code/context/tool_schema_budgeter.py +220 -0
- alysis_code/crash_diagnostics.py +282 -0
- alysis_code/custom_tools/README.md +34 -0
- alysis_code/custom_tools/__init__.py +43 -0
- alysis_code/custom_tools/discovery.py +903 -0
- alysis_code/custom_tools/runtime.py +1516 -0
- alysis_code/custom_tools/session.py +227 -0
- alysis_code/custom_tools/trust.py +232 -0
- alysis_code/diff_paths.py +113 -0
- alysis_code/direction_change.py +293 -0
- alysis_code/dispatch_timing.py +306 -0
- alysis_code/durable_service_manager.py +1236 -0
- alysis_code/edit_discipline.py +659 -0
- alysis_code/error_text.py +73 -0
- alysis_code/execution_budget.py +411 -0
- alysis_code/execution_context.py +915 -0
- alysis_code/execution_deadline.py +1065 -0
- alysis_code/execution_shared.py +1904 -0
- alysis_code/extensions/README.md +30 -0
- alysis_code/extensions/__init__.py +93 -0
- alysis_code/extensions/activation.py +138 -0
- alysis_code/extensions/install.py +1436 -0
- alysis_code/extensions/manifest.py +487 -0
- alysis_code/extensions/models.py +74 -0
- alysis_code/extensions/paths.py +56 -0
- alysis_code/extensions/registry.json +4 -0
- alysis_code/extensions/registry.py +52 -0
- alysis_code/extensions/state.py +83 -0
- alysis_code/extensions/workspace_trust.py +101 -0
- alysis_code/failed_task_evidence.py +369 -0
- alysis_code/failure_category.py +315 -0
- alysis_code/feedback_report.py +1647 -0
- alysis_code/file_classification.py +485 -0
- alysis_code/forge.py +2064 -0
- alysis_code/forge_completion.py +362 -0
- alysis_code/forge_events.py +475 -0
- alysis_code/frontmatter_utils.py +95 -0
- alysis_code/git_evidence.py +1181 -0
- alysis_code/git_ops.py +560 -0
- alysis_code/git_safe.py +62 -0
- alysis_code/git_worktrees.py +190 -0
- alysis_code/hooks/README.md +33 -0
- alysis_code/hooks/__init__.py +67 -0
- alysis_code/hooks/audit.py +171 -0
- alysis_code/hooks/config.py +225 -0
- alysis_code/hooks/dispatcher.py +1110 -0
- alysis_code/hooks/models.py +447 -0
- alysis_code/hooks/trust.py +202 -0
- alysis_code/host_actions.py +543 -0
- alysis_code/host_browser.py +103 -0
- alysis_code/ide/__init__.py +5 -0
- alysis_code/ide/activity_events.py +399 -0
- alysis_code/ide/approvals.py +337 -0
- alysis_code/ide/artifacts.py +153 -0
- alysis_code/ide/browser_egress_proxy.py +1076 -0
- alysis_code/ide/cdp_websocket_transport.py +1192 -0
- alysis_code/ide/change_ledger.py +1721 -0
- alysis_code/ide/context_blocks.py +979 -0
- alysis_code/ide/event_stream.py +531 -0
- alysis_code/ide/forge_protocol.py +3112 -0
- alysis_code/ide/forge_request_ledger.py +737 -0
- alysis_code/ide/health.py +965 -0
- alysis_code/ide/managed_browser.py +2251 -0
- alysis_code/ide/management_protocol.py +3414 -0
- alysis_code/ide/mcp_oauth_coordinator.py +744 -0
- alysis_code/ide/mcp_oauth_lifecycle.py +1504 -0
- alysis_code/ide/prompt_queue.py +1070 -0
- alysis_code/ide/protocol.py +191 -0
- alysis_code/ide/resumable_swarm.py +1543 -0
- alysis_code/ide/session_search.py +295 -0
- alysis_code/ide/stdio_bridge.py +9935 -0
- alysis_code/ide/structured_state.py +1579 -0
- alysis_code/ide/swarm_protocol.py +816 -0
- alysis_code/integration_gate.py +506 -0
- alysis_code/interactive_input_guard.py +39 -0
- alysis_code/interactive_plan_mode.py +26 -0
- alysis_code/internal_artifacts.py +179 -0
- alysis_code/knowledge_base.py +1409 -0
- alysis_code/knowledge_capture.py +1190 -0
- alysis_code/knowledge_librarian.py +605 -0
- alysis_code/language_policy.py +34 -0
- alysis_code/litellm_static_provider.py +535 -0
- alysis_code/llm/__init__.py +1 -0
- alysis_code/llm/anthropic_messages.py +2288 -0
- alysis_code/llm/base.py +71 -0
- alysis_code/llm/cache_capabilities.py +985 -0
- alysis_code/llm/cache_control_blocks.py +244 -0
- alysis_code/llm/cache_policy.py +388 -0
- alysis_code/llm/factory.py +373 -0
- alysis_code/llm/gemini_generate_content.py +2652 -0
- alysis_code/llm/gemini_interactions.py +739 -0
- alysis_code/llm/metadata.py +450 -0
- alysis_code/llm/openai_compat.py +2947 -0
- alysis_code/llm/openai_responses.py +2604 -0
- alysis_code/llm/protocols.py +609 -0
- alysis_code/llm/provider_limits.py +525 -0
- alysis_code/llm/request_plan.py +389 -0
- alysis_code/llm/request_shape.py +238 -0
- alysis_code/llm/streaming.py +108 -0
- alysis_code/llm/temperature_compat.py +78 -0
- alysis_code/llm/types.py +195 -0
- alysis_code/llm/usage_normalization.py +222 -0
- alysis_code/llm_error_display.py +315 -0
- alysis_code/logging_redaction.py +326 -0
- alysis_code/managed_host_deadline.py +196 -0
- alysis_code/mcp/README.md +33 -0
- alysis_code/mcp/__init__.py +24 -0
- alysis_code/mcp/client.py +1137 -0
- alysis_code/mcp/config.py +597 -0
- alysis_code/mcp/errors.py +113 -0
- alysis_code/mcp/forge_scope.py +154 -0
- alysis_code/mcp/jsonrpc.py +214 -0
- alysis_code/mcp/manager.py +2308 -0
- alysis_code/mcp/models.py +666 -0
- alysis_code/mcp/oauth.py +972 -0
- alysis_code/mcp/oauth_runtime.py +310 -0
- alysis_code/mcp/oauth_store.py +276 -0
- alysis_code/mcp/prompts.py +329 -0
- alysis_code/mcp/resources.py +295 -0
- alysis_code/mcp/roots.py +106 -0
- alysis_code/mcp/server_requests.py +75 -0
- alysis_code/mcp/token_store.py +859 -0
- alysis_code/mcp/transport_http.py +1338 -0
- alysis_code/mcp/transport_stdio.py +1267 -0
- alysis_code/mcp/untrusted_content.py +119 -0
- alysis_code/merge_conflict_reviewer.py +729 -0
- alysis_code/model_catalog/__init__.py +1 -0
- alysis_code/model_catalog/chatgpt_codex_subscription_snapshot.json +186 -0
- alysis_code/model_catalog/litellm_model_prices_snapshot.json +44715 -0
- alysis_code/model_catalog/litellm_model_prices_snapshot.meta.json +17 -0
- alysis_code/model_metadata_policy.py +223 -0
- alysis_code/model_metadata_utils.py +103 -0
- alysis_code/model_registry.py +1420 -0
- alysis_code/model_router.py +147 -0
- alysis_code/permission_policy.py +1016 -0
- alysis_code/personas.py +451 -0
- alysis_code/pipeline_facts.py +233 -0
- alysis_code/plan_assistant.py +4763 -0
- alysis_code/plan_mode.py +393 -0
- alysis_code/plan_reconciliation.py +1228 -0
- alysis_code/plan_repair.py +652 -0
- alysis_code/plan_validation.py +1099 -0
- alysis_code/planning_constraints.py +904 -0
- alysis_code/policy.py +95 -0
- alysis_code/preview_server.py +457 -0
- alysis_code/process_reaping.py +566 -0
- alysis_code/profile_presets.py +1834 -0
- alysis_code/profiles.py +666 -0
- alysis_code/provider_auth/__init__.py +29 -0
- alysis_code/provider_auth/base.py +99 -0
- alysis_code/provider_auth/openai_codex.py +951 -0
- alysis_code/provider_auth/registry.py +76 -0
- alysis_code/provider_auth/store.py +125 -0
- alysis_code/provider_diagnostics.py +1209 -0
- alysis_code/provider_model_catalog.py +685 -0
- alysis_code/provider_telemetry.py +1699 -0
- alysis_code/provider_url.py +75 -0
- alysis_code/reasoning_contracts.py +911 -0
- alysis_code/remote_sync.py +350 -0
- alysis_code/replanning.py +1195 -0
- alysis_code/repo_scan.py +1152 -0
- alysis_code/request_estimation.py +296 -0
- alysis_code/review_gate.py +617 -0
- alysis_code/run_lock.py +1141 -0
- alysis_code/run_outcome.py +58 -0
- alysis_code/run_provenance.py +774 -0
- alysis_code/run_state.py +445 -0
- alysis_code/runtime_artifacts.py +116 -0
- alysis_code/runtime_context_features.py +78 -0
- alysis_code/runtime_kind.py +52 -0
- alysis_code/safety/__init__.py +11 -0
- alysis_code/safety/mcp_sanitize.py +29 -0
- alysis_code/safety/safe_http.py +297 -0
- alysis_code/safety/subagent_report.py +184 -0
- alysis_code/sandbox_doctor.py +682 -0
- alysis_code/sandbox_runner.py +1025 -0
- alysis_code/sandbox_settings.py +423 -0
- alysis_code/serialized_paths.py +355 -0
- alysis_code/server/__init__.py +3 -0
- alysis_code/server/app.py +367 -0
- alysis_code/server/auth.py +34 -0
- alysis_code/server/job_config.py +30 -0
- alysis_code/server/settings.py +215 -0
- alysis_code/server/store.py +193 -0
- alysis_code/server/worker_runner.py +657 -0
- alysis_code/service_persistence.py +355 -0
- alysis_code/session_artifacts.py +108 -0
- alysis_code/session_metrics.py +331 -0
- alysis_code/session_store.py +624 -0
- alysis_code/skills/README.md +34 -0
- alysis_code/skills/__init__.py +104 -0
- alysis_code/skills/conventions.py +84 -0
- alysis_code/skills/discovery.py +176 -0
- alysis_code/skills/eval_models.py +232 -0
- alysis_code/skills/eval_runner.py +372 -0
- alysis_code/skills/evals.py +1344 -0
- alysis_code/skills/install.py +293 -0
- alysis_code/skills/loader.py +118 -0
- alysis_code/skills/matching.py +103 -0
- alysis_code/skills/models.py +71 -0
- alysis_code/skills/paths.py +56 -0
- alysis_code/skills/prompting.py +500 -0
- alysis_code/skills/scaffold.py +142 -0
- alysis_code/skills/state.py +441 -0
- alysis_code/skills/transactions.py +125 -0
- alysis_code/skills/validation.py +304 -0
- alysis_code/step_budget.py +238 -0
- alysis_code/subagent_labels.py +49 -0
- alysis_code/subagents.py +1072 -0
- alysis_code/surface/__init__.py +80 -0
- alysis_code/surface/base.py +305 -0
- alysis_code/surface/console.py +387 -0
- alysis_code/surface/events.py +372 -0
- alysis_code/surface/hidden_surface.py +529 -0
- alysis_code/surface/noop_surface.py +219 -0
- alysis_code/surface/rich_surface.py +1555 -0
- alysis_code/surface/styles.py +67 -0
- alysis_code/surface/theme.py +455 -0
- alysis_code/surface/types.py +100 -0
- alysis_code/swarm_backend.py +926 -0
- alysis_code/swarm_orchestrator.py +4020 -0
- alysis_code/swarm_scheduler.py +441 -0
- alysis_code/swarm_trace.py +429 -0
- alysis_code/swarm_worker.py +2119 -0
- alysis_code/swarm_write_guard.py +348 -0
- alysis_code/task_dependencies.py +170 -0
- alysis_code/task_readiness.py +992 -0
- alysis_code/task_scope.py +2148 -0
- alysis_code/terminal_manager.py +762 -0
- alysis_code/terminal_ownership.py +460 -0
- alysis_code/text_normalization.py +30 -0
- alysis_code/token_budget.py +97 -0
- alysis_code/tools/README.md +34 -0
- alysis_code/tools/__init__.py +1 -0
- alysis_code/tools/artifacts.py +127 -0
- alysis_code/tools/availability.py +188 -0
- alysis_code/tools/fs.py +1456 -0
- alysis_code/tools/git.py +461 -0
- alysis_code/tools/history.py +229 -0
- alysis_code/tools/http_timeout.py +78 -0
- alysis_code/tools/image_generation.py +552 -0
- alysis_code/tools/registry.py +2936 -0
- alysis_code/tools/repo_map.py +476 -0
- alysis_code/tools/search.py +563 -0
- alysis_code/tools/shell.py +135 -0
- alysis_code/tools/symbols.py +1350 -0
- alysis_code/tools/test_discovery.py +643 -0
- alysis_code/tools/web.py +482 -0
- alysis_code/tools/web_search.py +2012 -0
- alysis_code/tools/web_search_dashscope.py +557 -0
- alysis_code/tools/web_search_ddgs.py +221 -0
- alysis_code/tools/web_search_provider_adapters.py +1429 -0
- alysis_code/tools/web_search_tavily.py +194 -0
- alysis_code/updates.py +933 -0
- alysis_code/usage_tracker.py +1990 -0
- alysis_code/verification_command_analysis.py +1004 -0
- alysis_code/verification_contract.py +574 -0
- alysis_code/verification_failure_summary.py +273 -0
- alysis_code/verification_repair.py +385 -0
- alysis_code/verify_gate.py +3129 -0
- alysis_code/web_research.py +1872 -0
- alysis_code/web_search_adapters.py +66 -0
- alysis_code/web_search_policy.py +27 -0
- alysis_code/workspace_binding.py +389 -0
- alysis_code/workspace_binding_ui.py +408 -0
- alysis_code/workspace_context.py +273 -0
- alysis_code/workspace_isolation.py +138 -0
- alysis_code/workspace_provisioning.py +455 -0
- alysis_code-0.13.0.dist-info/METADATA +507 -0
- alysis_code-0.13.0.dist-info/RECORD +458 -0
- alysis_code-0.13.0.dist-info/WHEEL +4 -0
- alysis_code-0.13.0.dist-info/entry_points.txt +3 -0
- alysis_code-0.13.0.dist-info/licenses/LICENSE +176 -0
- alysis_code-0.13.0.dist-info/licenses/NOTICE +4 -0
|
@@ -0,0 +1,1137 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import copy
|
|
4
|
+
from collections.abc import Callable, Collection
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Any, Protocol, TypeVar
|
|
8
|
+
from urllib.parse import SplitResult, urlsplit, urlunsplit
|
|
9
|
+
|
|
10
|
+
from .. import __version__
|
|
11
|
+
from ..runtime_kind import RuntimeKind
|
|
12
|
+
from .errors import McpError, McpProtocolError, McpRemoteError
|
|
13
|
+
from .jsonrpc import JsonRpcErrorObject, JsonRpcNotification, JsonRpcResponse
|
|
14
|
+
from .models import ResolvedMcpServer
|
|
15
|
+
from .oauth import McpOAuthError
|
|
16
|
+
from .prompts import (
|
|
17
|
+
McpGetPromptResult,
|
|
18
|
+
McpListedPrompt,
|
|
19
|
+
McpPromptNormalizationError,
|
|
20
|
+
normalize_get_prompt_result,
|
|
21
|
+
normalize_list_prompts_result,
|
|
22
|
+
)
|
|
23
|
+
from .resources import (
|
|
24
|
+
McpListedResource,
|
|
25
|
+
McpReadResourceResult,
|
|
26
|
+
McpResourceNormalizationError,
|
|
27
|
+
normalize_list_resources_result,
|
|
28
|
+
normalize_read_resource_result,
|
|
29
|
+
)
|
|
30
|
+
from .roots import McpRootsRequestHandler
|
|
31
|
+
from .server_requests import McpServerRequestContext, McpServerRequestHandler
|
|
32
|
+
from .transport_http import (
|
|
33
|
+
McpHttpTransport,
|
|
34
|
+
McpHttpTransportError,
|
|
35
|
+
McpHttpTransportSessionExpiredError,
|
|
36
|
+
)
|
|
37
|
+
from .transport_stdio import McpStdioTransport, McpStdioTransportError
|
|
38
|
+
|
|
39
|
+
MCP_PROTOCOL_VERSION = "2024-11-05"
|
|
40
|
+
MCP_STDIO_PROTOCOL_VERSION = MCP_PROTOCOL_VERSION
|
|
41
|
+
MCP_HTTP_PREFERRED_PROTOCOL_VERSION = "2025-11-25"
|
|
42
|
+
MCP_HTTP_SUPPORTED_PROTOCOL_VERSIONS = (
|
|
43
|
+
MCP_HTTP_PREFERRED_PROTOCOL_VERSION,
|
|
44
|
+
"2025-06-18",
|
|
45
|
+
"2025-03-26",
|
|
46
|
+
)
|
|
47
|
+
_SUPPORTED_PROTOCOL_VERSIONS_BY_TRANSPORT = {
|
|
48
|
+
"stdio": (MCP_STDIO_PROTOCOL_VERSION,),
|
|
49
|
+
"http": MCP_HTTP_SUPPORTED_PROTOCOL_VERSIONS,
|
|
50
|
+
}
|
|
51
|
+
# `notifications/initialized` needs a real follow-up window for immediate
|
|
52
|
+
# server activity without turning every quiet startup into a full startup-timeout wait.
|
|
53
|
+
_INITIALIZED_NOTIFICATION_COMPLETION_TIMEOUT_S = 0.5
|
|
54
|
+
_ReadOnlyRequestResult = TypeVar("_ReadOnlyRequestResult")
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class McpClientError(McpError):
|
|
58
|
+
pass
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class McpClientProtocolError(McpClientError, McpProtocolError):
|
|
62
|
+
pass
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class McpClientRemoteError(McpClientError, McpRemoteError):
|
|
66
|
+
pass
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class _McpClientTransport(Protocol):
|
|
70
|
+
@property
|
|
71
|
+
def closed(self) -> bool: ...
|
|
72
|
+
|
|
73
|
+
def request(
|
|
74
|
+
self,
|
|
75
|
+
*,
|
|
76
|
+
method: str,
|
|
77
|
+
params: Any | None,
|
|
78
|
+
timeout_s: float,
|
|
79
|
+
) -> JsonRpcResponse: ...
|
|
80
|
+
|
|
81
|
+
def send_notification(
|
|
82
|
+
self,
|
|
83
|
+
*,
|
|
84
|
+
method: str,
|
|
85
|
+
params: Any | None,
|
|
86
|
+
completion_timeout_s: float | None = None,
|
|
87
|
+
) -> None: ...
|
|
88
|
+
|
|
89
|
+
def drain_notifications(self) -> tuple[JsonRpcNotification, ...]: ...
|
|
90
|
+
|
|
91
|
+
def close(self) -> None: ...
|
|
92
|
+
|
|
93
|
+
def stderr_tail(self) -> str: ...
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def _redacted_server_url(value: str | None) -> str:
|
|
97
|
+
split = urlsplit(str(value or ""))
|
|
98
|
+
hostname = split.hostname or ""
|
|
99
|
+
if split.port is None:
|
|
100
|
+
netloc = hostname
|
|
101
|
+
else:
|
|
102
|
+
netloc = f"{hostname}:{split.port}"
|
|
103
|
+
redacted = SplitResult(
|
|
104
|
+
scheme=split.scheme,
|
|
105
|
+
netloc=netloc,
|
|
106
|
+
path=split.path or "/",
|
|
107
|
+
query="",
|
|
108
|
+
fragment="",
|
|
109
|
+
)
|
|
110
|
+
return urlunsplit(redacted)
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def _server_context(server: ResolvedMcpServer) -> str:
|
|
114
|
+
if server.transport == "http":
|
|
115
|
+
return f"MCP server '{server.id}' (url '{_redacted_server_url(server.url)}')"
|
|
116
|
+
return f"MCP server '{server.id}' (command '{server.command}')"
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def _build_client_error(
|
|
120
|
+
*,
|
|
121
|
+
server: ResolvedMcpServer,
|
|
122
|
+
message: str,
|
|
123
|
+
stderr_tail: str = "",
|
|
124
|
+
error_type: type[McpClientError] = McpClientError,
|
|
125
|
+
) -> McpClientError:
|
|
126
|
+
full_message = f"{_server_context(server)}: {message}"
|
|
127
|
+
cleaned_tail = str(stderr_tail or "").strip()
|
|
128
|
+
if cleaned_tail:
|
|
129
|
+
full_message += f"\nstderr tail:\n{cleaned_tail}"
|
|
130
|
+
return error_type(full_message)
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def _initial_protocol_version_for_transport(transport: str) -> str:
|
|
134
|
+
if transport == "http":
|
|
135
|
+
return MCP_HTTP_PREFERRED_PROTOCOL_VERSION
|
|
136
|
+
return MCP_STDIO_PROTOCOL_VERSION
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def _supported_protocol_versions_for_transport(transport: str) -> tuple[str, ...]:
|
|
140
|
+
return tuple(_SUPPORTED_PROTOCOL_VERSIONS_BY_TRANSPORT.get(transport, ()))
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def _validate_protocol_version(
|
|
144
|
+
*,
|
|
145
|
+
server: ResolvedMcpServer,
|
|
146
|
+
transport: str,
|
|
147
|
+
value: Any,
|
|
148
|
+
stderr_tail: str,
|
|
149
|
+
) -> str:
|
|
150
|
+
protocol_version = _require_string(
|
|
151
|
+
value,
|
|
152
|
+
server=server,
|
|
153
|
+
context="initialize.protocolVersion",
|
|
154
|
+
)
|
|
155
|
+
supported_versions = _supported_protocol_versions_for_transport(transport)
|
|
156
|
+
if protocol_version in supported_versions:
|
|
157
|
+
return protocol_version
|
|
158
|
+
if not supported_versions:
|
|
159
|
+
supported_text = "no supported versions configured"
|
|
160
|
+
elif len(supported_versions) == 1:
|
|
161
|
+
supported_text = repr(supported_versions[0])
|
|
162
|
+
else:
|
|
163
|
+
supported_text = ", ".join(repr(item) for item in supported_versions)
|
|
164
|
+
raise _build_client_error(
|
|
165
|
+
server=server,
|
|
166
|
+
message=(
|
|
167
|
+
"server reported unsupported protocol version "
|
|
168
|
+
f"{protocol_version!r}; expected one of {supported_text}."
|
|
169
|
+
),
|
|
170
|
+
stderr_tail=stderr_tail,
|
|
171
|
+
error_type=McpClientProtocolError,
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def _require_object(
|
|
176
|
+
value: Any,
|
|
177
|
+
*,
|
|
178
|
+
server: ResolvedMcpServer,
|
|
179
|
+
context: str,
|
|
180
|
+
) -> dict[str, Any]:
|
|
181
|
+
if not isinstance(value, dict):
|
|
182
|
+
raise _build_client_error(
|
|
183
|
+
server=server,
|
|
184
|
+
message=f"{context} must be an object.",
|
|
185
|
+
error_type=McpClientProtocolError,
|
|
186
|
+
)
|
|
187
|
+
return value
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def _require_string(
|
|
191
|
+
value: Any,
|
|
192
|
+
*,
|
|
193
|
+
server: ResolvedMcpServer,
|
|
194
|
+
context: str,
|
|
195
|
+
) -> str:
|
|
196
|
+
if not isinstance(value, str):
|
|
197
|
+
raise _build_client_error(
|
|
198
|
+
server=server,
|
|
199
|
+
message=f"{context} must be a string.",
|
|
200
|
+
error_type=McpClientProtocolError,
|
|
201
|
+
)
|
|
202
|
+
cleaned = value.strip()
|
|
203
|
+
if not cleaned:
|
|
204
|
+
raise _build_client_error(
|
|
205
|
+
server=server,
|
|
206
|
+
message=f"{context} cannot be empty.",
|
|
207
|
+
error_type=McpClientProtocolError,
|
|
208
|
+
)
|
|
209
|
+
return cleaned
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
@dataclass(frozen=True)
|
|
213
|
+
class McpListedTool:
|
|
214
|
+
name: str
|
|
215
|
+
description: str
|
|
216
|
+
input_schema: dict[str, Any]
|
|
217
|
+
raw_payload: dict[str, Any] = field(repr=False)
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
@dataclass(frozen=True)
|
|
221
|
+
class McpToolCallResult:
|
|
222
|
+
is_error: bool
|
|
223
|
+
structured_content: Any | None
|
|
224
|
+
content: tuple[dict[str, Any], ...]
|
|
225
|
+
content_summary: str
|
|
226
|
+
extracted_text: str
|
|
227
|
+
raw_payload: dict[str, Any] = field(repr=False)
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
class _BaseMcpClient:
|
|
231
|
+
transport: _McpClientTransport
|
|
232
|
+
_transport_error_types: tuple[type[BaseException], ...]
|
|
233
|
+
|
|
234
|
+
def __init__(
|
|
235
|
+
self,
|
|
236
|
+
*,
|
|
237
|
+
server: ResolvedMcpServer,
|
|
238
|
+
workspace_root: Path,
|
|
239
|
+
transport: _McpClientTransport,
|
|
240
|
+
transport_error_types: tuple[type[BaseException], ...],
|
|
241
|
+
runtime_kind: RuntimeKind,
|
|
242
|
+
server_request_context: McpServerRequestContext,
|
|
243
|
+
server_request_handler: McpServerRequestHandler,
|
|
244
|
+
) -> None:
|
|
245
|
+
self.server = server
|
|
246
|
+
self.workspace_root = workspace_root.resolve()
|
|
247
|
+
self.transport = transport
|
|
248
|
+
self._transport_error_types = transport_error_types
|
|
249
|
+
self.runtime_kind = runtime_kind
|
|
250
|
+
self._server_request_context = server_request_context
|
|
251
|
+
self._server_request_handler = server_request_handler
|
|
252
|
+
self._client_capabilities = copy.deepcopy(
|
|
253
|
+
server_request_handler.client_capabilities(context=server_request_context)
|
|
254
|
+
)
|
|
255
|
+
self._initialized = False
|
|
256
|
+
self._supports_tools = False
|
|
257
|
+
self._supports_resources = False
|
|
258
|
+
self._supports_prompts = False
|
|
259
|
+
self._server_info: dict[str, Any] = {}
|
|
260
|
+
self._server_capabilities: dict[str, Any] = {}
|
|
261
|
+
self._tools_list_changed = False
|
|
262
|
+
self._resources_list_changed = False
|
|
263
|
+
self._prompts_list_changed = False
|
|
264
|
+
self._negotiated_protocol_version: str | None = None
|
|
265
|
+
|
|
266
|
+
@property
|
|
267
|
+
def closed(self) -> bool:
|
|
268
|
+
return self.transport.closed
|
|
269
|
+
|
|
270
|
+
@property
|
|
271
|
+
def tools_list_changed(self) -> bool:
|
|
272
|
+
return self._tools_list_changed
|
|
273
|
+
|
|
274
|
+
@property
|
|
275
|
+
def resources_list_changed(self) -> bool:
|
|
276
|
+
return self._resources_list_changed
|
|
277
|
+
|
|
278
|
+
@property
|
|
279
|
+
def prompts_list_changed(self) -> bool:
|
|
280
|
+
return self._prompts_list_changed
|
|
281
|
+
|
|
282
|
+
@property
|
|
283
|
+
def server_info(self) -> dict[str, Any]:
|
|
284
|
+
return dict(self._server_info)
|
|
285
|
+
|
|
286
|
+
@property
|
|
287
|
+
def server_capabilities(self) -> dict[str, Any]:
|
|
288
|
+
return copy.deepcopy(self._server_capabilities)
|
|
289
|
+
|
|
290
|
+
@property
|
|
291
|
+
def negotiated_protocol_version(self) -> str | None:
|
|
292
|
+
return self._negotiated_protocol_version
|
|
293
|
+
|
|
294
|
+
@property
|
|
295
|
+
def session_negotiated(self) -> bool:
|
|
296
|
+
return False
|
|
297
|
+
|
|
298
|
+
@property
|
|
299
|
+
def roots_capability_enabled(self) -> bool:
|
|
300
|
+
return isinstance(self._client_capabilities.get("roots"), dict)
|
|
301
|
+
|
|
302
|
+
@property
|
|
303
|
+
def supports_tools(self) -> bool:
|
|
304
|
+
return self._supports_tools
|
|
305
|
+
|
|
306
|
+
@property
|
|
307
|
+
def supports_resources(self) -> bool:
|
|
308
|
+
return self._supports_resources
|
|
309
|
+
|
|
310
|
+
@property
|
|
311
|
+
def supports_prompts(self) -> bool:
|
|
312
|
+
return self._supports_prompts
|
|
313
|
+
|
|
314
|
+
def _diagnostic_tail(self) -> str:
|
|
315
|
+
return self.transport.stderr_tail()
|
|
316
|
+
|
|
317
|
+
def _drain_notifications(self) -> tuple[JsonRpcNotification, ...]:
|
|
318
|
+
notifications = self.transport.drain_notifications()
|
|
319
|
+
for notification in notifications:
|
|
320
|
+
if notification.method == "notifications/tools/list_changed":
|
|
321
|
+
self._tools_list_changed = True
|
|
322
|
+
elif notification.method == "notifications/resources/list_changed":
|
|
323
|
+
self._resources_list_changed = True
|
|
324
|
+
elif notification.method == "notifications/prompts/list_changed":
|
|
325
|
+
self._prompts_list_changed = True
|
|
326
|
+
return notifications
|
|
327
|
+
|
|
328
|
+
def observe_notifications(self) -> None:
|
|
329
|
+
self._drain_notifications()
|
|
330
|
+
|
|
331
|
+
def _raise_remote_error(
|
|
332
|
+
self,
|
|
333
|
+
*,
|
|
334
|
+
method: str,
|
|
335
|
+
error: JsonRpcErrorObject,
|
|
336
|
+
) -> None:
|
|
337
|
+
raise _build_client_error(
|
|
338
|
+
server=self.server,
|
|
339
|
+
message=(f"remote JSON-RPC error during '{method}': {error.code} {error.message}"),
|
|
340
|
+
stderr_tail=self._diagnostic_tail(),
|
|
341
|
+
error_type=McpClientRemoteError,
|
|
342
|
+
)
|
|
343
|
+
|
|
344
|
+
def _response_result_object(
|
|
345
|
+
self,
|
|
346
|
+
*,
|
|
347
|
+
method: str,
|
|
348
|
+
response: JsonRpcResponse,
|
|
349
|
+
) -> dict[str, Any]:
|
|
350
|
+
if response.error is not None:
|
|
351
|
+
self._raise_remote_error(method=method, error=response.error)
|
|
352
|
+
return _require_object(
|
|
353
|
+
response.result,
|
|
354
|
+
server=self.server,
|
|
355
|
+
context=f"'{method}' result",
|
|
356
|
+
)
|
|
357
|
+
|
|
358
|
+
def _reset_initialization_state(self) -> None:
|
|
359
|
+
self._initialized = False
|
|
360
|
+
self._supports_tools = False
|
|
361
|
+
self._supports_resources = False
|
|
362
|
+
self._supports_prompts = False
|
|
363
|
+
self._server_info = {}
|
|
364
|
+
self._server_capabilities = {}
|
|
365
|
+
self._negotiated_protocol_version = None
|
|
366
|
+
|
|
367
|
+
def _apply_negotiated_protocol_version(self, protocol_version: str) -> None:
|
|
368
|
+
self._negotiated_protocol_version = protocol_version
|
|
369
|
+
|
|
370
|
+
def _initialize_once(self) -> None:
|
|
371
|
+
response = self.transport.request(
|
|
372
|
+
method="initialize",
|
|
373
|
+
params={
|
|
374
|
+
"protocolVersion": _initial_protocol_version_for_transport(self.server.transport),
|
|
375
|
+
"capabilities": copy.deepcopy(self._client_capabilities),
|
|
376
|
+
"clientInfo": {
|
|
377
|
+
"name": "alysis-code",
|
|
378
|
+
"version": __version__,
|
|
379
|
+
},
|
|
380
|
+
},
|
|
381
|
+
timeout_s=self.server.startup_timeout_s,
|
|
382
|
+
)
|
|
383
|
+
result = self._response_result_object(method="initialize", response=response)
|
|
384
|
+
protocol_version = _validate_protocol_version(
|
|
385
|
+
server=self.server,
|
|
386
|
+
transport=self.server.transport,
|
|
387
|
+
value=result.get("protocolVersion"),
|
|
388
|
+
stderr_tail=self._diagnostic_tail(),
|
|
389
|
+
)
|
|
390
|
+
capabilities = _require_object(
|
|
391
|
+
result.get("capabilities"),
|
|
392
|
+
server=self.server,
|
|
393
|
+
context="initialize.capabilities",
|
|
394
|
+
)
|
|
395
|
+
tools_capability = capabilities.get("tools")
|
|
396
|
+
if tools_capability is not None and not isinstance(tools_capability, dict):
|
|
397
|
+
raise _build_client_error(
|
|
398
|
+
server=self.server,
|
|
399
|
+
message="initialize.capabilities.tools must be an object when present.",
|
|
400
|
+
stderr_tail=self._diagnostic_tail(),
|
|
401
|
+
error_type=McpClientProtocolError,
|
|
402
|
+
)
|
|
403
|
+
resources_capability = capabilities.get("resources")
|
|
404
|
+
if resources_capability is not None and not isinstance(resources_capability, dict):
|
|
405
|
+
raise _build_client_error(
|
|
406
|
+
server=self.server,
|
|
407
|
+
message="initialize.capabilities.resources must be an object when present.",
|
|
408
|
+
stderr_tail=self._diagnostic_tail(),
|
|
409
|
+
error_type=McpClientProtocolError,
|
|
410
|
+
)
|
|
411
|
+
prompts_capability = capabilities.get("prompts")
|
|
412
|
+
if prompts_capability is not None and not isinstance(prompts_capability, dict):
|
|
413
|
+
raise _build_client_error(
|
|
414
|
+
server=self.server,
|
|
415
|
+
message="initialize.capabilities.prompts must be an object when present.",
|
|
416
|
+
stderr_tail=self._diagnostic_tail(),
|
|
417
|
+
error_type=McpClientProtocolError,
|
|
418
|
+
)
|
|
419
|
+
server_info = result.get("serverInfo") or {}
|
|
420
|
+
if not isinstance(server_info, dict):
|
|
421
|
+
raise _build_client_error(
|
|
422
|
+
server=self.server,
|
|
423
|
+
message="initialize.serverInfo must be an object when present.",
|
|
424
|
+
stderr_tail=self._diagnostic_tail(),
|
|
425
|
+
error_type=McpClientProtocolError,
|
|
426
|
+
)
|
|
427
|
+
self._server_info = copy.deepcopy(server_info)
|
|
428
|
+
self._server_capabilities = copy.deepcopy(capabilities)
|
|
429
|
+
self._supports_tools = "tools" in capabilities
|
|
430
|
+
self._supports_resources = "resources" in capabilities
|
|
431
|
+
self._supports_prompts = "prompts" in capabilities
|
|
432
|
+
self._apply_negotiated_protocol_version(protocol_version)
|
|
433
|
+
self.transport.send_notification(
|
|
434
|
+
method="notifications/initialized",
|
|
435
|
+
params={},
|
|
436
|
+
completion_timeout_s=min(
|
|
437
|
+
self.server.startup_timeout_s,
|
|
438
|
+
_INITIALIZED_NOTIFICATION_COMPLETION_TIMEOUT_S,
|
|
439
|
+
),
|
|
440
|
+
)
|
|
441
|
+
self._initialized = True
|
|
442
|
+
self._drain_notifications()
|
|
443
|
+
|
|
444
|
+
def ensure_initialized(self) -> None:
|
|
445
|
+
if self._initialized:
|
|
446
|
+
self._drain_notifications()
|
|
447
|
+
return
|
|
448
|
+
try:
|
|
449
|
+
self._initialize_once()
|
|
450
|
+
except Exception as exc: # noqa: BLE001
|
|
451
|
+
if isinstance(exc, (McpClientError, *self._transport_error_types)):
|
|
452
|
+
self.close()
|
|
453
|
+
raise
|
|
454
|
+
self.close()
|
|
455
|
+
raise _build_client_error(
|
|
456
|
+
server=self.server,
|
|
457
|
+
message=f"initialize failed: {exc}",
|
|
458
|
+
stderr_tail=self._diagnostic_tail(),
|
|
459
|
+
) from exc
|
|
460
|
+
|
|
461
|
+
def _require_tools_capability(self, *, method: str) -> None:
|
|
462
|
+
self.ensure_initialized()
|
|
463
|
+
if self._supports_tools:
|
|
464
|
+
return
|
|
465
|
+
raise _build_client_error(
|
|
466
|
+
server=self.server,
|
|
467
|
+
message=f"server does not advertise tools capability for '{method}'.",
|
|
468
|
+
stderr_tail=self._diagnostic_tail(),
|
|
469
|
+
error_type=McpClientProtocolError,
|
|
470
|
+
)
|
|
471
|
+
|
|
472
|
+
def _require_resources_capability(self, *, method: str) -> None:
|
|
473
|
+
self.ensure_initialized()
|
|
474
|
+
if self._supports_resources:
|
|
475
|
+
return
|
|
476
|
+
raise _build_client_error(
|
|
477
|
+
server=self.server,
|
|
478
|
+
message=f"server does not advertise resources capability for '{method}'.",
|
|
479
|
+
stderr_tail=self._diagnostic_tail(),
|
|
480
|
+
error_type=McpClientProtocolError,
|
|
481
|
+
)
|
|
482
|
+
|
|
483
|
+
def _require_prompts_capability(self, *, method: str) -> None:
|
|
484
|
+
self.ensure_initialized()
|
|
485
|
+
if self._supports_prompts:
|
|
486
|
+
return
|
|
487
|
+
raise _build_client_error(
|
|
488
|
+
server=self.server,
|
|
489
|
+
message=f"server does not advertise prompts capability for '{method}'.",
|
|
490
|
+
stderr_tail=self._diagnostic_tail(),
|
|
491
|
+
error_type=McpClientProtocolError,
|
|
492
|
+
)
|
|
493
|
+
|
|
494
|
+
def _list_tools_once(self) -> tuple[McpListedTool, ...]:
|
|
495
|
+
tools: list[McpListedTool] = []
|
|
496
|
+
seen_cursors: set[str] = set()
|
|
497
|
+
next_cursor: str | None = None
|
|
498
|
+
while True:
|
|
499
|
+
params: dict[str, Any] = {}
|
|
500
|
+
if next_cursor is not None:
|
|
501
|
+
params["cursor"] = next_cursor
|
|
502
|
+
response = self.transport.request(
|
|
503
|
+
method="tools/list",
|
|
504
|
+
params=params,
|
|
505
|
+
timeout_s=self.server.call_timeout_s,
|
|
506
|
+
)
|
|
507
|
+
result = self._response_result_object(method="tools/list", response=response)
|
|
508
|
+
page_tools = result.get("tools")
|
|
509
|
+
if not isinstance(page_tools, list):
|
|
510
|
+
raise _build_client_error(
|
|
511
|
+
server=self.server,
|
|
512
|
+
message="tools/list result.tools must be an array.",
|
|
513
|
+
stderr_tail=self._diagnostic_tail(),
|
|
514
|
+
error_type=McpClientProtocolError,
|
|
515
|
+
)
|
|
516
|
+
for raw_tool in page_tools:
|
|
517
|
+
payload = _require_object(
|
|
518
|
+
raw_tool,
|
|
519
|
+
server=self.server,
|
|
520
|
+
context="tools/list tool entry",
|
|
521
|
+
)
|
|
522
|
+
tools.append(
|
|
523
|
+
McpListedTool(
|
|
524
|
+
name=_require_string(
|
|
525
|
+
payload.get("name"),
|
|
526
|
+
server=self.server,
|
|
527
|
+
context="tools/list tool.name",
|
|
528
|
+
),
|
|
529
|
+
description=str(payload.get("description") or "").strip(),
|
|
530
|
+
input_schema=_require_object(
|
|
531
|
+
payload.get("inputSchema"),
|
|
532
|
+
server=self.server,
|
|
533
|
+
context="tools/list tool.inputSchema",
|
|
534
|
+
),
|
|
535
|
+
raw_payload=copy.deepcopy(payload),
|
|
536
|
+
)
|
|
537
|
+
)
|
|
538
|
+
raw_cursor = result.get("nextCursor")
|
|
539
|
+
if raw_cursor is None:
|
|
540
|
+
break
|
|
541
|
+
next_cursor = _require_string(
|
|
542
|
+
raw_cursor,
|
|
543
|
+
server=self.server,
|
|
544
|
+
context="tools/list nextCursor",
|
|
545
|
+
)
|
|
546
|
+
if next_cursor in seen_cursors:
|
|
547
|
+
raise _build_client_error(
|
|
548
|
+
server=self.server,
|
|
549
|
+
message=f"tools/list returned repeated nextCursor {next_cursor!r}.",
|
|
550
|
+
stderr_tail=self._diagnostic_tail(),
|
|
551
|
+
error_type=McpClientProtocolError,
|
|
552
|
+
)
|
|
553
|
+
seen_cursors.add(next_cursor)
|
|
554
|
+
self._drain_notifications()
|
|
555
|
+
return tuple(tools)
|
|
556
|
+
|
|
557
|
+
def list_tools(self) -> tuple[McpListedTool, ...]:
|
|
558
|
+
self._require_tools_capability(method="tools/list")
|
|
559
|
+
return self._list_tools_once()
|
|
560
|
+
|
|
561
|
+
def _list_resources_once(self) -> tuple[McpListedResource, ...]:
|
|
562
|
+
resources: list[McpListedResource] = []
|
|
563
|
+
seen_cursors: set[str] = set()
|
|
564
|
+
seen_uris: set[str] = set()
|
|
565
|
+
next_cursor: str | None = None
|
|
566
|
+
while True:
|
|
567
|
+
params: dict[str, Any] = {}
|
|
568
|
+
if next_cursor is not None:
|
|
569
|
+
params["cursor"] = next_cursor
|
|
570
|
+
response = self.transport.request(
|
|
571
|
+
method="resources/list",
|
|
572
|
+
params=params,
|
|
573
|
+
timeout_s=self.server.call_timeout_s,
|
|
574
|
+
)
|
|
575
|
+
result = self._response_result_object(method="resources/list", response=response)
|
|
576
|
+
try:
|
|
577
|
+
page_resources, raw_cursor = normalize_list_resources_result(result)
|
|
578
|
+
except McpResourceNormalizationError as exc:
|
|
579
|
+
raise _build_client_error(
|
|
580
|
+
server=self.server,
|
|
581
|
+
message=str(exc),
|
|
582
|
+
stderr_tail=self._diagnostic_tail(),
|
|
583
|
+
error_type=McpClientProtocolError,
|
|
584
|
+
) from exc
|
|
585
|
+
for resource in page_resources:
|
|
586
|
+
if resource.uri in seen_uris:
|
|
587
|
+
raise _build_client_error(
|
|
588
|
+
server=self.server,
|
|
589
|
+
message=f"resources/list returned repeated resource URI {resource.uri!r}.",
|
|
590
|
+
stderr_tail=self._diagnostic_tail(),
|
|
591
|
+
error_type=McpClientProtocolError,
|
|
592
|
+
)
|
|
593
|
+
seen_uris.add(resource.uri)
|
|
594
|
+
resources.append(resource)
|
|
595
|
+
if raw_cursor is None:
|
|
596
|
+
break
|
|
597
|
+
if raw_cursor in seen_cursors:
|
|
598
|
+
raise _build_client_error(
|
|
599
|
+
server=self.server,
|
|
600
|
+
message=f"resources/list returned repeated nextCursor {raw_cursor!r}.",
|
|
601
|
+
stderr_tail=self._diagnostic_tail(),
|
|
602
|
+
error_type=McpClientProtocolError,
|
|
603
|
+
)
|
|
604
|
+
seen_cursors.add(raw_cursor)
|
|
605
|
+
next_cursor = raw_cursor
|
|
606
|
+
self._drain_notifications()
|
|
607
|
+
return tuple(resources)
|
|
608
|
+
|
|
609
|
+
def list_resources(self) -> tuple[McpListedResource, ...]:
|
|
610
|
+
self._require_resources_capability(method="resources/list")
|
|
611
|
+
return self._list_resources_once()
|
|
612
|
+
|
|
613
|
+
def _list_prompts_once(self) -> tuple[McpListedPrompt, ...]:
|
|
614
|
+
prompts: list[McpListedPrompt] = []
|
|
615
|
+
seen_cursors: set[str] = set()
|
|
616
|
+
seen_names: set[str] = set()
|
|
617
|
+
next_cursor: str | None = None
|
|
618
|
+
while True:
|
|
619
|
+
params: dict[str, Any] = {}
|
|
620
|
+
if next_cursor is not None:
|
|
621
|
+
params["cursor"] = next_cursor
|
|
622
|
+
response = self.transport.request(
|
|
623
|
+
method="prompts/list",
|
|
624
|
+
params=params,
|
|
625
|
+
timeout_s=self.server.call_timeout_s,
|
|
626
|
+
)
|
|
627
|
+
result = self._response_result_object(method="prompts/list", response=response)
|
|
628
|
+
try:
|
|
629
|
+
page_prompts, raw_cursor = normalize_list_prompts_result(result)
|
|
630
|
+
except McpPromptNormalizationError as exc:
|
|
631
|
+
raise _build_client_error(
|
|
632
|
+
server=self.server,
|
|
633
|
+
message=str(exc),
|
|
634
|
+
stderr_tail=self._diagnostic_tail(),
|
|
635
|
+
error_type=McpClientProtocolError,
|
|
636
|
+
) from exc
|
|
637
|
+
for prompt in page_prompts:
|
|
638
|
+
name_key = prompt.name.casefold()
|
|
639
|
+
if name_key in seen_names:
|
|
640
|
+
raise _build_client_error(
|
|
641
|
+
server=self.server,
|
|
642
|
+
message=f"prompts/list returned repeated prompt name {prompt.name!r}.",
|
|
643
|
+
stderr_tail=self._diagnostic_tail(),
|
|
644
|
+
error_type=McpClientProtocolError,
|
|
645
|
+
)
|
|
646
|
+
seen_names.add(name_key)
|
|
647
|
+
prompts.append(prompt)
|
|
648
|
+
if raw_cursor is None:
|
|
649
|
+
break
|
|
650
|
+
if raw_cursor in seen_cursors:
|
|
651
|
+
raise _build_client_error(
|
|
652
|
+
server=self.server,
|
|
653
|
+
message=f"prompts/list returned repeated nextCursor {raw_cursor!r}.",
|
|
654
|
+
stderr_tail=self._diagnostic_tail(),
|
|
655
|
+
error_type=McpClientProtocolError,
|
|
656
|
+
)
|
|
657
|
+
seen_cursors.add(raw_cursor)
|
|
658
|
+
next_cursor = raw_cursor
|
|
659
|
+
self._drain_notifications()
|
|
660
|
+
return tuple(prompts)
|
|
661
|
+
|
|
662
|
+
def list_prompts(self) -> tuple[McpListedPrompt, ...]:
|
|
663
|
+
self._require_prompts_capability(method="prompts/list")
|
|
664
|
+
return self._list_prompts_once()
|
|
665
|
+
|
|
666
|
+
def _normalize_prompt_arguments(
|
|
667
|
+
self,
|
|
668
|
+
*,
|
|
669
|
+
arguments: dict[str, str] | None,
|
|
670
|
+
) -> dict[str, str] | None:
|
|
671
|
+
if arguments is None:
|
|
672
|
+
return None
|
|
673
|
+
if not isinstance(arguments, dict):
|
|
674
|
+
raise _build_client_error(
|
|
675
|
+
server=self.server,
|
|
676
|
+
message="prompts/get arguments must be an object mapping strings to strings.",
|
|
677
|
+
error_type=McpClientProtocolError,
|
|
678
|
+
)
|
|
679
|
+
normalized: dict[str, str] = {}
|
|
680
|
+
for raw_key, raw_value in arguments.items():
|
|
681
|
+
key = _require_string(
|
|
682
|
+
raw_key,
|
|
683
|
+
server=self.server,
|
|
684
|
+
context="prompts/get argument name",
|
|
685
|
+
)
|
|
686
|
+
if not isinstance(raw_value, str):
|
|
687
|
+
raise _build_client_error(
|
|
688
|
+
server=self.server,
|
|
689
|
+
message="prompts/get argument values must be strings.",
|
|
690
|
+
error_type=McpClientProtocolError,
|
|
691
|
+
)
|
|
692
|
+
normalized[key] = raw_value
|
|
693
|
+
return normalized
|
|
694
|
+
|
|
695
|
+
def _get_prompt_once(
|
|
696
|
+
self,
|
|
697
|
+
*,
|
|
698
|
+
name: str,
|
|
699
|
+
arguments: dict[str, str] | None,
|
|
700
|
+
) -> McpGetPromptResult:
|
|
701
|
+
params: dict[str, Any] = {"name": name}
|
|
702
|
+
normalized_arguments = self._normalize_prompt_arguments(arguments=arguments)
|
|
703
|
+
if normalized_arguments:
|
|
704
|
+
params["arguments"] = normalized_arguments
|
|
705
|
+
response = self.transport.request(
|
|
706
|
+
method="prompts/get",
|
|
707
|
+
params=params,
|
|
708
|
+
timeout_s=self.server.call_timeout_s,
|
|
709
|
+
)
|
|
710
|
+
result = self._response_result_object(method="prompts/get", response=response)
|
|
711
|
+
try:
|
|
712
|
+
normalized = normalize_get_prompt_result(result, expected_name=name)
|
|
713
|
+
except McpPromptNormalizationError as exc:
|
|
714
|
+
raise _build_client_error(
|
|
715
|
+
server=self.server,
|
|
716
|
+
message=str(exc),
|
|
717
|
+
stderr_tail=self._diagnostic_tail(),
|
|
718
|
+
error_type=McpClientProtocolError,
|
|
719
|
+
) from exc
|
|
720
|
+
self._drain_notifications()
|
|
721
|
+
return normalized
|
|
722
|
+
|
|
723
|
+
def get_prompt(
|
|
724
|
+
self,
|
|
725
|
+
*,
|
|
726
|
+
name: str,
|
|
727
|
+
arguments: dict[str, str] | None = None,
|
|
728
|
+
) -> McpGetPromptResult:
|
|
729
|
+
self._require_prompts_capability(method="prompts/get")
|
|
730
|
+
normalized_name = _require_string(
|
|
731
|
+
name,
|
|
732
|
+
server=self.server,
|
|
733
|
+
context="prompts/get name",
|
|
734
|
+
)
|
|
735
|
+
return self._get_prompt_once(name=normalized_name, arguments=arguments)
|
|
736
|
+
|
|
737
|
+
def _read_resource_once(
|
|
738
|
+
self,
|
|
739
|
+
*,
|
|
740
|
+
resource_uri: str,
|
|
741
|
+
allowed_uris: Collection[str] | None = None,
|
|
742
|
+
) -> McpReadResourceResult:
|
|
743
|
+
if allowed_uris is None:
|
|
744
|
+
raise _build_client_error(
|
|
745
|
+
server=self.server,
|
|
746
|
+
message=(
|
|
747
|
+
"resources/read requires a frozen resource snapshot allowlist from "
|
|
748
|
+
"resources/list; arbitrary URI reads are blocked."
|
|
749
|
+
),
|
|
750
|
+
error_type=McpClientError,
|
|
751
|
+
)
|
|
752
|
+
if isinstance(allowed_uris, str):
|
|
753
|
+
raise _build_client_error(
|
|
754
|
+
server=self.server,
|
|
755
|
+
message="resources/read allowed_uris must be a collection of URIs, not a string.",
|
|
756
|
+
error_type=McpClientProtocolError,
|
|
757
|
+
)
|
|
758
|
+
if not isinstance(allowed_uris, Collection):
|
|
759
|
+
raise _build_client_error(
|
|
760
|
+
server=self.server,
|
|
761
|
+
message="resources/read allowed_uris must be a collection of URI strings.",
|
|
762
|
+
error_type=McpClientProtocolError,
|
|
763
|
+
)
|
|
764
|
+
normalized_allowed_uris = {
|
|
765
|
+
_require_string(
|
|
766
|
+
item,
|
|
767
|
+
server=self.server,
|
|
768
|
+
context="resources/read allowed URI",
|
|
769
|
+
)
|
|
770
|
+
for item in allowed_uris
|
|
771
|
+
}
|
|
772
|
+
if resource_uri not in normalized_allowed_uris:
|
|
773
|
+
raise _build_client_error(
|
|
774
|
+
server=self.server,
|
|
775
|
+
message=(
|
|
776
|
+
"resources/read is limited to the frozen resource snapshot for this session; "
|
|
777
|
+
f"URI {resource_uri!r} was not snapshotted."
|
|
778
|
+
),
|
|
779
|
+
error_type=McpClientError,
|
|
780
|
+
)
|
|
781
|
+
response = self.transport.request(
|
|
782
|
+
method="resources/read",
|
|
783
|
+
params={"uri": resource_uri},
|
|
784
|
+
timeout_s=self.server.call_timeout_s,
|
|
785
|
+
)
|
|
786
|
+
result = self._response_result_object(method="resources/read", response=response)
|
|
787
|
+
try:
|
|
788
|
+
normalized = normalize_read_resource_result(result, expected_uri=resource_uri)
|
|
789
|
+
except McpResourceNormalizationError as exc:
|
|
790
|
+
raise _build_client_error(
|
|
791
|
+
server=self.server,
|
|
792
|
+
message=str(exc),
|
|
793
|
+
stderr_tail=self._diagnostic_tail(),
|
|
794
|
+
error_type=McpClientProtocolError,
|
|
795
|
+
) from exc
|
|
796
|
+
self._drain_notifications()
|
|
797
|
+
return normalized
|
|
798
|
+
|
|
799
|
+
def read_resource(
|
|
800
|
+
self,
|
|
801
|
+
*,
|
|
802
|
+
resource_uri: str,
|
|
803
|
+
allowed_uris: Collection[str] | None = None,
|
|
804
|
+
) -> McpReadResourceResult:
|
|
805
|
+
self._require_resources_capability(method="resources/read")
|
|
806
|
+
normalized_uri = _require_string(
|
|
807
|
+
resource_uri,
|
|
808
|
+
server=self.server,
|
|
809
|
+
context="resources/read uri",
|
|
810
|
+
)
|
|
811
|
+
return self._read_resource_once(resource_uri=normalized_uri, allowed_uris=allowed_uris)
|
|
812
|
+
|
|
813
|
+
def _call_tool_once(
|
|
814
|
+
self,
|
|
815
|
+
*,
|
|
816
|
+
tool_name: str,
|
|
817
|
+
arguments: dict[str, Any],
|
|
818
|
+
) -> McpToolCallResult:
|
|
819
|
+
response = self.transport.request(
|
|
820
|
+
method="tools/call",
|
|
821
|
+
params={
|
|
822
|
+
"name": tool_name,
|
|
823
|
+
"arguments": arguments,
|
|
824
|
+
},
|
|
825
|
+
timeout_s=self.server.call_timeout_s,
|
|
826
|
+
)
|
|
827
|
+
result = self._response_result_object(method="tools/call", response=response)
|
|
828
|
+
content = result.get("content")
|
|
829
|
+
if content is None:
|
|
830
|
+
content = []
|
|
831
|
+
if not isinstance(content, list):
|
|
832
|
+
raise _build_client_error(
|
|
833
|
+
server=self.server,
|
|
834
|
+
message="tools/call result.content must be an array when present.",
|
|
835
|
+
stderr_tail=self._diagnostic_tail(),
|
|
836
|
+
error_type=McpClientProtocolError,
|
|
837
|
+
)
|
|
838
|
+
is_error = bool(result.get("isError", False))
|
|
839
|
+
if "isError" in result and not isinstance(result.get("isError"), bool):
|
|
840
|
+
raise _build_client_error(
|
|
841
|
+
server=self.server,
|
|
842
|
+
message="tools/call result.isError must be a boolean when present.",
|
|
843
|
+
stderr_tail=self._diagnostic_tail(),
|
|
844
|
+
error_type=McpClientProtocolError,
|
|
845
|
+
)
|
|
846
|
+
normalized_content, content_summary, extracted_text = _normalize_content_items(
|
|
847
|
+
server=self.server,
|
|
848
|
+
content_items=content,
|
|
849
|
+
stderr_tail=self._diagnostic_tail(),
|
|
850
|
+
)
|
|
851
|
+
self._drain_notifications()
|
|
852
|
+
return McpToolCallResult(
|
|
853
|
+
is_error=is_error,
|
|
854
|
+
structured_content=copy.deepcopy(result.get("structuredContent")),
|
|
855
|
+
content=normalized_content,
|
|
856
|
+
content_summary=content_summary,
|
|
857
|
+
extracted_text=extracted_text,
|
|
858
|
+
raw_payload=copy.deepcopy(result),
|
|
859
|
+
)
|
|
860
|
+
|
|
861
|
+
def call_tool(
|
|
862
|
+
self,
|
|
863
|
+
*,
|
|
864
|
+
tool_name: str,
|
|
865
|
+
arguments: dict[str, Any],
|
|
866
|
+
) -> McpToolCallResult:
|
|
867
|
+
self._require_tools_capability(method="tools/call")
|
|
868
|
+
if not isinstance(arguments, dict):
|
|
869
|
+
raise _build_client_error(
|
|
870
|
+
server=self.server,
|
|
871
|
+
message="tools/call arguments must be an object.",
|
|
872
|
+
stderr_tail=self._diagnostic_tail(),
|
|
873
|
+
error_type=McpClientProtocolError,
|
|
874
|
+
)
|
|
875
|
+
return self._call_tool_once(tool_name=tool_name, arguments=arguments)
|
|
876
|
+
|
|
877
|
+
def close(self) -> None:
|
|
878
|
+
self.transport.close()
|
|
879
|
+
|
|
880
|
+
|
|
881
|
+
class McpStdioClient(_BaseMcpClient):
|
|
882
|
+
def __init__(
|
|
883
|
+
self,
|
|
884
|
+
*,
|
|
885
|
+
server: ResolvedMcpServer,
|
|
886
|
+
workspace_root: Path,
|
|
887
|
+
runtime_kind: RuntimeKind | str | None = None,
|
|
888
|
+
) -> None:
|
|
889
|
+
resolved_workspace_root = workspace_root.resolve()
|
|
890
|
+
request_context = McpServerRequestContext.create(
|
|
891
|
+
server_id=server.id,
|
|
892
|
+
workspace_root=resolved_workspace_root,
|
|
893
|
+
runtime_kind=runtime_kind,
|
|
894
|
+
fallback_runtime_kind=RuntimeKind.SUBAGENT,
|
|
895
|
+
)
|
|
896
|
+
request_handler = McpRootsRequestHandler(server=server)
|
|
897
|
+
super().__init__(
|
|
898
|
+
server=server,
|
|
899
|
+
workspace_root=resolved_workspace_root,
|
|
900
|
+
transport=McpStdioTransport(
|
|
901
|
+
server=server,
|
|
902
|
+
workspace_root=resolved_workspace_root,
|
|
903
|
+
server_request_context=request_context,
|
|
904
|
+
server_request_handler=request_handler,
|
|
905
|
+
),
|
|
906
|
+
transport_error_types=(McpStdioTransportError,),
|
|
907
|
+
runtime_kind=request_context.runtime_kind,
|
|
908
|
+
server_request_context=request_context,
|
|
909
|
+
server_request_handler=request_handler,
|
|
910
|
+
)
|
|
911
|
+
|
|
912
|
+
|
|
913
|
+
class McpHttpClient(_BaseMcpClient):
|
|
914
|
+
transport: McpHttpTransport
|
|
915
|
+
|
|
916
|
+
def __init__(
|
|
917
|
+
self,
|
|
918
|
+
*,
|
|
919
|
+
server: ResolvedMcpServer,
|
|
920
|
+
workspace_root: Path,
|
|
921
|
+
runtime_kind: RuntimeKind | str | None = None,
|
|
922
|
+
) -> None:
|
|
923
|
+
resolved_workspace_root = workspace_root.resolve()
|
|
924
|
+
request_context = McpServerRequestContext.create(
|
|
925
|
+
server_id=server.id,
|
|
926
|
+
workspace_root=resolved_workspace_root,
|
|
927
|
+
runtime_kind=runtime_kind,
|
|
928
|
+
fallback_runtime_kind=RuntimeKind.SUBAGENT,
|
|
929
|
+
)
|
|
930
|
+
request_handler = McpRootsRequestHandler(server=server)
|
|
931
|
+
super().__init__(
|
|
932
|
+
server=server,
|
|
933
|
+
workspace_root=resolved_workspace_root,
|
|
934
|
+
transport=McpHttpTransport(
|
|
935
|
+
server=server,
|
|
936
|
+
workspace_root=resolved_workspace_root,
|
|
937
|
+
server_request_context=request_context,
|
|
938
|
+
server_request_handler=request_handler,
|
|
939
|
+
),
|
|
940
|
+
transport_error_types=(McpHttpTransportError, McpOAuthError),
|
|
941
|
+
runtime_kind=request_context.runtime_kind,
|
|
942
|
+
server_request_context=request_context,
|
|
943
|
+
server_request_handler=request_handler,
|
|
944
|
+
)
|
|
945
|
+
|
|
946
|
+
@property
|
|
947
|
+
def session_negotiated(self) -> bool:
|
|
948
|
+
return self.transport.session_negotiated
|
|
949
|
+
|
|
950
|
+
def _apply_negotiated_protocol_version(self, protocol_version: str) -> None:
|
|
951
|
+
super()._apply_negotiated_protocol_version(protocol_version)
|
|
952
|
+
self.transport.set_negotiated_protocol_version(protocol_version)
|
|
953
|
+
|
|
954
|
+
def _reset_http_session_state(self) -> None:
|
|
955
|
+
self.transport.reset_session_state()
|
|
956
|
+
self._reset_initialization_state()
|
|
957
|
+
|
|
958
|
+
def _reinitialize_after_session_expiry(self) -> None:
|
|
959
|
+
self._reset_http_session_state()
|
|
960
|
+
self.ensure_initialized()
|
|
961
|
+
|
|
962
|
+
def _retry_read_only_request_once(
|
|
963
|
+
self,
|
|
964
|
+
*,
|
|
965
|
+
method: str,
|
|
966
|
+
request_fn: Callable[[], _ReadOnlyRequestResult],
|
|
967
|
+
) -> _ReadOnlyRequestResult:
|
|
968
|
+
for attempt in range(2):
|
|
969
|
+
try:
|
|
970
|
+
return request_fn()
|
|
971
|
+
except McpHttpTransportSessionExpiredError as exc:
|
|
972
|
+
if attempt > 0:
|
|
973
|
+
raise _build_client_error(
|
|
974
|
+
server=self.server,
|
|
975
|
+
message=(
|
|
976
|
+
f"HTTP MCP session expired during '{method}' even after one "
|
|
977
|
+
"reinitialize+retry."
|
|
978
|
+
),
|
|
979
|
+
error_type=McpClientError,
|
|
980
|
+
) from exc
|
|
981
|
+
self._reinitialize_after_session_expiry()
|
|
982
|
+
raise AssertionError("unreachable")
|
|
983
|
+
|
|
984
|
+
def list_tools(self) -> tuple[McpListedTool, ...]:
|
|
985
|
+
self._require_tools_capability(method="tools/list")
|
|
986
|
+
return self._retry_read_only_request_once(
|
|
987
|
+
method="tools/list", request_fn=self._list_tools_once
|
|
988
|
+
)
|
|
989
|
+
|
|
990
|
+
def list_resources(self) -> tuple[McpListedResource, ...]:
|
|
991
|
+
self._require_resources_capability(method="resources/list")
|
|
992
|
+
return self._retry_read_only_request_once(
|
|
993
|
+
method="resources/list",
|
|
994
|
+
request_fn=self._list_resources_once,
|
|
995
|
+
)
|
|
996
|
+
|
|
997
|
+
def list_prompts(self) -> tuple[McpListedPrompt, ...]:
|
|
998
|
+
self._require_prompts_capability(method="prompts/list")
|
|
999
|
+
return self._retry_read_only_request_once(
|
|
1000
|
+
method="prompts/list",
|
|
1001
|
+
request_fn=self._list_prompts_once,
|
|
1002
|
+
)
|
|
1003
|
+
|
|
1004
|
+
def read_resource(
|
|
1005
|
+
self,
|
|
1006
|
+
*,
|
|
1007
|
+
resource_uri: str,
|
|
1008
|
+
allowed_uris: Collection[str] | None = None,
|
|
1009
|
+
) -> McpReadResourceResult:
|
|
1010
|
+
self._require_resources_capability(method="resources/read")
|
|
1011
|
+
normalized_uri = _require_string(
|
|
1012
|
+
resource_uri,
|
|
1013
|
+
server=self.server,
|
|
1014
|
+
context="resources/read uri",
|
|
1015
|
+
)
|
|
1016
|
+
return self._retry_read_only_request_once(
|
|
1017
|
+
method="resources/read",
|
|
1018
|
+
request_fn=lambda: self._read_resource_once(
|
|
1019
|
+
resource_uri=normalized_uri,
|
|
1020
|
+
allowed_uris=allowed_uris,
|
|
1021
|
+
),
|
|
1022
|
+
)
|
|
1023
|
+
|
|
1024
|
+
def get_prompt(
|
|
1025
|
+
self,
|
|
1026
|
+
*,
|
|
1027
|
+
name: str,
|
|
1028
|
+
arguments: dict[str, str] | None = None,
|
|
1029
|
+
) -> McpGetPromptResult:
|
|
1030
|
+
self._require_prompts_capability(method="prompts/get")
|
|
1031
|
+
normalized_name = _require_string(
|
|
1032
|
+
name,
|
|
1033
|
+
server=self.server,
|
|
1034
|
+
context="prompts/get name",
|
|
1035
|
+
)
|
|
1036
|
+
return self._retry_read_only_request_once(
|
|
1037
|
+
method="prompts/get",
|
|
1038
|
+
request_fn=lambda: self._get_prompt_once(
|
|
1039
|
+
name=normalized_name,
|
|
1040
|
+
arguments=arguments,
|
|
1041
|
+
),
|
|
1042
|
+
)
|
|
1043
|
+
|
|
1044
|
+
def call_tool(
|
|
1045
|
+
self,
|
|
1046
|
+
*,
|
|
1047
|
+
tool_name: str,
|
|
1048
|
+
arguments: dict[str, Any],
|
|
1049
|
+
) -> McpToolCallResult:
|
|
1050
|
+
self._require_tools_capability(method="tools/call")
|
|
1051
|
+
if not isinstance(arguments, dict):
|
|
1052
|
+
raise _build_client_error(
|
|
1053
|
+
server=self.server,
|
|
1054
|
+
message="tools/call arguments must be an object.",
|
|
1055
|
+
stderr_tail=self._diagnostic_tail(),
|
|
1056
|
+
error_type=McpClientProtocolError,
|
|
1057
|
+
)
|
|
1058
|
+
try:
|
|
1059
|
+
return self._call_tool_once(tool_name=tool_name, arguments=arguments)
|
|
1060
|
+
except McpHttpTransportSessionExpiredError as exc:
|
|
1061
|
+
reinitialize_error: Exception | None = None
|
|
1062
|
+
try:
|
|
1063
|
+
self._reinitialize_after_session_expiry()
|
|
1064
|
+
except Exception as recovery_exc: # noqa: BLE001
|
|
1065
|
+
if isinstance(recovery_exc, Exception):
|
|
1066
|
+
reinitialize_error = recovery_exc
|
|
1067
|
+
else:
|
|
1068
|
+
reinitialize_error = RuntimeError(str(recovery_exc))
|
|
1069
|
+
message = (
|
|
1070
|
+
"HTTP MCP session expired before or during 'tools/call'; the active call was "
|
|
1071
|
+
"not automatically retried because it may be side-effectful."
|
|
1072
|
+
)
|
|
1073
|
+
if reinitialize_error is not None:
|
|
1074
|
+
message += f" Session reinitialization also failed: {reinitialize_error}"
|
|
1075
|
+
raise _build_client_error(
|
|
1076
|
+
server=self.server,
|
|
1077
|
+
message=message,
|
|
1078
|
+
error_type=McpClientError,
|
|
1079
|
+
) from exc
|
|
1080
|
+
|
|
1081
|
+
|
|
1082
|
+
def _normalize_content_items(
|
|
1083
|
+
*,
|
|
1084
|
+
server: ResolvedMcpServer,
|
|
1085
|
+
content_items: list[Any],
|
|
1086
|
+
stderr_tail: str,
|
|
1087
|
+
) -> tuple[tuple[dict[str, Any], ...], str, str]:
|
|
1088
|
+
normalized: list[dict[str, Any]] = []
|
|
1089
|
+
summaries: list[str] = []
|
|
1090
|
+
text_parts: list[str] = []
|
|
1091
|
+
for index, raw_item in enumerate(content_items):
|
|
1092
|
+
item = _require_object(
|
|
1093
|
+
raw_item,
|
|
1094
|
+
server=server,
|
|
1095
|
+
context=f"tools/call content[{index}]",
|
|
1096
|
+
)
|
|
1097
|
+
item_type = _require_string(
|
|
1098
|
+
item.get("type"),
|
|
1099
|
+
server=server,
|
|
1100
|
+
context=f"tools/call content[{index}].type",
|
|
1101
|
+
)
|
|
1102
|
+
if item_type == "text":
|
|
1103
|
+
text = _require_string(
|
|
1104
|
+
item.get("text"),
|
|
1105
|
+
server=server,
|
|
1106
|
+
context=f"tools/call content[{index}].text",
|
|
1107
|
+
)
|
|
1108
|
+
normalized.append({"type": "text", "text": text, "summary": f"text({len(text)} chars)"})
|
|
1109
|
+
summaries.append(f"text({len(text)} chars)")
|
|
1110
|
+
text_parts.append(text)
|
|
1111
|
+
continue
|
|
1112
|
+
|
|
1113
|
+
summary = f"{item_type} item omitted"
|
|
1114
|
+
payload: dict[str, Any] = {
|
|
1115
|
+
"type": item_type,
|
|
1116
|
+
"summary": summary,
|
|
1117
|
+
}
|
|
1118
|
+
mime_type = item.get("mimeType")
|
|
1119
|
+
if isinstance(mime_type, str) and mime_type.strip():
|
|
1120
|
+
payload["mime_type"] = mime_type.strip()
|
|
1121
|
+
summary = f"{mime_type.strip()} {summary}"
|
|
1122
|
+
payload["summary"] = summary
|
|
1123
|
+
uri = item.get("uri")
|
|
1124
|
+
if isinstance(uri, str) and uri.strip():
|
|
1125
|
+
payload["uri"] = uri.strip()
|
|
1126
|
+
if item_type == "resource":
|
|
1127
|
+
resource = item.get("resource")
|
|
1128
|
+
if isinstance(resource, dict):
|
|
1129
|
+
resource_uri = resource.get("uri")
|
|
1130
|
+
if isinstance(resource_uri, str) and resource_uri.strip():
|
|
1131
|
+
payload["resource_uri"] = resource_uri.strip()
|
|
1132
|
+
resource_mime = resource.get("mimeType")
|
|
1133
|
+
if isinstance(resource_mime, str) and resource_mime.strip():
|
|
1134
|
+
payload["resource_mime_type"] = resource_mime.strip()
|
|
1135
|
+
normalized.append(payload)
|
|
1136
|
+
summaries.append(summary)
|
|
1137
|
+
return tuple(normalized), ", ".join(summaries) or "no content", "\n\n".join(text_parts)
|